Skip to content

feat: the web app talks to the reception (08b) - #55

Merged
pegner-ag merged 8 commits into
mainfrom
feat/08b-web
Oct 18, 2026
Merged

pegner-ag merged 8 commits into
mainfrom
feat/08b-web

Conversation

@pegner-ag

Copy link
Copy Markdown
Owner

Plan 08b (tasks/f1-skeleton/task-08-reception/plan-08b.md, includes the plan commits of #54 with the owner's change to decision 7).

  • Home page: one box with a project picker (remembered), the live conversation with the reception (Markdown replies, numbered options, buttons, a link to each task it started, reports), earlier conversations, the folded exact-task form, the inbox's lists.
  • Conversation page /conversations/{id} (read only unless open on the web); the server's redaction notice links there.
  • Memory /memory/{scope} and Operator preferences /memory/operator: add, edit, pin, done, forget.
  • Server: GET /api/v1/projects (names only, within the cap) and POST /api/v1/memory/{scope} (web session only, secret scan, refused when personal data would raise the item above the author's cap; source = its own MEMORY_NOTED, so the event schema is unchanged).
  • A tap always answers the newest decision, so older replies' taps are disabled.

Tests: full suite 1424 passed / 7 skipped; npm run check, npm test (35). Checked in headless Chromium against a scripted reception (send/Enter, options, Yes/Differently with a correction, Forget, project switch, memory add/refusal/pin, phone width). Reviewer: approve; its suggestions are applied.

Open point for the owner: operator memory is one scope per server, not per operator (as since 08a); fine for a single owner.

🤖 Generated with Claude Code

pegner-ag and others added 8 commits October 17, 2026 18:31
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…on 7)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… item by hand, the shell for the conversation and memory pages (08b task 1)

An item written by hand is the operator's act: its source is its own MEMORY_NOTED, so the event schema stays.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…f the new pages (08b task 2)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…Operator preferences pages (08b tasks 3, 4)

One box with the project picker, the live conversation with its buttons (a tap always answers the newest
decision, so older replies' taps turn off), a conversation's own page, and memory with add, edit, pin, done and
forget. Checked in headless Chromium against a scripted reception.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… allowed place (08b)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…seen projects respect the cap, one abort listener per screen (review of 08b)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"to remember")
if scope_name == memory.OPERATOR_SCOPE and body.kind != "preference":
raise ApiError(400, "INVALID", "the operator's memory holds preferences only")
data_class = "internal" if scope_name == memory.OPERATOR_SCOPE else project_class(server, scope_name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: any slug passes the scope check here. A slug that isn't in ooat.toml and has never been used (e.g. a typo in /memory/dcos) quietly falls back to internal through project_class and creates a new memory scope. That scope can hold a pinned item that steers later tasks. Consider accepting only the slugs that GET /projects would offer this operator, or operator. The class check itself is correct: note() raises the class for personal data, and require_within checks the raised class.

listed = {slug: {"project": slug, "data_class": p.data_class, "write": p.write, "folders": len(p.folders)}
for slug, p in server.config.projects.items() if views.within(p.data_class, who.max_data_class)}
for event in server.runtime().ledger.events(types=["TASK_SUBMITTED", "CONVERSATION_MESSAGE"]):
slug = event["body"].get("project")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (performance): every home page load reads all TASK_SUBMITTED and CONVERSATION_MESSAGE events in the ledger, from every operator. That is fine for F1 with SQLite, but it grows with the ledger. If it becomes a hotspot, filter by actor in the query, or move this to a view (db-dev).

function addForm() {
const kind = h("select", {"aria-label": t("memory.kind")}, ...(preferences ? ["preference"] : ADDABLE)
.map((name) => h("option", {value: name}, t(/** @type {import("../i18n.js").Key} */ (`memory.kind.${name}`)))));
const text = h("textarea", {rows: "2", required: true, maxlength: "2000", placeholder: t("memory.addText"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "2000" copies memory.MAX_TEXT here and at line 91. If the server limit changes, the UI will drift from it without any error. A named constant (or a comment pointing at memory.MAX_TEXT) would make the link visible.

@claude

claude Bot commented Oct 18, 2026

Copy link
Copy Markdown

Verdict: looks good. Only minor suggestions, posted inline.

Security invariants: hold

  • POST /memory/{scope} is refused outside a web session. This matches the existing rule for editing and pinning (design 08 §8).
  • Text that looks like a secret is refused, and the secret is not echoed back in the error (tested).
  • If the text contains personal data, note() raises the item's class. The class is checked against the author's cap with require_within before anything is written, so no text file is left behind without its event (tested, DATA_CLASS_ABOVE_TOKEN).
  • The ledger stays append-only. For an item written by hand, source is its own MEMORY_NOTED id. That is valid under body_memory_noted.source (an event_id), so the event schema does not change (ADR 0018).
  • Markdown replies go through the existing markdown.js, which uses markdown-it with html: false and is the app's only innerHTML.
  • GET /projects returns names only, no folder paths, and limits them to the reader's cap. Projects that are not in ooat.toml are shown only to the operator who used them (tested).
  • No R3 path is touched. No secrets or real personal data in fixtures: the email in the test uses example.cz.

Suggestions (minor)

  1. add_memory accepts any slug, so a typo creates a new internal scope that can hold a pinned item. Consider limiting it to the projects the picker would offer.
  2. list_projects reads every event of those two types on every home page load. Fine for now; worth watching as the ledger grows.
  3. maxlength="2000" in memory.js copies memory.MAX_TEXT by hand.

Docs: docs/description.md now describes the web app and the chat bridge as they are in this PR. The /pamet command it mentions already exists in bridge.py.

Open point for the owner: the operator memory is one scope per server, not one per operator, as the PR description says. That is fine for a single owner, but it needs a decision before multi-operator use.

I could not run pytest in this environment. The tests I read (test_api_conversations.py) cover the new endpoints: the web-only rule, secret scan, cap refusal, kind and scope validation, and project listing.

@pegner-ag
pegner-ag merged commit b6ec1cb into main Oct 18, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant