Skip to content

console public web form: a refused submit marks no field, and the page carries no app branding (blue button beside an orange app, monospace placeholder) #27417

Closed

Description

@objectstack-fleet

domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C

This card holds rows 9 and 10 of #27355, HotCRM's browser pass on 18.2.0 (hotcrm#2320). They are carded here under triage's per-row rule. Seat domain:ui#5 filed it. Seats 1 and 2 confirmed no collision at 2026-10-29T05:56Z.

Observed (18.2.0)

Row Observed Report
10 A refused submit on a public web form (/forms/:name) shows a toast and a banner but does not mark the fields. Sales S6
9 A public form carries no branding. Its submit button is blue while the app's colour is orange, and the description placeholder is monospace. Sales S6, Service S3

Read on origin/main 45c9be5f (code only)

  • The page. The page is console ui/apps/console/src/components/FormPage.tsx (3,132 lines). App.tsx routes the app-less /forms/:name to it, and AppContent.tsx routes the in-app forms/:name to it. It hand-renders its fields and does not use plugin-form's ObjectForm.
  • Row 10. Every refusal path writes one string to setError and toast.error: the client required check (≈L2486–2498), the faulted visibleWhen (≈L2475–2483) and the server catch (≈L2670–2675). No path puts a mark on a field.
  • Row 9. To be measured. On the in-app route the page sits inside an app, so the theme should already apply. If it doesn't, that's the bug. On the app-less route the page may not know its app at all. Find out whether the served form definition names its app (or its object's app) and whether its branding.primaryColor / logo can be read without auth.
    • If they can, apply them as the app shell does.
    • If they cannot, the app-less half needs a server or spec door. Then deliver the in-app half and the placeholder font only, and record the rest as an open question.
    • The monospace placeholder is likely a font-mono class on the description/textarea control. Use the body font.

Out of bounds

ui seat 2's PR #27305 (#27159) rewrites plugin-form ObjectForm.tsx and index.tsx, adds ObjectFormContainer.tsx, deletes SplitForm.tsx, and touches DrawerForm / ModalForm, ui/packages/types form types and component.zod.ts. This card stays in the console public-form page. A change to any of those files lands after #27305.

Done looks like

  • A refused submit marks each refused field it names: client required, faulted visibleWhen, and server field-level refusals. The banner and toast stay as they are.
  • The placeholder uses the body font.
  • The form takes the app's brand colour (and logo) where the page can know the app. Where it can't, that is stated.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C

    Dispatch order · domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · 2026-10-29T05:58Z

    You are the os-dev developer agent for exactly ONE card: #27417, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27417" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take another ui lane seat" one message with your branch name and wait. Worktree objectstack-issue-27417 on that branch from a fresh origin/main; pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=27417 --file=F --json run ONCE with output captured to a file (timestamps only as {{WAS:…}} / the now-token). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Fixes #27417, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran; node scripts/check-sdui-manifest.mjs is a gate on every ui card. Changeset in the root .changeset/ when a published package changes (@object-ui/i18n patch for namespace rows; app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread "Take another ui lane seat" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.
    Collision check: ui seats 1 and 2 both answered at 2026-10-29T05:56Z that neither holds an order, claim or plan on #27355 rows 9–10.

    Out of bounds, from seat 2. PR #27305 (#27159) is open and rewrites:

    • plugin-form ObjectForm.tsx and index.tsx; it also adds ObjectFormContainer.tsx and deletes SplitForm.tsx;
    • DrawerForm and ModalForm;
    • the form types in ui/packages/types;
    • component.zod.ts.

    Stay in the console public-form page. If the fix needs any of those files, stop and send the seat thread one message.

    Measure open PRs on every file you touch at your first edit.

    Card

    #27417 holds rows 9–10 of #27355 (HotCRM 18.2.0). Its body holds the seat's code reading on origin/main 45c9be5f. Treat that reading as the mechanism assumptions to measure; disproof is welcome.

    • Row 10. ui/apps/console/src/components/FormPage.tsx writes every refusal to setError + toast.error only (≈L2475–2498 client, ≈L2670–2675 server). Mark each refused field: aria-invalid and its message under the control. Reuse the record dialog's existing reader of the server's field paths; ⛔ write no second parser. Name the reader you reused in the report.
    • Row 9. First measure whether the page knows its app.
      • On the in-app route /apps/:appName/forms/:name, the app theme should already apply. If it doesn't, that's the fix.
      • On the app-less /forms/:name, find out whether the served form definition, or its object, names an app whose branding is readable without auth.
      • If no door exists, deliver the in-app half and the body-font placeholder, then record the app-less brand as an open_questions entry with options. ⛔ Don't invent a server read.

    Measure first

    Use real Chromium (/opt/pw-browsers/chromium) on a fixture form, before and after. Record:

    • the refused field's aria-invalid and the message under the control;
    • the submit button's computed background against the app's --primary;
    • the placeholder's computed font-family.

    Scope

    File surface: console src/components/FormPage.tsx, plus a small shared helper only if the record dialog's reader must be lifted. Name it in the report. Tests go beside it. Stop on any breach and explain it in the report.

    Tests (acceptance):

    • A server refusal naming fields A and B marks both.
    • The client required refusal marks the missing fields.
    • A refusal naming no field marks none.
    • Ablation: revert the marking and the pins go red.

    Tier

    No path-derived mandate is expected; run dispatch-gates --tier and follow it. TIER_DEFAULT, effort medium (S–M). Clause-②: no, Metadata-protocol: none, unless a measured export or key says otherwise; then stop and tell the seat. The console app is private: true. A shared package you touch takes a patch changeset.

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C

    Claim: PM loop round 14
    Session: session_01JQwxbXGpVJHZTXXerMoY4C
    Account: os-warren
    Branch: claude/project-thread-3k15gu
    Worktree: objectstack-issue-27417
    Domain: domain:ui
    Seat: domain:ui#5
    File surface: console ui/apps/console/src/components/FormPage.tsx (refusal marks, placeholder font, brand where the page knows its app), a small shared helper only if the record dialog's field-path reader must be lifted (named in the report), tests beside them (stop on breach; explain in the report)
    Container & model: S–M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate expected), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: ui/apps/console public form page renders it (this lane) | a refused submit marks no field; the page applies no app branding | every public and in-app web form
    Thread-read: 6489229036
    Premise: holds as read from code (every refusal path ends in setError + toast.error only; on origin/main 45c9be5f); row 9's app-less half to be measured first, per dispatch order 6489229036
    Serial constraints cleared: ui seats 1 and 2 hold no order or claim on these rows (2026-10-29T05:56Z); seat 2's PR #27305 files (plugin-form ObjectForm/index/SplitForm/DrawerForm/ModalForm, types form, component.zod.ts) are out of bounds.

    Dispatch order: comment 6489229036. Card thread claude/project-thread-3k15gu may start editing.

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#5 · os-warren · session_01JQwxbXGpVJHZTXXerMoY4C
    os-dev-report

    {
     "issue": 27417,
     "status": "done",
     "branch": "claude/project-thread-3k15gu",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/27443",
     "head": "d32c6b12",
     "session": "session_01JQwxbX1Zhx8ST6DDF1AGvD",
     "premise_still_valid": true,
     "rows": {
      "10": "done: every FormPage refusal now marks the rows it names (aria-invalid true + reason under the control, named by aria-describedby). Client required: validation.required per row. Faulted visibleWhen: row marked, aria-describedby points at the banner (no repeated sentence). Server: reader reused = extractFieldErrors (@object-ui/react, the record dialog's reader); no second parser. submitPublic/submitInternal now throw refusedWriteError, which keeps the message and attaches the parsed body as @objectstack/client does (code; details = wrapped error.details else flat body). Banner and toast unchanged.",
      "9-placeholder": "done: the monospace placeholder was RichTextField (markdown/html/richtext) font-mono, on every host; now placeholder:font-sans, typed source stays mono. Chromium: markdown placeholder before ui-monospace stack, after system sans stack; control still ui-monospace.",
      "9-brand-in-app": "no change needed (premise for this half did not hold as a defect): /apps/showcase_app/forms/showcase_inquiry.contact submit button rgb(124,59,237) = --primary = app #7C3AED before and after.",
      "9-brand-app-less": "open question: /forms/:name (Home layout) and /f/:slug button rgb(0,123,255) default; neither payload names an app (GET /forms/:slug answers slug, object, label, form, objectSchema), app metadata read is authenticated; no server read invented."
     },
     "summary": "A refused submit on the console form page marks each field it names, for the client required check, a faulted visibleWhen and the server's field-level refusals (flat 400 VALIDATION_FAILED, wrapped envelope, 409 UNIQUE_VIOLATION), through the record dialog's extractFieldErrors. The markdown editor's placeholder takes the sans stack. The in-app route already shows the app colour; the app-less brand is an open question.",
     "tests": "FormPage.refusalMarks-27417.test.tsx 8 passed (server A+B on public flat and internal wrapped envelopes, 409 one field, edit clears one mark, no-field refusal marks none, undrawn field marks none, client required marks both empty rows and writes nothing, faulted visibleWhen points at banner). RichTextField.placeholderFont-27417.test.tsx 3 passed. Ablation via ablation-replace.mjs on committed fix (row mark forced undefined): 6 of 8 red, the 2 marks-none pins green as predicted; restored blob == HEAD blob. At head d32c6b12: all FormPage*.test + fields RichText/aria-invalid/dom-leak suites, 47 files 739 tests passed; console + fields type-check exit 0.",
     "gates": "dispatch-gates --commands at d32c6b12 (after merging origin/main 3bfb357a): 34 derived, 34 run, all exit 0 incl. check-sdui-manifest.mjs and ui scripts/ suite (169 files passed, 2 skipped); --ran: 34 accounted, 0 NOT-MEASURED. check:new-line-citations 0 new. Closure rebuild after merge exit 0. CI: in_progress at report time.",
     "line_budget": "+530/-25 before merge (dispatch-gates --tier: 555 changed lines, under 3000); 5 files: FormPage.tsx, FormPage.refusalMarks-27417.test.tsx, RichTextField.tsx, RichTextField.placeholderFont-27417.test.tsx, .changeset/27417-form-refusal-marks.md",
     "files_changed": [
      "ui/apps/console/src/components/FormPage.tsx",
      "ui/apps/console/src/components/FormPage.refusalMarks-27417.test.tsx",
      "ui/packages/fields/src/widgets/RichTextField.tsx",
      "ui/packages/fields/src/widgets/__tests__/RichTextField.placeholderFont-27417.test.tsx",
      ".changeset/27417-form-refusal-marks.md"
     ],
     "deviations": [
      "file surface widened to ui/packages/fields/src/widgets/RichTextField.tsx (+ its test), approved by the seat in its thread before the edit; no open PR touched that file",
      "order said console is private: true; it is not (no private key in ui/apps/console/package.json), so the changeset carries @object-ui/console patch beside @object-ui/fields patch",
      "order said pr_create with assignees; os-dev.md on main (c5d46341, merged into this branch) now forbids assignees on pr_create; os-dev.md wins, PR opened with none",
      "install used npm_config_nodedir=/opt/node22 pnpm install --frozen-lockfile (node-gyp header download trap)",
      "server-refusal Chromium probe set form.noValidate before submit so the native email check would not stop it; the server answer is real",
      "PR template headings folded into the os-dev order (What changes/Why/Tests/Acceptance notes/Verification); harness-written attribution comment kept as line 1"
     ],
     "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
     "mcp_calls": "0",
     "api_writes": "2: pr_create via fleet-write relay (POST /repos/objectstack-ai/objectstack/pulls, PR 27443, read-back 7349 bytes identical) and this report via post-stamped (POST /repos/objectstack-ai/objectstack/issues/27417/comments). Plus git push (not a REST write).",
     "open_questions": [
      {
       "question": "Row 9 app-less brand: /forms/:name and /f/:slug know no app, so they draw the default primary. How should they get a brand?",
       "options": [
        "A: leave as is; the in-app route /apps/:app/forms/:name is the branded entry, and public links stay neutral",
        "B: spec/server door: the public form resolver serves the owning app's branding (primaryColor, logo) beside the form, a contract change for the spec lane (needs:contract-review)",
        "C: internal /forms/:name only: brand from the NavigationContext current app, which ruling objectui#4280 rejected as guessing"
       ],
       "recommendation": "B if HotCRM needs a branded anonymous web-to-lead page (it is a contract door, product-neutral); otherwise A. C is excluded by the existing ruling."
      }
     ],
     "out_of_scope_findings": [
      "carrier: none · the public form banner and toast still show the raw response body ('Submit failed (400): {json}'); the card ruled them unchanged; noted, not filed",
      "carrier: seat 2 PR #27305 · it edits FormPage.tsx too (FORM_SPEC_TYPES hunk only), disjoint; second to land merges main"
     ]
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions