Deploy AIOS web (React glide grid + FastAPI slice)
Browse files- RELEASES.json +1 -1
- VERSION +1 -1
- web/src/customer-grid/MapView.tsx +36 -1
- web/src/settings/PermsEditor.tsx +18 -0
RELEASES.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"current": "
|
| 3 |
"releases": [
|
| 4 |
{
|
| 5 |
"version": "v29",
|
|
|
|
| 1 |
{
|
| 2 |
+
"current": "624b9af",
|
| 3 |
"releases": [
|
| 4 |
{
|
| 5 |
"version": "v29",
|
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
624b9af
|
web/src/customer-grid/MapView.tsx
CHANGED
|
@@ -384,6 +384,9 @@ export function MapView({
|
|
| 384 |
/** A finished drag must not also read as a click on the pin underneath β the
|
| 385 |
* kanban card's lesson (viewModes.tsx), same fix. */
|
| 386 |
const movedRef = useRef(false);
|
|
|
|
|
|
|
|
|
|
| 387 |
/** Once the user has zoomed or panned, a data change must NOT yank the view
|
| 388 |
* back. Before that, refitting on new data is the helpful behaviour. */
|
| 389 |
const touchedRef = useRef(false);
|
|
@@ -1035,7 +1038,24 @@ export function MapView({
|
|
| 1035 |
if (e.button !== 0 || !view) return;
|
| 1036 |
const { x, y } = localPoint(e.clientX, e.clientY);
|
| 1037 |
movedRef.current = false;
|
| 1038 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1039 |
// Shift (or Ctrl/Cmd) turns the drag into a SELECTION rectangle; a plain
|
| 1040 |
// drag pans. Both gestures are on the same button because a map that
|
| 1041 |
// needs a mode toggle to select is a map people never select on.
|
|
@@ -1048,6 +1068,21 @@ export function MapView({
|
|
| 1048 |
|
| 1049 |
const onPointerMove = useCallback(
|
| 1050 |
(e: ReactPointerEvent<SVGSVGElement>) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
const { x, y } = localPoint(e.clientX, e.clientY);
|
| 1052 |
if (drag) {
|
| 1053 |
// β W37-T30: this used to set `movedRef` UNCONDITIONALLY, and that is what swallowed a
|
|
|
|
| 384 |
/** A finished drag must not also read as a click on the pin underneath β the
|
| 385 |
* kanban card's lesson (viewModes.tsx), same fix. */
|
| 386 |
const movedRef = useRef(false);
|
| 387 |
+
/** Where a gesture began, in CLIENT pixels, so `onPointerMove` can take the pointer capture
|
| 388 |
+
* only once it has become a drag. See the block in `onPointerDown` (W38-T09). */
|
| 389 |
+
const downRef = useRef<{ id: number; cx: number; cy: number } | null>(null);
|
| 390 |
/** Once the user has zoomed or panned, a data change must NOT yank the view
|
| 391 |
* back. Before that, refitting on new data is the helpful behaviour. */
|
| 392 |
const touchedRef = useRef(false);
|
|
|
|
| 1038 |
if (e.button !== 0 || !view) return;
|
| 1039 |
const { x, y } = localPoint(e.clientX, e.clientY);
|
| 1040 |
movedRef.current = false;
|
| 1041 |
+
// ββ THE CAPTURE IS TAKEN LAZILY, IN `onPointerMove`, AND MOVING IT THERE IS THE WHOLE
|
| 1042 |
+
// FIX FOR W38-T09 (found by wave-38 QA on the deployed build). This line used to read
|
| 1043 |
+
// `e.currentTarget.setPointerCapture(e.pointerId)`, unconditionally, on every pointerdown
|
| 1044 |
+
// anywhere in the SVG -- including on a pin. Pointer capture retargets the REST of the
|
| 1045 |
+
// gesture to the capture element, so `pointerup`, `mouseup` and the synthesised `click`
|
| 1046 |
+
// all landed on the `<svg>` and a pin's own `onClick` NEVER RAN. Measured live:
|
| 1047 |
+
// pointerdown -> cg-map-pin mousedown -> cg-map-pin
|
| 1048 |
+
// pointerup -> cg-map-svg mouseup -> cg-map-svg click -> cg-map-svg
|
| 1049 |
+
// β RELEASING ON `pointerup` DOES NOT SAVE IT, and that was the tempting wrong fix: the
|
| 1050 |
+
// release below already ran and the click was still retargeted. Reproduced in a 20-line
|
| 1051 |
+
// isolation page with zero app code, IDENTICALLY in headed and headless Chromium, so it is
|
| 1052 |
+
// browser behaviour rather than a test-harness artifact.
|
| 1053 |
+
// β THE PIN'S HANDLER WAS ALWAYS CORRECT. `onOpen`, `pinSelect`, the drawer and the cohort
|
| 1054 |
+
// path were all provably healthy (keyboard Enter opened the record; shift-drag, which the
|
| 1055 |
+
// SVG handles itself and so is immune to the retargeting, selected 2,381 pins). Only
|
| 1056 |
+
// pointer DELIVERY to a pin was broken -- and it had been since wave 8, thirty waves before
|
| 1057 |
+
// the ticket that wired this handler. `git log -S "setPointerCapture"` returns one commit.
|
| 1058 |
+
downRef.current = { id: e.pointerId, cx: e.clientX, cy: e.clientY };
|
| 1059 |
// Shift (or Ctrl/Cmd) turns the drag into a SELECTION rectangle; a plain
|
| 1060 |
// drag pans. Both gestures are on the same button because a map that
|
| 1061 |
// needs a mode toggle to select is a map people never select on.
|
|
|
|
| 1068 |
|
| 1069 |
const onPointerMove = useCallback(
|
| 1070 |
(e: ReactPointerEvent<SVGSVGElement>) => {
|
| 1071 |
+
// ββ THE LAZY CAPTURE (W38-T09's fix). A gesture only becomes a drag once it has TRAVELLED,
|
| 1072 |
+
// so the capture is taken here rather than on pointerdown. A zero-movement click therefore
|
| 1073 |
+
// never captures and reaches the pin it was aimed at; a pan or a marquee captures the
|
| 1074 |
+
// instant it stops being a click, which is what keeps the pointer inside this handler when
|
| 1075 |
+
// the cursor leaves the SVG mid-drag. Both directions proven in isolation before this
|
| 1076 |
+
// landed: a plain click fires the pin's own `onClick`, and a real drag still logs the
|
| 1077 |
+
// capture and retargets exactly as it did before.
|
| 1078 |
+
// β THE THRESHOLD IS IN CLIENT PIXELS, deliberately unlike the two below it. Those compare
|
| 1079 |
+
// LOCAL (viewBox) coordinates to decide whether a gesture counts as travel for selection;
|
| 1080 |
+
// this one asks whether the BROWSER should stop delivering events to what is under the
|
| 1081 |
+
// cursor, which is a screen-space question at every zoom.
|
| 1082 |
+
const dn = downRef.current;
|
| 1083 |
+
if (dn && dn.id === e.pointerId && !e.currentTarget.hasPointerCapture(e.pointerId)
|
| 1084 |
+
&& Math.abs(e.clientX - dn.cx) + Math.abs(e.clientY - dn.cy) > 3)
|
| 1085 |
+
e.currentTarget.setPointerCapture(e.pointerId);
|
| 1086 |
const { x, y } = localPoint(e.clientX, e.clientY);
|
| 1087 |
if (drag) {
|
| 1088 |
// β W37-T30: this used to set `movedRef` UNCONDITIONALLY, and that is what swallowed a
|
web/src/settings/PermsEditor.tsx
CHANGED
|
@@ -84,6 +84,7 @@ import {
|
|
| 84 |
setAccess,
|
| 85 |
setFilter,
|
| 86 |
setHidden,
|
|
|
|
| 87 |
toPutBody,
|
| 88 |
toggleHidden,
|
| 89 |
} from "./permsModel";
|
|
@@ -733,6 +734,22 @@ export function PermsEditor({
|
|
| 733 |
of JSX here; it is `settings/ModulePermsList.tsx` now, and
|
| 734 |
`manage-agent/ManageAgentPane` mounts the SAME component. Nothing
|
| 735 |
about the list is duplicated, so nothing about it can drift. */}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
<ModulePermsList
|
| 737 |
modules={payload.modules}
|
| 738 |
entries={draft}
|
|
@@ -740,6 +757,7 @@ export function PermsEditor({
|
|
| 740 |
onFilter={(key, next) => setDraft((d) => setFilter(d, key, next))}
|
| 741 |
onToggleHidden={(key, fk) => setDraft((d) => toggleHidden(d, key, fk))}
|
| 742 |
onSetHidden={(key, keys) => setDraft((d) => setHidden(d, key, keys))}
|
|
|
|
| 743 |
userOptions={userOptions}
|
| 744 |
emptyNote="This deployment declares no modules that can be restricted."
|
| 745 |
headExtra={(m) =>
|
|
|
|
| 84 |
setAccess,
|
| 85 |
setFilter,
|
| 86 |
setHidden,
|
| 87 |
+
setMetrics,
|
| 88 |
toPutBody,
|
| 89 |
toggleHidden,
|
| 90 |
} from "./permsModel";
|
|
|
|
| 734 |
of JSX here; it is `settings/ModulePermsList.tsx` now, and
|
| 735 |
`manage-agent/ManageAgentPane` mounts the SAME component. Nothing
|
| 736 |
about the list is duplicated, so nothing about it can drift. */}
|
| 737 |
+
{/* ββ `onMetrics` BELOW IS THE LINE W38-T19 SHIPPED WITHOUT, AND EVERY GATE WAS GREEN
|
| 738 |
+
OVER ITS ABSENCE. The capability was complete on both sides β `perm_scope.may_metrics`,
|
| 739 |
+
four gated read doors, `_clean_perms`'s fourth entry field, `parseEntry`/`toPutBody`,
|
| 740 |
+
and the checkbox itself in `ModulePermsList` β with 39 new gate checks across
|
| 741 |
+
`api_scopes`, `web_login` and `web_ui`. What did not exist was this prop, so the
|
| 742 |
+
component's own `{on && !schemaless && onMetrics}` render gate never fired and the
|
| 743 |
+
toggle the `done-when` names had NO MOUNT SITE anywhere in the product. Found by
|
| 744 |
+
wave-38 QA on the deployed build, by opening the screen and looking.
|
| 745 |
+
β THE PROP IS OPTIONAL AND THAT IS PRECISELY WHAT LET IT SHIP: `tsc` cannot name a
|
| 746 |
+
call site that forgot an optional prop. It STAYS optional because `manage-agent`
|
| 747 |
+
legitimately does not want it (an agent's reads never open a grid door, so the rule
|
| 748 |
+
would be stored and never applied) β so what pins this line is `verify_ui.py`'s
|
| 749 |
+
`metrics_toggle_has_a_mount_site`, not the type system.
|
| 750 |
+
β AND THIS COMMENT SITS HERE, NOT BETWEEN THE ATTRIBUTES, because a JSX comment in
|
| 751 |
+
the attribute list TYPE-CHECKS under `tsc --noEmit` and then EMITS a bare `...` into
|
| 752 |
+
the props object β invalid JS that only the render smoke catches. */}
|
| 753 |
<ModulePermsList
|
| 754 |
modules={payload.modules}
|
| 755 |
entries={draft}
|
|
|
|
| 757 |
onFilter={(key, next) => setDraft((d) => setFilter(d, key, next))}
|
| 758 |
onToggleHidden={(key, fk) => setDraft((d) => toggleHidden(d, key, fk))}
|
| 759 |
onSetHidden={(key, keys) => setDraft((d) => setHidden(d, key, keys))}
|
| 760 |
+
onMetrics={(key, on) => setDraft((d) => setMetrics(d, key, on))}
|
| 761 |
userOptions={userOptions}
|
| 762 |
emptyNote="This deployment declares no modules that can be restricted."
|
| 763 |
headExtra={(m) =>
|