fsanyoto commited on
Commit
4a5c5df
Β·
verified Β·
1 Parent(s): e65eb28

Deploy AIOS web (React glide grid + FastAPI slice)

Browse files
RELEASES.json CHANGED
@@ -1,6 +1,12 @@
1
  {
2
- "current": "2b8e675",
3
  "releases": [
 
 
 
 
 
 
4
  {
5
  "version": "v25",
6
  "sha": "bc24829",
 
1
  {
2
+ "current": "v27 (c05a794)",
3
  "releases": [
4
+ {
5
+ "version": "v27",
6
+ "sha": "c05a794",
7
+ "date": "2026-08-18",
8
+ "subject": "release v27"
9
+ },
10
  {
11
  "version": "v25",
12
  "sha": "bc24829",
VERSION CHANGED
@@ -1 +1 @@
1
- 2b8e675
 
1
+ v27 (c05a794)
api/routes_feedback.py CHANGED
@@ -157,7 +157,19 @@ def list_feedback(tenant: str = "", session: Session = Depends(require_session))
157
  if tenant:
158
  want = str(tenant).strip().lower()
159
  rows = [r for r in rows if str(r.get("tenant") or "").lower() == want]
160
- rows = sorted(rows, key=lambda r: int(r.get("ts") or 0), reverse=True)
 
 
 
 
 
 
 
 
 
 
 
 
161
  out = {"rows": [dict(r) for r in rows], "total": len(rows), "capacity": MAX_ROWS,
162
  "tenants": sorted({str(r.get("tenant") or "") for r in _rows() if r.get("tenant")})}
163
  if len(_rows()) >= MAX_ROWS:
 
157
  if tenant:
158
  want = str(tenant).strip().lower()
159
  rows = [r for r in rows if str(r.get("tenant") or "").lower() == want]
160
+ # β›” THE TIEBREAK IS THE STORE'S OWN ORDER, AND WITHOUT IT "NEWEST FIRST" IS FALSE FOR ANY TWO
161
+ # ROWS FILED IN THE SAME SECOND (wave-35 QA). `ts` is `int(time.time())` β€” SECOND resolution β€”
162
+ # and Python's sort is STABLE, so under `reverse=True` a tie keeps INSERTION order, which is
163
+ # oldest-first: the one direction this route promises never to return. It is not hypothetical
164
+ # and it is not a flake: `api_api`'s R8 row failed on it in the close-out battery, in the
165
+ # wave-35 QA battery and in a solo re-run, and the baseline recorded the true count anyway, so
166
+ # a REAL regression would have read as the known-flake line.
167
+ # ⚠ ENUMERATE OVER THE ALREADY-FILTERED LIST: `_rows()` appends, so a row's index IS its arrival
168
+ # order (the module docstring's "newest LAST in the store"), and `?tenant=` only drops rows, so
169
+ # the surviving indexes stay monotonic. Raising `ts` to sub-second resolution instead would
170
+ # break every id already minted as `fb_<ts>_…`.
171
+ rows = [r for _, r in sorted(enumerate(rows),
172
+ key=lambda p: (int(p[1].get("ts") or 0), p[0]), reverse=True)]
173
  out = {"rows": [dict(r) for r in rows], "total": len(rows), "capacity": MAX_ROWS,
174
  "tenants": sorted({str(r.get("tenant") or "") for r in _rows() if r.get("tenant")})}
175
  if len(_rows()) >= MAX_ROWS:
web/src/customer-grid/CustomerGrid.tsx CHANGED
@@ -1591,10 +1591,26 @@ function CustomerGridSurface({
1591
  // scope, which for a Query surface is the SOURCE database β€” so autosaving a resize would
1592
  // have written a view into the source's own workspace under the artefact's id. The router
1593
  // is what keeps "which store does this spec belong to" answered in one place.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1594
  const routed = routeQueryViewMutation(queryBinding, scope, {
1595
  id: eventId(queryBinding ? "query-view-update" : "view"),
1596
  type: "view_upsert",
1597
- view: updated as unknown as Record<string, unknown>,
1598
  }, { query: mutateQueryWorkspace, native: emitHostEvent });
1599
  if (routed.channel === "refused")
1600
  signal(TOAST_EVENT, routed.refusal?.message ?? "That view change could not be saved.");
 
1591
  // scope, which for a Query surface is the SOURCE database β€” so autosaving a resize would
1592
  // have written a view into the source's own workspace under the artefact's id. The router
1593
  // is what keeps "which store does this spec belong to" answered in one place.
1594
+ // β›”β›” WAVE 35 QA β€” THE SUBJECT OF A QUERY WRITE IS THE ARTEFACT, AND STAMPING IT IS WHAT
1595
+ // MAKES W35-T23 TRUE. This emitted `updated` unchanged, whose `id` is the SOURCE database's
1596
+ // active view (the grid mounts on `gridScopeFor(active.source.database)`, so `views` are the
1597
+ // source's). `routeQueryViewMutation` requires `view.id === binding.artifactId` and the
1598
+ // server requires the same β€” so EVERY spec write on a Query surface was refused before it
1599
+ // left the browser, and the person saw *"A Query view holds one saved answer…"*, the message
1600
+ // written for a genuine duplicate-view attempt.
1601
+ // ⚠ MEASURED, not deduced: resize, sort, group, hide and row height all failed identically
1602
+ // on a pre-wave artefact AND a brand-new one β€” six for six, zero network writes, `edited`
1603
+ // never flipping. The owner's item was *"I should be able to interact in each of the View
1604
+ // under Query as well, exactly like how I would be able to interact with it under Database
1605
+ // view"*, and the router's own docstring already says these are one thing to it.
1606
+ // ⚠ ONLY the emitted subject is re-identified. `setViews` and the tombstone keep using
1607
+ // `updated.id`, because the LOCAL row really is the source's view β€” the artefact id is who
1608
+ // the WRITE is about, not who the grid is showing.
1609
+ const subject = queryBinding ? { ...updated, id: queryBinding.artifactId } : updated;
1610
  const routed = routeQueryViewMutation(queryBinding, scope, {
1611
  id: eventId(queryBinding ? "query-view-update" : "view"),
1612
  type: "view_upsert",
1613
+ view: subject as unknown as Record<string, unknown>,
1614
  }, { query: mutateQueryWorkspace, native: emitHostEvent });
1615
  if (routed.channel === "refused")
1616
  signal(TOAST_EVENT, routed.refusal?.message ?? "That view change could not be saved.");
web/src/customer-grid/useVisibleRows.ts CHANGED
@@ -804,7 +804,20 @@ export function runPipeline(input: PipelineInput): VisibleRowsResult {
804
  // it client-side would rank the page and label the answer with the scope's count β€” the exact
805
  // "12 rows under 'showing 200 of 5,728'" failure this bypass exists to prevent. The refusal
806
  // belongs to `filter_sql`, which maps rank leaves to its UNANSWERABLE tri-state.
807
- if (serverWindowed) return asRows(rawRows);
 
 
 
 
 
 
 
 
 
 
 
 
 
808
 
809
  const fieldByKey = new Map<string, Field>();
810
  for (const f of fields) fieldByKey.set(f.key, f);
@@ -863,12 +876,24 @@ export function runPipeline(input: PipelineInput): VisibleRowsResult {
863
  ctx.rankSets = NO_RANKING.sets;
864
  }
865
 
866
- const members = new Set(memberPids);
867
  out = out.filter(
868
  (r) =>
869
- members.has(r.pid) ||
870
  matchFilterTree(filters, filterConj, r, fieldByKey, ctx)
871
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
872
  }
873
 
874
  const q = search.trim().toLowerCase();
 
804
  // it client-side would rank the page and label the answer with the scope's count β€” the exact
805
  // "12 rows under 'showing 200 of 5,728'" failure this bypass exists to prevent. The refusal
806
  // belongs to `filter_sql`, which maps rank leaves to its UNANSWERABLE tri-state.
807
+ // β›”β›” WAVE 35 QA β€” A CURATED SET IS NOT A FILTER, AND THE BYPASS ABOVE DOES NOT COVER IT.
808
+ // `memberPids` is a list of pids the VIEW names; the SQL window never applied it (measured: the
809
+ // "Starred records" view on `ut_odoo_gl_lines` painted "showing 200 of 978,257" with exactly one
810
+ // record starred). So narrowing by membership here is not the double-narrowing this bypass
811
+ // exists to prevent β€” it is the ONLY narrowing, and skipping it renders a view that promises the
812
+ // starred rows and shows the whole table.
813
+ // ⚠ The honest limit, stated rather than hidden: the window is one PAGE the server chose, so a
814
+ // starred record outside it still will not appear. Showing the starred rows that ARE on the page
815
+ // is strictly better than showing 200 unrelated ones under a heading that says "Starred records",
816
+ // and the real fix is the server applying `memberPids` to the window.
817
+ const curated = memberPids.length ? new Set(memberPids) : null;
818
+ if (serverWindowed) {
819
+ return asRows(curated ? rawRows.filter((r) => curated.has(r.pid)) : rawRows);
820
+ }
821
 
822
  const fieldByKey = new Map<string, Field>();
823
  for (const f of fields) fieldByKey.set(f.key, f);
 
876
  ctx.rankSets = NO_RANKING.sets;
877
  }
878
 
 
879
  out = out.filter(
880
  (r) =>
881
+ (curated ? curated.has(r.pid) : false) ||
882
  matchFilterTree(filters, filterConj, r, fieldByKey, ctx)
883
  );
884
+ } else if (curated) {
885
+ // β›”β›” WAVE 35 QA β€” THE WHOLE NARROWING USED TO LIVE INSIDE `if (filters.length)`, SO A VIEW
886
+ // THAT IS *ONLY* A CURATED SET SHOWED EVERY ROW IN THE TABLE. `memberPids` was designed as
887
+ // pinned rows that "ride the `||` below without matching anything" (the note above) β€” an
888
+ // ADDITION to a filter result β€” and nothing covered the case where the membership IS the view.
889
+ // W35-T40's "Starred records" is exactly that: `filters: []` with `memberPids` non-empty. The
890
+ // server was right every time (`GET /starred/records` returned `ids:[7745], count:1` and the
891
+ // view's own `config.memberPids` carried it); the client threw the answer away and painted
892
+ // "3,634 records". Reproduced three times, on a small database and on a 978k-row one.
893
+ // ⚠ It is an `else if`, not a second unconditional pass: with filters present, membership must
894
+ // stay a UNION (pinned rows ride along), and re-applying it here would turn that union into an
895
+ // intersection and hide every row the filters matched.
896
+ out = out.filter((r) => curated.has(r.pid));
897
  }
898
 
899
  const q = search.trim().toLowerCase();
web/src/index.css CHANGED
@@ -3087,6 +3087,21 @@ body {
3087
  .shell-nav-templates {
3088
  margin: 0 8px 2px;
3089
  width: calc(100% - 16px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3090
  }
3091
  /* β›” W33-T75's law reaches this row too. `.shell-nav.is-rail-loading > *` cannot select it β€” this
3092
  button is a SIBLING of `.shell-nav`, not a child β€” so while `/nav` is in flight it would paint in
@@ -3097,6 +3112,10 @@ body {
3097
  margin: 0 6px 2px;
3098
  width: calc(100% - 12px);
3099
  padding: 7px 10px;
 
 
 
 
3100
  }
3101
  .shell-side.is-collapsed .shell-side-bottom { padding: 8px 2px 10px; }
3102
  .shell-side.is-collapsed .shell-account {
 
3087
  .shell-nav-templates {
3088
  margin: 0 8px 2px;
3089
  width: calc(100% - 16px);
3090
+ /* β›”β›” WAVE 35 QA β€” `margin-top: auto` IS WHAT MAKES THE ROW'S OWN `done-when` TRUE. It ships as a
3091
+ SIBLING of `.shell-side-bottom` for a real behavioural reason (that band's `onClickCapture`
3092
+ swallows a collapsed click and expands the rail instead, which a row with no popover must not
3093
+ inherit) β€” but the band ALSO carries `margin-top: auto`, so the band went to the bottom and
3094
+ this row stayed pinned under Connectors. MEASURED on the deployed build at 1600x1000: the row
3095
+ at y=362, the account button at y=945, a **549 px** gap. The ticket says "the rail's bottom
3096
+ band, directly above the account button" and the owner said "just above our User button".
3097
+ Taking the auto-margin HERE makes this row absorb the free space instead, so it lands against
3098
+ the band's top border and the band's own auto-margin becomes a no-op behind it.
3099
+ ⚠ It must survive `.is-rail-hidden`: while `/nav` is in flight this row is `display: none`, the
3100
+ free space falls back to the band's own `margin-top: auto`, and the account row stays pinned.
3101
+ ⚠ The gate that was green over this compared SOURCE-STRING INDEXES (`verify_ui.py`:
3102
+ `_shell_code.find("shell-nav-templates")` vs the account marker) β€” source order cannot see an
3103
+ auto margin, so it read "above" for a row 549 px away. [[gate-pins-a-spelling-not-a-claim]] */
3104
+ margin-top: auto;
3105
  }
3106
  /* β›” W33-T75's law reaches this row too. `.shell-nav.is-rail-loading > *` cannot select it β€” this
3107
  button is a SIBLING of `.shell-nav`, not a child β€” so while `/nav` is in flight it would paint in
 
3112
  margin: 0 6px 2px;
3113
  width: calc(100% - 12px);
3114
  padding: 7px 10px;
3115
+ /* The collapsed rule RE-SETS the whole margin shorthand, so it re-sets `margin-top` to 0 with it
3116
+ β€” the fix above would hold expanded and silently revert the moment the rail folded. Restated
3117
+ here rather than reordered, because the shorthand is what carries the collapsed gutters. */
3118
+ margin-top: auto;
3119
  }
3120
  .shell-side.is-collapsed .shell-side-bottom { padding: 8px 2px 10px; }
3121
  .shell-side.is-collapsed .shell-account {
web/src/shell/Shell.tsx CHANGED
@@ -924,7 +924,18 @@ function ShellFrame() {
924
  */
925
  const VIEW_OPEN_RETRY_MS = [0, 250, 700, 1500, 2600, 5000, 9000, 14000, 21000] as const;
926
  const openViewInDatabase = useCallback((database: string, viewId: string) => {
927
- window.location.hash = `#/db/${database}`;
 
 
 
 
 
 
 
 
 
 
 
928
  viewEmitCancel.current?.();
929
  viewEmitCancel.current = retryEmit(
930
  () => {
@@ -2222,7 +2233,20 @@ function ShellFrame() {
2222
  <button
2223
  type="button"
2224
  ref={dbButton}
2225
- className={"shell-nav-item shell-nav-db" + (dbAt ? " is-active" : "")}
 
 
 
 
 
 
 
 
 
 
 
 
 
2226
  aria-haspopup="dialog"
2227
  aria-expanded={!!dbAt}
2228
  onMouseEnter={navCollapsed ? tipEnter("Database") : undefined}
 
924
  */
925
  const VIEW_OPEN_RETRY_MS = [0, 250, 700, 1500, 2600, 5000, 9000, 14000, 21000] as const;
926
  const openViewInDatabase = useCallback((database: string, viewId: string) => {
927
+ // β›”β›” WAVE 35 QA β€” THIS WROTE `#/db/${database}` AND NOTHING IN THE APP READS A `db/` PREFIX,
928
+ // SO EVERY STARRED-VIEW TILE ON HOME AND ON STARRED WAS A DEAD CLICK. `routeKeyOf` strips only
929
+ // a leading `#/` (`nav.ts`), so the route became the literal `"db/ut_odoo_invoices"`;
930
+ // `resolveRoute` looks that up against entry keys that are BARE (`"ut_odoo_invoices"`), missed,
931
+ // and the shell fell back to Home β€” the URL changed, the screen did not. Reproduced on two
932
+ // different views, held for 30 s+, and confirmed structurally: `"db/"` appeared exactly ONCE
933
+ // in `Shell.tsx` + `nav.ts` + `dbFrame.tsx` combined β€” here, the writer, with no reader
934
+ // anywhere. Every other hash write in this file is `#/${key}` (`:1764`, `:2934`, …); this one
935
+ // invented a second vocabulary for the same thing. [[one-question-two-normalizers]]
936
+ // ⚠ The failure is silent by construction, which is why no gate saw it: a route that does not
937
+ // resolve is the SAME code path as "no route yet", and that path renders Home on purpose.
938
+ window.location.hash = `#/${database}`;
939
  viewEmitCancel.current?.();
940
  viewEmitCancel.current = retryEmit(
941
  () => {
 
2233
  <button
2234
  type="button"
2235
  ref={dbButton}
2236
+ // β›”β›” WAVE 35 QA β€” `dbAt` ALONE IS THE FLYOUT ANCHOR, NOT THE CURRENT SURFACE, AND
2237
+ // THAT MADE W35-T03 FALSE ON THE ONE ROW A PERSON SPENDS MOST OF THEIR DAY IN.
2238
+ // Measured on the deployed build: standing on "Odoo customers", `.shell-nav-db` read
2239
+ // exactly `shell-nav-item shell-nav-db` β€” every rail row painted identically plain,
2240
+ // nothing marking Database as current. It lit up only while the flyout was manually
2241
+ // REOPENED, which is a click the ticket's "at a glance" does not allow for. Six of the
2242
+ // seven rows in that clause were right; this one was gated on the wrong fact.
2243
+ // ⭐ THE FLYOUT'S OWN ROWS ALREADY HAD THE RIGHT ANSWER (`active.key === item.key`,
2244
+ // below) β€” the button was the only thing in the rail reading a different question.
2245
+ // So this asks the SAME question the rows ask: is the resolved route one of the
2246
+ // databases this flyout lists? [[one-evaluator-per-question]]
2247
+ className={"shell-nav-item shell-nav-db"
2248
+ + (dbAt || (!!active && dbEntries.some((e) => e.key === active.key))
2249
+ ? " is-active" : "")}
2250
  aria-haspopup="dialog"
2251
  aria-expanded={!!dbAt}
2252
  onMouseEnter={navCollapsed ? tipEnter("Database") : undefined}