Deploy AIOS web (React glide grid + FastAPI slice)
Browse files- RELEASES.json +7 -1
- VERSION +1 -1
- web/src/customer-grid/MapView.tsx +15 -8
- web/src/customer-grid/useCustomerData.ts +28 -6
RELEASES.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
| 1 |
{
|
| 2 |
-
"current": "
|
| 3 |
"releases": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
{
|
| 5 |
"version": "v50",
|
| 6 |
"sha": "8027c83",
|
|
|
|
| 1 |
{
|
| 2 |
+
"current": "v51 (5740d20)",
|
| 3 |
"releases": [
|
| 4 |
+
{
|
| 5 |
+
"version": "v51",
|
| 6 |
+
"sha": "5740d20",
|
| 7 |
+
"date": "2026-08-23",
|
| 8 |
+
"subject": "release v51"
|
| 9 |
+
},
|
| 10 |
{
|
| 11 |
"version": "v50",
|
| 12 |
"sha": "8027c83",
|
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
v51 (5740d20)
|
web/src/customer-grid/MapView.tsx
CHANGED
|
@@ -1445,17 +1445,24 @@ export function MapView({
|
|
| 1445 |
*/
|
| 1446 |
const addableRecords = useMemo(() => {
|
| 1447 |
if (!allRows || allRows.length === 0) return [];
|
| 1448 |
-
const named: { pid: number; title: string }[] = [];
|
| 1449 |
-
const
|
| 1450 |
for (const r of allRows) {
|
| 1451 |
if (selectedPids.has(r.pid)) continue;
|
| 1452 |
-
const
|
| 1453 |
-
|
| 1454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1455 |
}
|
| 1456 |
-
named.sort((a, b) => a.
|
| 1457 |
-
|
| 1458 |
-
return [...named, ...
|
| 1459 |
}, [allRows, selectedPids, field.key]);
|
| 1460 |
|
| 1461 |
const moveStop = useCallback(
|
|
|
|
| 1445 |
*/
|
| 1446 |
const addableRecords = useMemo(() => {
|
| 1447 |
if (!allRows || allRows.length === 0) return [];
|
| 1448 |
+
const named: { pid: number; title: string; sort: string }[] = [];
|
| 1449 |
+
const rest: { pid: number; title: string }[] = [];
|
| 1450 |
for (const r of allRows) {
|
| 1451 |
if (selectedPids.has(r.pid)) continue;
|
| 1452 |
+
const raw = String(r[field.key] ?? "").trim();
|
| 1453 |
+
// β THE FIRST LETTER OR DIGIT ONWARD IS WHAT A PERSON ALPHABETISES BY. Leading
|
| 1454 |
+
// punctuation is not part of a name, so `'AMBIANCE` and `AMBIANCE` file together instead
|
| 1455 |
+
// of the apostrophe deciding.
|
| 1456 |
+
const sort = raw.replace(/^[^\p{L}\p{N}]+/u, "");
|
| 1457 |
+
if (sort) named.push({ pid: r.pid, title: raw, sort });
|
| 1458 |
+
// β no letter and no digit anywhere: `?`, `-`, `--`. Not a name; it is a record whose
|
| 1459 |
+
// name was never filled in, wearing a placeholder somebody typed.
|
| 1460 |
+
else if (raw) rest.push({ pid: r.pid, title: `${raw} (record ${r.pid})` });
|
| 1461 |
+
else rest.push({ pid: r.pid, title: `Unnamed record ${r.pid}` });
|
| 1462 |
}
|
| 1463 |
+
named.sort((a, b) => a.sort.localeCompare(b.sort));
|
| 1464 |
+
rest.sort((a, b) => a.pid - b.pid);
|
| 1465 |
+
return [...named.map(({ pid, title }) => ({ pid, title })), ...rest];
|
| 1466 |
}, [allRows, selectedPids, field.key]);
|
| 1467 |
|
| 1468 |
const moveStop = useCallback(
|
web/src/customer-grid/useCustomerData.ts
CHANGED
|
@@ -77,7 +77,8 @@ function readHostPayload(): CustomersPayload | null {
|
|
| 77 |
* a measure column populate, a measure condition resolve, and the assignee picker fill,
|
| 78 |
* without refetching the heavy pool. `derived` is keyed by String(pid) (JSON object keys).
|
| 79 |
*/
|
| 80 |
-
function withWorkspace(payload: CustomersPayload, ws: GridWorkspace
|
|
|
|
| 81 |
const out: CustomersPayload = { ...payload, workspace: ws };
|
| 82 |
// 2026-08-04 β the FIELD CONTRACT rides this cheap call too. The derived "Locked views"
|
| 83 |
// column exists only once the caller owns a cohort, so creating the first one changes the
|
|
@@ -87,7 +88,25 @@ function withWorkspace(payload: CustomersPayload, ws: GridWorkspace): CustomersP
|
|
| 87 |
// downstream (CustomerGrid's live adopt), and a fresh array on every echo would re-run
|
| 88 |
// that effect for no reason. An empty/absent list keeps what we already have rather than
|
| 89 |
// blanking the table β absent means "an older host", never "this table has no columns".
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
JSON.stringify(ws.fields) !== JSON.stringify(payload.fields))
|
| 92 |
out.fields = ws.fields;
|
| 93 |
if (Array.isArray(ws.measures)) out.measures = ws.measures;
|
|
@@ -111,9 +130,9 @@ function withWorkspace(payload: CustomersPayload, ws: GridWorkspace): CustomersP
|
|
| 111 |
const extra = ws.limits.filter((l) => l && l.subject && !seen.has(l.subject));
|
| 112 |
if (extra.length) out.limits = [...(payload.limits ?? []), ...extra];
|
| 113 |
}
|
| 114 |
-
if (ws.viewer && typeof ws.viewer.name === "string") out.viewer = ws.viewer;
|
| 115 |
-
if (Array.isArray(ws.userOptions)) out.userOptions = ws.userOptions;
|
| 116 |
-
if (Array.isArray(ws.permissionUserOptions)) out.permissionUserOptions = ws.permissionUserOptions;
|
| 117 |
const derived = ws.derived;
|
| 118 |
if (derived && typeof derived === "object" && Object.keys(derived).length > 0) {
|
| 119 |
out.rows = payload.rows.map((r) => {
|
|
@@ -454,7 +473,10 @@ export function useCustomerData(
|
|
| 454 |
wsPromise,
|
| 455 |
]);
|
| 456 |
if (cancelled || seq !== windowSeq.current || !payload) return;
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
| 458 |
}
|
| 459 |
|
| 460 |
load();
|
|
|
|
| 77 |
* a measure column populate, a measure condition resolve, and the assignee picker fill,
|
| 78 |
* without refetching the heavy pool. `derived` is keyed by String(pid) (JSON object keys).
|
| 79 |
*/
|
| 80 |
+
function withWorkspace(payload: CustomersPayload, ws: GridWorkspace,
|
| 81 |
+
adoptFields = true): CustomersPayload {
|
| 82 |
const out: CustomersPayload = { ...payload, workspace: ws };
|
| 83 |
// 2026-08-04 β the FIELD CONTRACT rides this cheap call too. The derived "Locked views"
|
| 84 |
// column exists only once the caller owns a cohort, so creating the first one changes the
|
|
|
|
| 88 |
// downstream (CustomerGrid's live adopt), and a fresh array on every echo would re-run
|
| 89 |
// that effect for no reason. An empty/absent list keeps what we already have rather than
|
| 90 |
// blanking the table β absent means "an older host", never "this table has no columns".
|
| 91 |
+
// ββ `adoptFields` β MEASURED ON runloopable.com, 2026-08-23, AND IT IS WHY A SAVED ROUTE
|
| 92 |
+
// NEVER APPEARED. The LOAD effect fetches rows fresh and the workspace with
|
| 93 |
+
// `allowCached: true` (W31-T21's first-paint memo). Both envelopes are built by the SAME
|
| 94 |
+
// `grid_assembly`, so on that path the ROWS payload's contract is the newer one β and this
|
| 95 |
+
// line was handing the older, memoised list the last word over it.
|
| 96 |
+
//
|
| 97 |
+
// The sequence, all four steps observed: `POST /customers/route-order` returns 200 with the
|
| 98 |
+
// new column; the client re-reads and `/customers` answers **53 fields** while the memo still
|
| 99 |
+
// holds **52**; this merge overwrites 53 with 52; `CustomerGrid.adoptNewFields` is then handed
|
| 100 |
+
// a field list that does not contain the column and correctly adopts nothing. Proven by
|
| 101 |
+
// dispatching a second `WORKSPACE_STALE_EVENT` by hand two seconds later β `reread()` takes no
|
| 102 |
+
// options, so it reads fresh, and the panel went 52 -> 53 on the spot.
|
| 103 |
+
//
|
| 104 |
+
// β `reread()` KEEPS THE DEFAULT, and that is the whole point of making this a parameter
|
| 105 |
+
// rather than deleting the adopt: on the post-write path there is no rows call at all, and
|
| 106 |
+
// this cheap envelope is the ONLY carrier of a contract change (the derived "Locked views"
|
| 107 |
+
// column, which appears the moment a user owns their first cohort). One path has a fresher
|
| 108 |
+
// source and one does not; the rule is which, not whether.
|
| 109 |
+
if (adoptFields && Array.isArray(ws.fields) && ws.fields.length > 0 &&
|
| 110 |
JSON.stringify(ws.fields) !== JSON.stringify(payload.fields))
|
| 111 |
out.fields = ws.fields;
|
| 112 |
if (Array.isArray(ws.measures)) out.measures = ws.measures;
|
|
|
|
| 130 |
const extra = ws.limits.filter((l) => l && l.subject && !seen.has(l.subject));
|
| 131 |
if (extra.length) out.limits = [...(payload.limits ?? []), ...extra];
|
| 132 |
}
|
| 133 |
+
if (ws.viewer && typeof ws.viewer.name === "string") out.viewer = ws.viewer;
|
| 134 |
+
if (Array.isArray(ws.userOptions)) out.userOptions = ws.userOptions;
|
| 135 |
+
if (Array.isArray(ws.permissionUserOptions)) out.permissionUserOptions = ws.permissionUserOptions;
|
| 136 |
const derived = ws.derived;
|
| 137 |
if (derived && typeof derived === "object" && Object.keys(derived).length > 0) {
|
| 138 |
out.rows = payload.rows.map((r) => {
|
|
|
|
| 473 |
wsPromise,
|
| 474 |
]);
|
| 475 |
if (cancelled || seq !== windowSeq.current || !payload) return;
|
| 476 |
+
// β `false`: this call has just fetched `payload` FROM THE SERVER, and the workspace
|
| 477 |
+
// beside it may be a memo hit. See `withWorkspace`'s `adoptFields` note β letting the
|
| 478 |
+
// cached list win here is what kept a newly created column off the screen until reload.
|
| 479 |
+
setData(workspace ? withWorkspace(payload, workspace, false) : payload);
|
| 480 |
}
|
| 481 |
|
| 482 |
load();
|