fsanyoto commited on
Commit
a6313df
Β·
verified Β·
1 Parent(s): ffabd96

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
RELEASES.json CHANGED
@@ -1,6 +1,12 @@
1
  {
2
- "current": "v50 (8027c83)",
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
- v50 (8027c83)
 
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 unnamed: { pid: number; title: string }[] = [];
1450
  for (const r of allRows) {
1451
  if (selectedPids.has(r.pid)) continue;
1452
- const title = String(r[field.key] ?? "").trim();
1453
- if (title) named.push({ pid: r.pid, title });
1454
- else unnamed.push({ pid: r.pid, title: `Unnamed record ${r.pid}` });
 
 
 
 
 
 
 
1455
  }
1456
- named.sort((a, b) => a.title.localeCompare(b.title));
1457
- unnamed.sort((a, b) => a.pid - b.pid);
1458
- return [...named, ...unnamed];
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): CustomersPayload {
 
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
- if (Array.isArray(ws.fields) && ws.fields.length > 0 &&
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- setData(workspace ? withWorkspace(payload, workspace) : payload);
 
 
 
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();