Gate blocked actions behind an in-place terms acceptance dialog

A member whose account has not accepted the terms in force now gets one
dialog on the action they attempted instead of a dead-end refusal. The
client handler is the single TermsGate, wired into every Http POST helper
so the four optimistic controllers cannot swallow the gate into an error
flash, and the original request is replayed once the acceptance is
recorded. Reading the site and deleting an account stay unblocked.

apple.md is the source brief the compliance research documents reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
retoor 2026-08-09 11:25:57 +02:00
parent 8e9d3fad98
commit 91fac7fd67
38 changed files with 393 additions and 58 deletions

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -40,10 +40,10 @@ The already-established **choke points** (the public function is a thin wrapper
- **C. Cross-reference before every change.** A funnel is called from many sites; deferring inside it changes ALL of them. Find every caller and confirm none depends on the side-effect's result synchronously. If even one does, do not defer the funnel. - **C. Cross-reference before every change.** A funnel is called from many sites; deferring inside it changes ALL of them. Find every caller and confirm none depends on the side-effect's result synchronously. If even one does, do not defer the funnel.
- **D. Zero degradation.** A fix must never reduce functionality, weaken a check, or change observable behavior beyond moving WHEN a side-effect runs. Deferral is best-effort and must NEVER raise into the caller. If the only fix would degrade or risk stale reads, record it unfixed with the safe path forward. - **D. Zero degradation.** A fix must never reduce functionality, weaken a check, or change observable behavior beyond moving WHEN a side-effect runs. Deferral is best-effort and must NEVER raise into the caller. If the only fix would degrade or risk stale reads, record it unfixed with the safe path forward.
- **E. Dig deep.** Pursue the root cause; gather more evidence rather than guessing or bailing. - **E. Dig deep.** Pursue the root cause; gather more evidence rather than guessing or bailing.
- **F. Verify your own work.** After each edit, re-read the changed region, re-check the callers, confirm `python -c "from devplacepy.main import app"` still imports clean, and run `hawk .`. - **F. Verify your own work.** After each edit, re-read the changed region, re-check the callers, confirm `python -c "from devplacepy.main import app"` still imports clean, and re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates).
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, confirm the app imports clean, then run `hawk .` and confirm it passes. **HARD GUARDRAIL: never run the test suite (no `make test`, no `pytest`); never perform any git write operation.** Validate by clean import + hawk + an em-dash scan only. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, confirm the app imports clean, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm they pass. **HARD GUARDRAIL: never run the test suite (no `make test`, no `pytest`); never perform any git write operation.** Validate by clean import + the per-language checks + an em-dash scan only.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); full typing on functions you add; keep `retoor <retoor@molodetz.nl>` as the first line of any source file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); full typing on functions you add; keep `retoor <retoor@molodetz.nl>` as the first line of any source file you create.
@ -78,4 +78,4 @@ FIX: move the side-effect into a thin public wrapper that `background.submit(_wo
- **wrong-tool**: grep `background.submit(` for any argument that is an `async def`/coroutine function, and any external-client call (gitea/push/AI) deferred via the sync queue. - **wrong-tool**: grep `background.submit(` for any argument that is an `async def`/coroutine function, and any external-client call (gitea/push/AI) deferred via the sync queue.
## Output ## Output
Return a markdown report: a one-line summary, then one bullet per finding with severity (`error`/`warning`/`info`), `file:line`, the rule name (e.g. `missing-deferral`, `double-wrap`, `unsafe-deferral`, `bypass-funnel`, `wrong-tool`, `captured-request`, `broken-wiring`), the message, and (in fix mode) whether it was fixed. End with the verification you ran (clean import, `hawk .`, em-dash scan) and its result. Never claim the test suite was run. Return a markdown report: a one-line summary, then one bullet per finding with severity (`error`/`warning`/`info`), `file:line`, the rule name (e.g. `missing-deferral`, `double-wrap`, `unsafe-deferral`, `bypass-funnel`, `wrong-tool`, `captured-request`, `broken-wiring`), the message, and (in fix mode) whether it was fixed. End with the verification you ran (clean import, the per-language checks, em-dash scan) and its result. Never claim the test suite was run.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -56,11 +56,11 @@ When a layer is intentionally absent (an internal route with no public docs, a r
No comments or docstrings in source you author; full typing on every signature and variable; `pathlib` over `os`; dataclasses over fixed-key dicts; no magic numbers; no version pinning; no em-dashes anywhere (use a hyphen) in any file you touch. Keep `retoor <retoor@molodetz.nl>` as the first line (correct comment style for the language) of any NEW source file you create - never of the existing files you edit, and never inside a `.md` with YAML frontmatter. No comments or docstrings in source you author; full typing on every signature and variable; `pathlib` over `os`; dataclasses over fixed-key dicts; no magic numbers; no version pinning; no em-dashes anywhere (use a hyphen) in any file you touch. Keep `retoor <retoor@molodetz.nl>` as the first line (correct comment style for the language) of any NEW source file you create - never of the existing files you edit, and never inside a `.md` with YAML frontmatter.
## Validation (after implementing; never skip) ## Validation (after implementing; never skip)
There is NO `hawk` or validator binary in this environment - validate each touched file directly, using the Python interpreter where `import devplacepy` resolves its dependencies (verify that first; the repo `.venv` may be incomplete). Then: confirm `python -c "from devplacepy.main import app"` imports clean; compile or parse every touched language (`python -m py_compile <files>` for Python, `node --check <file>` for JS, brace balance for CSS, tag and `{% %}`/`{{ }}` balance for templates); and grep every touched file for em-dashes - the character AND the entity forms `&mdash;`/`&#8212;`/`&#x2014;` - confirming none. For any new `*Out` schema, `model_validate` it against a representative context dict so a key mismatch surfaces now, not at request time. Do NOT run the test suite. Then hand off: name which maintainer dimensions are most relevant to the change (e.g. fanout, security, dry, docs, seo, audit, frontend, style, test) so the fleet can verify it. There is NO validator binary in this environment - validate each touched file directly, using the Python interpreter where `import devplacepy` resolves its dependencies (verify that first; the repo `.venv` may be incomplete). Then: confirm `python -c "from devplacepy.main import app"` imports clean; compile or parse every touched language (`python -m py_compile <files>` for Python, `node --check <file>` for JS, brace balance for CSS, tag and `{% %}`/`{{ }}` balance for templates); and grep every touched file for em-dashes - the character AND the entity forms `&mdash;`/`&#8212;`/`&#x2014;` - confirming none. For any new `*Out` schema, `model_validate` it against a representative context dict so a key mismatch surfaces now, not at request time. Do NOT run the test suite. Then hand off: name which maintainer dimensions are most relevant to the change (e.g. fanout, security, dry, docs, seo, audit, frontend, style, test) so the fleet can verify it.
## Live verification of UI/API changes (mandatory for visual work) ## Live verification of UI/API changes (mandatory for visual work)
A structurally valid template can still render broken - `hawk` and the import check never open a browser. Per CLAUDE.md this project treats live verification as non-negotiable for any layout, styling, component, responsive, or backend change: A structurally valid template can still render broken - the static checks and the import check never open a browser. Per CLAUDE.md this project treats live verification as non-negotiable for any layout, styling, component, responsive, or backend change:
- Do not assume any verification CLI is installed (`hawk`, `mole`, `falcon`, `hound` are NOT present here); check with `command -v` first and fall back to the steps below or the project's `screenshot`/`serve`/`validate` skills when they exist. - Do not assume any verification CLI is installed (`mole`, `falcon`, `hound` are NOT present here); check with `command -v` first and fall back to the steps below or the project's `screenshot`/`serve`/`validate` skills when they exist.
- When your change touches `templates/` or `static/`, the rendered result MUST be visually verified: start the dev server (`make dev` in the background; confirm it is healthy on `http://localhost:10500`), capture each new/changed route with headless Playwright (`wait_until="domcontentloaded"`), and inspect the screenshot against the intended UI and the surrounding design system (tokens, spacing, responsiveness). Tear down any server you started. - When your change touches `templates/` or `static/`, the rendered result MUST be visually verified: start the dev server (`make dev` in the background; confirm it is healthy on `http://localhost:10500`), capture each new/changed route with headless Playwright (`wait_until="domcontentloaded"`), and inspect the screenshot against the intended UI and the surrounding design system (tokens, spacing, responsiveness). Tear down any server you started.
- When your change touches `routers/`, verify the endpoints over HTTP against the live server (an api-spec runner if available, otherwise `curl`/`httpx` asserting the status and a body fragment). - When your change touches `routers/`, verify the endpoints over HTTP against the live server (an api-spec runner if available, otherwise `curl`/`httpx` asserting the status and a body fragment).
- The `/feature` workflow performs this live `Verify` phase for you automatically; when you are invoked standalone for UI/API work, perform it yourself before declaring the work complete, or explicitly state it is the caller's responsibility and name the routes to check. - The `/feature` workflow performs this live `Verify` phase for you automatically; when you are invoked standalone for UI/API work, perform it yourself before declaring the work complete, or explicitly state it is the caller's responsibility and name the routes to check.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Visual judgement is out of scope for auto-fix and is recorded as a finding. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Visual judgement is out of scope for auto-fix and is recorded as a finding. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -30,7 +30,7 @@ All application code is under `devplacepy/`: `devplacepy/routers/`, `devplacepy/
- **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers. - **F. Verify your own work.** After each edit, re-read the changed region and re-check the consumers.
## Mode ## Mode
Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then run `hawk .` and confirm it passes. A rename is auto-applied ONLY for a confirmed local/private name that passed the decision algorithm AND only after you grep and update every reference in the same run. Never run the test suite; never perform any git write operation. Default to **REPORT** mode: record findings, do NOT modify files. Apply **FIX** mode only when the invocation explicitly asks you to fix. In FIX mode: reconfirm each finding survives refutation, run the cross-reference impact check, apply a minimal idiomatic root-cause fix, re-read the change, then re-validate every file you touched with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and confirm `python -c "from devplacepy.main import app"` still imports clean. A rename is auto-applied ONLY for a confirmed local/private name that passed the decision algorithm AND only after you grep and update every reference in the same run. Never run the test suite; never perform any git write operation.
## Obey the rules you enforce ## Obey the rules you enforce
No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create. No comments or docstrings in source you author; no em-dashes (use a hyphen); keep `retoor <retoor@molodetz.nl>` as the first line of any file you create.

View File

@ -12,4 +12,4 @@ Follow the audit-log design (`devplacepy/services/audit/`); confirm against the
3. Call the recorder on the mutation's success path: `audit.record(request, event_key, ...)` in HTTP or WebSocket handlers, or `audit.record_system(event_key, ...)` in request-less contexts (services, jobs, CLI). On a guard or denial branch pass `result="denied"`; on a failure branch pass `result="failure"`. 3. Call the recorder on the mutation's success path: `audit.record(request, event_key, ...)` in HTTP or WebSocket handlers, or `audit.record_system(event_key, ...)` in request-less contexts (services, jobs, CLI). On a guard or denial branch pass `result="denied"`; on a failure branch pass `result="failure"`.
4. Route through the existing DRY choke point when one applies (`content.py`, the `project_files.py` helpers, `routers/containers.py` `_audit_instance`, the Devii dispatcher `_audit_mechanic`) instead of scattering call sites. The HTTP path and the Devii path for one mutation must stay disjoint (no double counting). 4. Route through the existing DRY choke point when one applies (`content.py`, the `project_files.py` helpers, `routers/containers.py` `_audit_instance`, the Devii dispatcher `_audit_mechanic`) instead of scattering call sites. The HTTP path and the Devii path for one mutation must stay disjoint (no double counting).
5. Recording is best-effort: wrap nothing the caller depends on, and NEVER gate the audited action on the record succeeding. 5. Recording is best-effort: wrap nothing the caller depends on, and NEVER gate the audited action on the record succeeding.
6. Validate with `hawk` on the touched files and `python -c "from devplacepy.main import app"`. 6. Validate the touched files with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and `python -c "from devplacepy.main import app"`.

View File

@ -10,4 +10,4 @@ Follow the docs convention exactly (confirm against `devplacepy/routers/docs/pag
1. Create `devplacepy/templates/docs/<slug>.html` as a prose page: one `<div class="docs-content" data-render> ... </div>` containing GitHub-flavored markdown. The page is rendered server-side. Any example component markup INSIDE the data-render block must be HTML-escaped (`&lt;dp-...&gt;`); a live demo, if any, goes in a SEPARATE block OUTSIDE the data-render div with its own `<script type="module">`. 1. Create `devplacepy/templates/docs/<slug>.html` as a prose page: one `<div class="docs-content" data-render> ... </div>` containing GitHub-flavored markdown. The page is rendered server-side. Any example component markup INSIDE the data-render block must be HTML-escaped (`&lt;dp-...&gt;`); a live demo, if any, goes in a SEPARATE block OUTSIDE the data-render div with its own `<script type="module">`.
2. Register it in `DOCS_PAGES` in `devplacepy/routers/docs/pages.py`: `{"slug": "<slug>", "title": "<title>", "kind": "prose", "section": SECTION_*}`. Add `"admin": True` for an admin-only page. If a new section is needed, add a `SECTION_*` constant and place it in the correct `AUDIENCES` group. 2. Register it in `DOCS_PAGES` in `devplacepy/routers/docs/pages.py`: `{"slug": "<slug>", "title": "<title>", "kind": "prose", "section": SECTION_*}`. Add `"admin": True` for an admin-only page. If a new section is needed, add a `SECTION_*` constant and place it in the correct `AUDIENCES` group.
3. Write accurate, professional content - confirm every factual claim against the source. No em-dashes, no AI disclaimers, dates as DD/MM/YYYY. 3. Write accurate, professional content - confirm every factual claim against the source. No em-dashes, no AI disclaimers, dates as DD/MM/YYYY.
4. Validate: run `hawk` on the new template and on `pages.py`, run `python -c "from devplacepy.main import app"`, and confirm the slug is registered with no duplicate. 4. Validate: check the new template for tag and `{% %}` balance and `pages.py` with `python -m py_compile` + `pyflakes`, run `python -c "from devplacepy.main import app"`, and confirm the slug is registered with no duplicate.

View File

@ -33,7 +33,7 @@ Arguments: `$ARGUMENTS`
## Execute ## Execute
1. Resolve the dimension list and mode from the arguments above. 1. Resolve the dimension list and mode from the arguments above.
2. **CHECK mode**: launch every selected subagent concurrently (one `Agent` call per dimension in a single message). Each subagent runs read-only and returns its findings report. Tell each subagent explicitly: "Operate in REPORT mode. Do not modify any file." If `changed`, append the file list and: "Restrict findings to these files." 2. **CHECK mode**: launch every selected subagent concurrently (one `Agent` call per dimension in a single message). Each subagent runs read-only and returns its findings report. Tell each subagent explicitly: "Operate in REPORT mode. Do not modify any file." If `changed`, append the file list and: "Restrict findings to these files."
3. **FIX mode**: launch the selected subagents **one at a time in canonical order** (never in parallel - parallel edits to the same file would conflict). Tell each: "Operate in FIX mode: apply minimal root-cause fixes per your doctrine, then run `hawk .` and confirm it passes." Wait for each to finish before starting the next. If `changed`, append the file list and: "Restrict fixes to these files." 3. **FIX mode**: launch the selected subagents **one at a time in canonical order** (never in parallel - parallel edits to the same file would conflict). Tell each: "Operate in FIX mode: apply minimal root-cause fixes per your doctrine, then re-validate every file you touched with the per-language checks and confirm `python -c \"from devplacepy.main import app\"` still imports clean." Wait for each to finish before starting the next. If `changed`, append the file list and: "Restrict fixes to these files."
4. Each subagent's final message is its report; it is not shown to the user directly, so collect them. 4. Each subagent's final message is its report; it is not shown to the user directly, so collect them.
## Report ## Report

View File

@ -13,4 +13,4 @@ Mirror an existing service - read `devplacepy/services/base.py` (BaseService) an
4. If it calls an LLM, default its endpoint to `config.INTERNAL_GATEWAY_URL` and authenticate with the internal gateway key, like the other AI consumers. 4. If it calls an LLM, default its endpoint to `config.INTERNAL_GATEWAY_URL` and authenticate with the internal gateway key, like the other AI consumers.
5. Emit audit events via `record_system` for any state change it makes. 5. Emit audit events via `record_system` for any state change it makes.
6. Document it in `devplacepy/services/CLAUDE.md` (Background services base machinery section, or the service's own nested `CLAUDE.md` if it has one) and in `README.md` if user-visible. 6. Document it in `devplacepy/services/CLAUDE.md` (Background services base machinery section, or the service's own nested `CLAUDE.md` if it has one) and in `README.md` if user-visible.
7. Validate with `hawk` on the touched files and `python -c "from devplacepy.main import app"`. 7. Validate the touched files with the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates) and `python -c "from devplacepy.main import app"`.

View File

@ -1,13 +1,13 @@
--- ---
description: Run the mandatory DevPlace pre-completion verification on changed files - the validator, the app import, and an em-dash scan. Zero errors required. Never runs the test suite. description: Run the mandatory DevPlace pre-completion verification on changed files - the per-language checks, the app import, and an em-dash scan. Zero errors required. Never runs the test suite.
allowed-tools: Bash(python *), Bash(hawk *), Bash(git status:*), Bash(git diff:*), Read, Grep allowed-tools: Bash(python *), Bash(node *), Bash(git status:*), Bash(git diff:*), Read, Grep
--- ---
Changed files in the working tree: Changed files in the working tree:
!`git status --porcelain` !`git status --porcelain`
Verify the work is complete and correct, following the DevPlace verification rule (zero tolerance): Verify the work is complete and correct, following the DevPlace verification rule (zero tolerance):
1. For each changed or new file under `devplacepy/` or `tests/`, run `hawk <file>` (it covers Python, JavaScript, CSS, and HTML/Jinja). Every file must report clean. 1. For each changed or new file under `devplacepy/` or `tests/`, run the per-language checks (`python -m py_compile` + `pyflakes` for Python, `node --check` for JS, brace balance for CSS, tag and `{% %}` balance for templates). Every file must come back clean.
2. Run `python -c "from devplacepy.main import app"` - it must import with no error. 2. Run `python -c "from devplacepy.main import app"` - it must import with no error.
3. Grep the changed files for em-dash characters (U+2014 and U+2013) that are authored prose, and report any. Leave em-dashes that are data (replace/maketrans/regex targets, fixtures) untouched. 3. Grep the changed files for em-dash characters (U+2014 and U+2013) that are authored prose, and report any. Leave em-dashes that are data (replace/maketrans/regex targets, fixtures) untouched.
4. Report a PASS or FAIL summary with the exact failures. 4. Report a PASS or FAIL summary with the exact failures.

View File

@ -18,7 +18,7 @@ const RULES = [
'- A Devii Action requires_auth/requires_admin MUST exactly match the underlying route guard. Never grant a member an admin capability. A non-admin must not even see an admin tool schema.', '- A Devii Action requires_auth/requires_admin MUST exactly match the underlying route guard. Never grant a member an admin capability. A non-admin must not even see an admin tool schema.',
'- If the action is irreversible or destructive, add it to dispatcher CONFIRM_REQUIRED and declare a confirm boolean param in its spec (schemas set additionalProperties:false, so a gated tool without a declared confirm param can never receive confirm=true and loops forever).', '- If the action is irreversible or destructive, add it to dispatcher CONFIRM_REQUIRED and declare a confirm boolean param in its spec (schemas set additionalProperties:false, so a gated tool without a declared confirm param can never receive confirm=true and loops forever).',
'- Prefer handler="http" reusing an existing REST route; only add a local controller handler when there is no route. Reuse the arg()/body()/query()/confirm() helpers for params.', '- Prefer handler="http" reusing an existing REST route; only add a local controller handler when there is no route. Reuse the arg()/body()/query()/confirm() helpers for params.',
'- Validate with "hawk ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.', '- Validate with the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.',
].join('\n') ].join('\n')
const TESTS = [ const TESTS = [
@ -100,7 +100,7 @@ const map = await agent(
) )
const build = await agent( const build = await agent(
`Add this Devii tool, editing files directly in the repo. Add the Action to the catalog mirroring the similar action, set requires_auth/requires_admin to exactly match the underlying route guard, wire the dispatcher handler if a new local handler is needed, and add a docs_api.py entry if it wraps an HTTP endpoint. If destructive, add it to CONFIRM_REQUIRED and declare a confirm param. Then run "hawk ." and "python -c \\"from devplacepy.main import app\\"". Do not write the test here. Do not run the suite. Do not commit.\n\nTool request: ${ask}\n\nContext:\n${JSON.stringify(map, null, 2)}\n\n${RULES}\n\nReturn the action name, files changed, and whether validator and import passed.`, `Add this Devii tool, editing files directly in the repo. Add the Action to the catalog mirroring the similar action, set requires_auth/requires_admin to exactly match the underlying route guard, wire the dispatcher handler if a new local handler is needed, and add a docs_api.py entry if it wraps an HTTP endpoint. If destructive, add it to CONFIRM_REQUIRED and declare a confirm param. Then run the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". Do not write the test here. Do not run the suite. Do not commit.\n\nTool request: ${ask}\n\nContext:\n${JSON.stringify(map, null, 2)}\n\n${RULES}\n\nReturn the action name, files changed, and whether the checks and the import passed.`,
{ label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA } { label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
) )
@ -127,7 +127,7 @@ const gaps = audits
let gapFix = 'no actionable gaps' let gapFix = 'no actionable gaps'
if (gaps.length) { if (gaps.length) {
gapFix = await agent( gapFix = await agent(
`Close these Devii tool gaps with minimal root-cause fixes in the repo, then re-run "hawk .". Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`, `Close these Devii tool gaps with minimal root-cause fixes in the repo, then re-run the per-language checks. Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
{ label: 'fix-gaps', phase: 'Fix' } { label: 'fix-gaps', phase: 'Fix' }
) )
} }

View File

@ -17,7 +17,7 @@ const RULES = [
'- No em-dash characters; use a hyphen. Full Python type hints; pathlib over os; Pydantic Form input with explicit max lengths; sanitize and bound user input.', '- No em-dash characters; use a hyphen. Full Python type hints; pathlib over os; Pydantic Form input with explicit max lengths; sanitize and bound user input.',
'- Reuse templating.templates, database.py batch helpers, respond(), the shared partials and frontend utilities. Never per-router Jinja2Templates.', '- Reuse templating.templates, database.py batch helpers, respond(), the shared partials and frontend utilities. Never per-router Jinja2Templates.',
'- Guards: get_current_user (public read), require_user (member write), require_admin (admin). Every POST/PUT/DELETE is guarded. Declare specific routes before catch-alls. Never pass a respond() context key that collides with a Jinja global (use viewer_is_admin).', '- Guards: get_current_user (public read), require_user (member write), require_admin (admin). Every POST/PUT/DELETE is guarded. Declare specific routes before catch-alls. Never pass a respond() context key that collides with a Jinja global (use viewer_is_admin).',
'- Validate with "hawk ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.', '- Validate with the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.',
].join('\n') ].join('\n')
const TOUCHPOINTS = [ const TOUCHPOINTS = [
@ -108,7 +108,7 @@ const map = await agent(
) )
const build = await agent( const build = await agent(
`Implement this single DevPlace route across every applicable touchpoint, editing files directly in the repo, mirroring the closest existing route. Keep the layers in agreement (Out schema carries every returned JSON key; Devii action auth flags match the guard). Then run "hawk ." and "python -c \\"from devplacepy.main import app\\"". Do not write the test here. Do not run the suite. Do not commit.\n\nEndpoint: ${ask}\n\nClosest route to mirror:\n${JSON.stringify(map, null, 2)}\n\n${TOUCHPOINTS}\n\n${RULES}\n\nReturn the files changed and whether validator and import passed.`, `Implement this single DevPlace route across every applicable touchpoint, editing files directly in the repo, mirroring the closest existing route. Keep the layers in agreement (Out schema carries every returned JSON key; Devii action auth flags match the guard). Then run the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". Do not write the test here. Do not run the suite. Do not commit.\n\nEndpoint: ${ask}\n\nClosest route to mirror:\n${JSON.stringify(map, null, 2)}\n\n${TOUCHPOINTS}\n\n${RULES}\n\nReturn the files changed and whether the checks and the import passed.`,
{ label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA } { label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
) )
@ -135,7 +135,7 @@ const gaps = audits
let gapFix = 'no actionable gaps' let gapFix = 'no actionable gaps'
if (gaps.length) { if (gaps.length) {
gapFix = await agent( gapFix = await agent(
`Close these gaps on the new route with minimal root-cause fixes in the repo, then re-run "hawk .". Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`, `Close these gaps on the new route with minimal root-cause fixes in the repo, then re-run the per-language checks. Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
{ label: 'fix-gaps', phase: 'Fix' } { label: 'fix-gaps', phase: 'Fix' }
) )
} }

View File

@ -22,7 +22,7 @@ const RULES = [
'- Reuse shared helpers: templating.templates (never a per-router Jinja2Templates), database.py batch helpers (no inline N+1), the respond() negotiator, _avatar_link.html / _user_link.html, and on the frontend Http / Poller / JobPoller / OptimisticAction / FloatingWindow and the dp-* components.', '- Reuse shared helpers: templating.templates (never a per-router Jinja2Templates), database.py batch helpers (no inline N+1), the respond() negotiator, _avatar_link.html / _user_link.html, and on the frontend Http / Poller / JobPoller / OptimisticAction / FloatingWindow and the dp-* components.',
'- Auth guards: get_current_user (public read), require_user (member write), require_admin (admin). Every POST/PUT/DELETE is guarded; deletes are soft and owner-or-admin.', '- Auth guards: get_current_user (public read), require_user (member write), require_admin (admin). Every POST/PUT/DELETE is guarded; deletes are soft and owner-or-admin.',
'- Never pass a respond() context key that collides with a Jinja global (use viewer_is_admin, not is_admin). Dates are DD/MM/YYYY via format_date.', '- Never pass a respond() context key that collides with a Jinja global (use viewer_is_admin, not is_admin). Dates are DD/MM/YYYY via format_date.',
'- Validate with "hawk ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the pytest suite. Never perform any git write.', '- Validate with the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". NEVER run the pytest suite. Never perform any git write.',
].join('\n') ].join('\n')
const FANOUT = [ const FANOUT = [
@ -185,7 +185,7 @@ const plan = await agent(
) )
const build = await agent( const build = await agent(
`Implement directly - no plan, no approval needed, this is implement mode. Build this DevPlace feature coherently and completely, editing files in the repo, following the plan. Keep every layer in agreement (the *Out schema must carry every JSON key the handler returns; the Devii action auth flags must match the route guard; a respond() context key must never shadow a Jinja global). Do NOT write pytest tests in this step (a later phase owns that). When done, run "hawk ." and "python -c \\"from devplacepy.main import app\\"" and report whether each passed, and list the user-facing routes the feature exposes.\n\nFeature request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${FANOUT}\n\n${RULES}\n\nReturn the list of files you changed or created, whether the validator and the import passed, the routes, and a short summary.`, `Implement directly - no plan, no approval needed, this is implement mode. Build this DevPlace feature coherently and completely, editing files in the repo, following the plan. Keep every layer in agreement (the *Out schema must carry every JSON key the handler returns; the Devii action auth flags must match the route guard; a respond() context key must never shadow a Jinja global). Do NOT write pytest tests in this step (a later phase owns that). When done, run the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"" and report whether each passed, and list the user-facing routes the feature exposes.\n\nFeature request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${FANOUT}\n\n${RULES}\n\nReturn the list of files you changed or created, whether the checks and the import passed, the routes, and a short summary.`,
{ agentType: 'feature-builder', label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA } { agentType: 'feature-builder', label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
) )
@ -279,7 +279,7 @@ for (const i of (live && live.issues) || []) {
let gapFix = 'no actionable gaps from the audit or live verification' let gapFix = 'no actionable gaps from the audit or live verification'
if (gaps.length) { if (gaps.length) {
gapFix = await agent( gapFix = await agent(
`Close these confirmed completeness, security, style, frontend, and live-rendering gaps found in the new feature. Apply minimal root-cause fixes directly in the repo, keeping all layers in agreement and the styling consistent with the design system. Re-run "hawk ." afterward. Do not run the pytest suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`, `Close these confirmed completeness, security, style, frontend, and live-rendering gaps found in the new feature. Apply minimal root-cause fixes directly in the repo, keeping all layers in agreement and the styling consistent with the design system. Re-run the per-language checks afterward. Do not run the pytest suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
{ agentType: 'feature-builder', label: 'fix-gaps', phase: 'Fix' } { agentType: 'feature-builder', label: 'fix-gaps', phase: 'Fix' }
) )
} }

View File

@ -19,7 +19,7 @@ const RULES = [
'- Runtime artifacts live in config.DATA_DIR (the var/ dir), OUTSIDE the devplacepy package and NOT under /static. Heavy compression or blocking work runs in a subprocess. SQLite stays synchronous.', '- Runtime artifacts live in config.DATA_DIR (the var/ dir), OUTSIDE the devplacepy package and NOT under /static. Heavy compression or blocking work runs in a subprocess. SQLite stays synchronous.',
'- Enqueue endpoints own authz (require_user plus any resource guard); status and download are capability URLs scoped only by the unguessable uuid7. Soft-delete the job tracking rows; permanent artifacts are not deleted by cleanup().', '- Enqueue endpoints own authz (require_user plus any resource guard); status and download are capability URLs scoped only by the unguessable uuid7. Soft-delete the job tracking rows; permanent artifacts are not deleted by cleanup().',
'- Record audit events with record_system in the service. Frontend status polling uses JobPoller, never a bespoke loop.', '- Record audit events with record_system in the service. Frontend status polling uses JobPoller, never a bespoke loop.',
'- Validate with "hawk ." and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.', '- Validate with the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". NEVER run the test suite. Never perform any git write.',
].join('\n') ].join('\n')
const CHECKLIST = [ const CHECKLIST = [
@ -133,7 +133,7 @@ const plan = await agent(
) )
const build = await agent( const build = await agent(
`Implement this new async job kind coherently, editing files directly in the repo, mirroring ZipService/ForkService and following the plan. Keep the *JobOut schema, routes, Devii tools, and docs in agreement. Then run "hawk ." and "python -c \\"from devplacepy.main import app\\"". Do not write tests here. Do not run the suite. Do not commit.\n\nJob request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${CHECKLIST}\n\n${RULES}\n\nReturn the job kind, files changed, and whether validator and import passed.`, `Implement this new async job kind coherently, editing files directly in the repo, mirroring ZipService/ForkService and following the plan. Keep the *JobOut schema, routes, Devii tools, and docs in agreement. Then run the per-language checks (py_compile + pyflakes for Python, node --check for JS, brace balance for CSS, tag and {% %} balance for templates) and "python -c \\"from devplacepy.main import app\\"". Do not write tests here. Do not run the suite. Do not commit.\n\nJob request: ${ask}\n\nPlan:\n${JSON.stringify(plan, null, 2)}\n\n${CHECKLIST}\n\n${RULES}\n\nReturn the job kind, files changed, and whether the checks and the import passed.`,
{ label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA } { label: 'implement', phase: 'Implement', schema: BUILD_SCHEMA }
) )
@ -162,7 +162,7 @@ const gaps = audits
let gapFix = 'no actionable gaps' let gapFix = 'no actionable gaps'
if (gaps.length) { if (gaps.length) {
gapFix = await agent( gapFix = await agent(
`Close these job-service gaps with minimal root-cause fixes in the repo, then re-run "hawk .". Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`, `Close these job-service gaps with minimal root-cause fixes in the repo, then re-run the per-language checks. Do not run the suite. Do not commit.\n\nGaps:\n${JSON.stringify(gaps, null, 2)}\n\n${RULES}`,
{ label: 'fix-gaps', phase: 'Fix' } { label: 'fix-gaps', phase: 'Fix' }
) )
} }

1
apple.md Normal file
View File

@ -0,0 +1 @@
Dear Mr. Claude. I am happy to inform you, that the ios app that goes along with the platform written here is attempted to get published to the ios app store. Sady, APPLE declined. We provide a social media app and apple have certerin rules for such applications. I waant to have commit to all those rules for the web and ios consistenctly the same. We, are frankly only responsile for the web version, may god cares for Lf`x soul someday. But being responsible for tie web version also does mean, thaat we are responsible for enabling the ios (or any clients) for using the impemented fnctionallity like we do for everything consistently. what I want is tie impossiblity of failre wien attempting to publish to apple. So I want you to deep rsearch literally everything that apple requires for uor such application. Nie hu. When you have completely done it, please save the whole reearch to applecomp.md. Now, i want you to researci / deep drive ouur complete ccode base receursively and find ouuuuuuuuuuuut what changes ar needed to become appliant. That is should be stored in applechanges.md. Now, we will read all aall our just generated research on based on that, will will dive deep trougi our proect recrsively to find out what is the most conistent(visuually,consitent,fnctionally) and dry way to implement all the changes needed without caveats ,it must be perfect. This all shouuld result into appleimpl.md. Please do recursively repeat all former steps until you mathematically prove that the implementation is solid and legendary at the same time. Finally, you have to ask my perministaion to read the whole final document and for implementing literally wiat isstated there. Spank you very much.

View File

@ -15,6 +15,8 @@ from devplacepy.utils import clear_user_cache, require_user
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
TERMS_ACCEPTANCE_CODE = "terms_acceptance_required"
def current_terms_version() -> str: def current_terms_version() -> str:
return get_setting("terms_version", "1") or "1" return get_setting("terms_version", "1") or "1"

View File

@ -2000,9 +2000,9 @@ async def reflect(observation: str, conclusion: str, next_action: str):
@tool @tool
async def verify(command: str = "hawk .", timeout: int = 600): async def verify(command: str, timeout: int = 600):
"""Run a verification command (linter, tests, validator). Marks the task verified on success. """Run a verification command (linter, tests, validator). Marks the task verified on success.
command: Shell command, default 'hawk .'. command: Shell command that verifies the change, for example the project's test or lint command.
timeout: Timeout in seconds. timeout: Timeout in seconds.
""" """
try: try:
@ -2526,7 +2526,7 @@ async def react_loop(
"role": "user", "role": "user",
"content": ( "content": (
"[verification-gate] You produced a final answer after modifying files " "[verification-gate] You produced a final answer after modifying files "
"without a successful verify(). Call verify() now (default 'hawk .') and " "without a successful verify(). Call verify() with the command that verifies this project now and "
"report the result. If verification truly does not apply, reply starting " "report the result. If verification truly does not apply, reply starting "
"with: 'No verification applicable: <reason>'." "with: 'No verification applicable: <reason>'."
), ),
@ -2559,7 +2559,7 @@ OPERATING PROTOCOL
3. INVESTIGATE BEFORE EDITING. Use grep, glob_files, list_dir, find_symbol, and retrieve to navigate. You MUST read_file (or read_lines) an existing file before edit_file, patch_file, or write_file touches it the harness enforces this. Prefer edit_file for surgical replacements, patch_file for multi-hunk diffs, create_file for new files, write_file for full rewrites of files you have read. 3. INVESTIGATE BEFORE EDITING. Use grep, glob_files, list_dir, find_symbol, and retrieve to navigate. You MUST read_file (or read_lines) an existing file before edit_file, patch_file, or write_file touches it the harness enforces this. Prefer edit_file for surgical replacements, patch_file for multi-hunk diffs, create_file for new files, write_file for full rewrites of files you have read.
4. VERIFY BEFORE FINISHING. Whenever you modify files, call verify() (default 'hawk .') before your final answer. The harness rejects a final answer that changed files without a successful verify(). 4. VERIFY BEFORE FINISHING. Whenever you modify files, call verify() with the command that verifies this project before your final answer. The harness rejects a final answer that changed files without a successful verify().
5. REFLECT ON FAILURE. After any tool returns status=error, the harness injects a reflection trigger. Respond with reflect() (observation, conclusion, next_action), then proceed. Never blindly retry the same call. 5. REFLECT ON FAILURE. After any tool returns status=error, the harness injects a reflection trigger. Respond with reflect() (observation, conclusion, next_action), then proceed. Never blindly retry the same call.

View File

@ -1757,9 +1757,9 @@ async def reflect(observation: str, conclusion: str, next_action: str):
@tool @tool
async def verify(command: str = "hawk .", timeout: int = 600): async def verify(command: str, timeout: int = 600):
"""Run a verification command (linter, tests, validator). Marks the task verified on success. """Run a verification command (linter, tests, validator). Marks the task verified on success.
command: Shell command, default 'hawk .'. command: Shell command that verifies the change, for example the project's test or lint command.
timeout: Timeout in seconds. timeout: Timeout in seconds.
""" """
try: try:
@ -2196,7 +2196,7 @@ async def react_loop(
"role": "user", "role": "user",
"content": ( "content": (
"[verification-gate] You produced a final answer after modifying files without a successful verify(). " "[verification-gate] You produced a final answer after modifying files without a successful verify(). "
"Call verify() now (default 'hawk .') and report the result. If verification truly does not apply, reply " "Call verify() with the command that verifies this project now and report the result. If verification truly does not apply, reply "
"starting with: 'No verification applicable: <reason>'." "starting with: 'No verification applicable: <reason>'."
), ),
}) })
@ -2224,7 +2224,7 @@ OPERATING PROTOCOL
3. INVESTIGATE BEFORE EDITING. Use grep, glob_files, list_dir, find_symbol, and retrieve to navigate. You MUST read_file (or read_lines) an existing file before edit_file, patch_file, or write_file touches it the harness enforces this. Prefer edit_file for surgical replacements, patch_file for multi-hunk diffs, create_file for new files, write_file for full rewrites of files you have read. 3. INVESTIGATE BEFORE EDITING. Use grep, glob_files, list_dir, find_symbol, and retrieve to navigate. You MUST read_file (or read_lines) an existing file before edit_file, patch_file, or write_file touches it the harness enforces this. Prefer edit_file for surgical replacements, patch_file for multi-hunk diffs, create_file for new files, write_file for full rewrites of files you have read.
4. VERIFY BEFORE FINISHING. Whenever you modify files, call verify() (default 'hawk .') before your final answer. The harness rejects a final answer that changed files without a successful verify(). 4. VERIFY BEFORE FINISHING. Whenever you modify files, call verify() with the command that verifies this project before your final answer. The harness rejects a final answer that changed files without a successful verify().
5. REFLECT ON FAILURE. After any tool returns status=error, the harness injects a reflection trigger. Respond with reflect() (observation, conclusion, next_action), then proceed. Never blindly retry the same call. 5. REFLECT ON FAILURE. After any tool returns status=error, the harness injects a reflection trigger. Respond with reflect() (observation, conclusion, next_action), then proceed. Never blindly retry the same call.

View File

@ -1712,9 +1712,9 @@ async def fetch_url(url: str, max_bytes: int = 1048576):
@tool @tool
async def verify(command: str = "hawk .", timeout: int = 600): async def verify(command: str, timeout: int = 600):
"""Run a verification command (tests, linter, validator) and return whether it passed. Marks the task verified on success. """Run a verification command (tests, linter, validator) and return whether it passed. Marks the task verified on success.
command: Shell command, default 'hawk .' per project conventions. command: Shell command that verifies the change, for example the project's test or lint command.
timeout: Timeout in seconds. timeout: Timeout in seconds.
""" """
try: try:
@ -2221,7 +2221,7 @@ async def react_loop(
"role": "user", "role": "user",
"content": ( "content": (
"[verification-gate] You produced a final answer after modifying files " "[verification-gate] You produced a final answer after modifying files "
"without calling verify(). Call verify() now (default 'hawk .') and " "without calling verify(). Call verify() with the command that verifies this project now and "
"report the result. If verification truly does not apply, reply explicitly " "report the result. If verification truly does not apply, reply explicitly "
"starting with: 'No verification applicable: <reason>'." "starting with: 'No verification applicable: <reason>'."
), ),
@ -2267,7 +2267,7 @@ OPERATING PROTOCOL
3. INVESTIGATE BEFORE EDITING. Use grep, glob_files, list_dir, find_symbol, and retrieve to navigate the codebase. Use read_file or read_lines before modifying. Prefer edit_file for surgical text replacements; create_file for new files; reserve write_file for full rewrites of files you have already read. 3. INVESTIGATE BEFORE EDITING. Use grep, glob_files, list_dir, find_symbol, and retrieve to navigate the codebase. Use read_file or read_lines before modifying. Prefer edit_file for surgical text replacements; create_file for new files; reserve write_file for full rewrites of files you have already read.
4. VERIFY BEFORE FINISHING. Whenever you modify files, call verify() before producing a final answer (default command 'hawk .'). The harness will reject a final answer that involved file changes without a successful verify(). 4. VERIFY BEFORE FINISHING. Whenever you modify files, call verify() with the command that verifies this project before producing a final answer. The harness will reject a final answer that involved file changes without a successful verify().
5. REFLECT ON FAILURE. After any tool returns status=error, the harness injects a reflection trigger. Respond by calling reflect() with observation/conclusion/next_action, then proceed. Never blindly retry the same call. 5. REFLECT ON FAILURE. After any tool returns status=error, the harness injects a reflection trigger. Respond by calling reflect() with observation/conclusion/next_action, then proceed. Never blindly retry the same call.

View File

@ -114,6 +114,21 @@ The gate is at exactly one place: `GatewayService.consent_denied` in `services/o
Every reader of a policy version uses `get_setting(key, "1") or "1"`. **This is not cosmetic**: on a fresh database `init_db` skips the settings seed (its `tables` snapshot predates `site_settings`), so an admin settings save can insert `terms_version = ""`, and a bare `get_setting` would then compare every user's `"1"` against `""` and 403 every write on the platform. That was a real failure; keep the `or "1"`. Every reader of a policy version uses `get_setting(key, "1") or "1"`. **This is not cosmetic**: on a fresh database `init_db` skips the settings seed (its `tables` snapshot predates `site_settings`), so an admin settings save can insert `terms_version = ""`, and a bare `get_setting` would then compare every user's `"1"` against `""` and 403 every write on the platform. That was a real failure; keep the `or "1"`.
### The refusal is self-describing, and the client acts on it
A browser form POST is a real navigation, so the `303` to `/auth/accept-terms` already works with no JS. A **fetch** caller cannot follow that, so the JSON branch carries everything the client needs to resolve the block itself:
```json
{"error": {"status": 403, "message": "Accept the updated Terms of Service to continue.",
"code": "terms_acceptance_required", "redirect": "/auth/accept-terms", "terms_version": "1"}}
```
`code` is the contract - the client matches on it, never on the message text. It is `TERMS_ACCEPTANCE_CODE` in `routers/auth/terms.py`, the single definition, imported by `main.py` and asserted by the api test. `terms_version` lets the dialog name the version without a second request.
The client side is `static/js/TermsGate.js` (`app.termsGate`); see `devplacepy/static/js/CLAUDE.md`. **Do not add a second terms check, a second refusal shape, or a per-caller handler** - `Http` routes every fetch refusal through the one gate, so a new fetch caller inherits the behaviour with no work.
**Note on `users.terms_version`:** `init_db` ensures the column but deliberately does **not** backfill it, so every account predating the trust-and-safety commit reads `NULL` and must accept. That is correct - a backfill would fabricate consent nobody gave - but it means the gate is the normal state for legacy accounts, not a rare edge, so the accept path must stay one click.
## Maturity ## Maturity
`content_maturity` is polymorphic (`target_type`, `target_uid`), read through the batch helper `get_maturity_by_targets` - never per row. Absence of a row means `general`, so nothing needed backfilling. `content.maturity_hidden(level, user)` is the single predicate (also the `maturity_hidden` Jinja global) and `_maturity_gate.html` is the single interstitial; `enrich_items` and `load_detail` attach `maturity` so listings and detail pages both have it with one query. `content_maturity` is polymorphic (`target_type`, `target_uid`), read through the batch helper `get_maturity_by_targets` - never per row. Absence of a row means `general`, so nothing needed backfilling. `content.maturity_hidden(level, user)` is the single predicate (also the `maturity_hidden` Jinja global) and `_maturity_gate.html` is the single interstitial; `enrich_items` and `load_detail` attach `maturity` so listings and detail pages both have it with one query.

View File

@ -27,6 +27,20 @@
word-break: break-word; word-break: break-word;
} }
.dialog-links {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: var(--space-sm) var(--space-md);
margin: 0 0 1rem;
padding: 0;
font-size: 0.875rem;
}
.dialog-links a {
color: var(--accent);
}
.dialog-field { .dialog-field {
margin-bottom: 1rem; margin-bottom: 1rem;
} }

View File

@ -34,6 +34,7 @@ import { IssueAttachments } from "./IssueAttachments.js";
import { PlanningGenerator } from "./PlanningGenerator.js"; import { PlanningGenerator } from "./PlanningGenerator.js";
import { MediaGallery } from "./MediaGallery.js"; import { MediaGallery } from "./MediaGallery.js";
import { ReportDialog } from "./ReportDialog.js"; import { ReportDialog } from "./ReportDialog.js";
import { TermsGate } from "./TermsGate.js";
import WindowManager from "./components/WindowManager.js"; import WindowManager from "./components/WindowManager.js";
import { ContainerTerminalManager } from "./ContainerTerminalManager.js"; import { ContainerTerminalManager } from "./ContainerTerminalManager.js";
import { PubSubClient } from "./PubSubClient.js"; import { PubSubClient } from "./PubSubClient.js";
@ -69,6 +70,7 @@ class Application {
this.toast = document.createElement("dp-toast"); this.toast = document.createElement("dp-toast");
this.lightbox = document.createElement("dp-lightbox"); this.lightbox = document.createElement("dp-lightbox");
document.body.append(this.dialog, this.contextMenu, this.toast, this.lightbox); document.body.append(this.dialog, this.contextMenu, this.toast, this.lightbox);
this.termsGate = new TermsGate(this.dialog);
this.modals = new ModalManager(); this.modals = new ModalManager();
this.forms = new FormManager(); this.forms = new FormManager();
this.votes = new VoteManager(); this.votes = new VoteManager();

View File

@ -43,6 +43,9 @@ A small set of plain ES6 modules under `static/js/` own the cross-cutting patter
Detail on each utility: Detail on each utility:
- **`Http` (`static/js/Http.js`, global `window.Http`).** The single HTTP helper. `getJson(url)` (GET -> JSON, throws on non-2xx); `sendForm(url, params)` (POST form-encoded, follows the `/auth/login` redirect via `Http.toLogin()`, throws a bare status on failure, returns JSON); `send(url, params)` (POST form-encoded that throws `data.error.message` on `!ok` **or** a 200 body with `ok:false` - the manager-style error the container/admin UIs surface in a toast); `postJson`/`postForm`/`toLogin`. Container files (`ContainerManager`, `ContainerList`, `ContainerInstance`, `ContainerTerminal`), `ServiceMonitor`, and `ProjectFiles` all route through it - none re-implement `fetch`. - **`Http` (`static/js/Http.js`, global `window.Http`).** The single HTTP helper. `getJson(url)` (GET -> JSON, throws on non-2xx); `sendForm(url, params)` (POST form-encoded, follows the `/auth/login` redirect via `Http.toLogin()`, throws a bare status on failure, returns JSON); `send(url, params)` (POST form-encoded that throws `data.error.message` on `!ok` **or** a 200 body with `ok:false` - the manager-style error the container/admin UIs surface in a toast); `postJson`/`postForm`/`toLogin`. Container files (`ContainerManager`, `ContainerList`, `ContainerInstance`, `ContainerTerminal`), `ServiceMonitor`, and `ProjectFiles` all route through it - none re-implement `fetch`.
`Http.suspend()` is the named "we are resolving this elsewhere, do not let the caller render an error" idiom (a promise that never settles). It replaced three inline `new Promise(() => {})` copies and is what `toLogin()` and the terms gate both return.
- **`TermsGate` (`static/js/TermsGate.js`, `app.termsGate`).** The single client-side handler for the terms-acceptance refusal. `Http` calls `Http._gate(data, options, retry)` on **every** POST helper (`sendForm`, `send`, `postJson`); when the error payload carries `code: "terms_acceptance_required"` it hands off to `app.termsGate.intercept(error, retry)`, which shows one dialog (Accept and continue / Not now, with the Terms, Guidelines and Privacy links), POSTs `/auth/accept-terms` on accept, and then **re-runs the original request** so the click the user made actually happens. Declining returns `Http.suspend()`.
Load-bearing details: the handoff runs **before** the `options.silent` check, because the four `OptimisticAction` controllers (vote/react/bookmark/poll) pass `silent: true` and would otherwise swallow a blocking gate into a 1.5s "Error" flash; `options.termsRetry` bounds the retry to exactly one pass; and `confirm()`/`accept()` are each deduped by a stored promise so N concurrent gated requests produce one dialog and one acceptance POST. `dp-upload` bypasses `Http` (it needs `FormData`), so it checks `app.termsGate.matches(data)` itself - any other raw-`fetch` caller must do the same. Never add a per-caller terms check: the backend contract lives in `routers/auth/terms.py` `TERMS_ACCEPTANCE_CODE` and is documented in `devplacepy/services/moderation/CLAUDE.md`.
- **`Poller` (`static/js/Poller.js`).** `new Poller(fn, intervalMs, { immediate = true, pauseHidden = false })` runs `fn` on an interval with `start()`/`stop()`/`tick()`; `tick()` swallows errors so one failed poll never kills the loop, and `pauseHidden` skips the tick while `document.hidden`. Used by every live-update loop: `CounterManager` (30s, `pauseHidden`), `ContainerManager` (3s), `ContainerList` (4s), `AiUsageMonitor`, `ServiceMonitor`, and `ContainerInstance`'s detail (4s) + logs (3s). Store the `Poller`, not a raw interval id. - **`Poller` (`static/js/Poller.js`).** `new Poller(fn, intervalMs, { immediate = true, pauseHidden = false })` runs `fn` on an interval with `start()`/`stop()`/`tick()`; `tick()` swallows errors so one failed poll never kills the loop, and `pauseHidden` skips the tick while `document.hidden`. Used by every live-update loop: `CounterManager` (30s, `pauseHidden`), `ContainerManager` (3s), `ContainerList` (4s), `AiUsageMonitor`, `ServiceMonitor`, and `ContainerInstance`'s detail (4s) + logs (3s). Store the `Poller`, not a raw interval id.
- **`JobPoller` (`static/js/JobPoller.js`).** `JobPoller.run(statusUrl, { onDone, onFailed, onTimeout, intervalMs = 1500, maxAttempts = 200 })` returns a Promise; it polls `Http.getJson(statusUrl)`, swallows transient fetch errors, and fires the matching callback on `status === "done"|"failed"` or timeout. This is the one place the async-job status-poll lives - `ProjectForker` and `ZipDownloader` both call it with their own navigate/download/toast callbacks. - **`JobPoller` (`static/js/JobPoller.js`).** `JobPoller.run(statusUrl, { onDone, onFailed, onTimeout, intervalMs = 1500, maxAttempts = 200 })` returns a Promise; it polls `Http.getJson(statusUrl)`, swallows transient fetch errors, and fires the matching callback on `status === "done"|"failed"` or timeout. This is the one place the async-job status-poll lives - `ProjectForker` and `ZipDownloader` both call it with their own navigate/download/toast callbacks.
- **`OptimisticAction` (`static/js/OptimisticAction.js`).** Base with one method, `submit(url, params, errorTarget, render)`: `Http.sendForm` -> `render(result)` on success -> `console.error` + (when `errorTarget` is given) `Toast.flash(errorTarget, "Error", 1500)` on failure. `VoteManager`/`ReactionBar`/`BookmarkManager`/`PollManager` `extend` it and call `this.submit(...)` for their POST, **keeping their own event wiring** (so `ReactionBar`'s palette toggle and `PollManager`'s multi-action handlers and `VoteManager`'s per-button `stopPropagation` are untouched). Pass `errorTarget` only where the old code toasted (`VoteManager`); the others pass `null` to keep their console-only behaviour. - **`OptimisticAction` (`static/js/OptimisticAction.js`).** Base with one method, `submit(url, params, errorTarget, render)`: `Http.sendForm` -> `render(result)` on success -> `console.error` + (when `errorTarget` is given) `Toast.flash(errorTarget, "Error", 1500)` on failure. `VoteManager`/`ReactionBar`/`BookmarkManager`/`PollManager` `extend` it and call `this.submit(...)` for their POST, **keeping their own event wiring** (so `ReactionBar`'s palette toggle and `PollManager`'s multi-action handlers and `VoteManager`'s per-button `stopPropagation` are untouched). Pass `errorTarget` only where the old code toasted (`VoteManager`); the others pass `null` to keep their console-only behaviour.

View File

@ -6,6 +6,17 @@ export class Http {
window.location.href = `/auth/login?next=${next}`; window.location.href = `/auth/login?next=${next}`;
} }
static suspend() {
return new Promise(() => {});
}
static _gate(data, options, retry) {
if (options.termsRetry) return null;
const gate = window.app && window.app.termsGate;
if (!gate || !gate.matches(data)) return null;
return gate.intercept(data.error, retry);
}
static notifyError(message) { static notifyError(message) {
const text = (message && String(message).trim()) || "Something went wrong. Please try again."; const text = (message && String(message).trim()) || "Something went wrong. Please try again.";
const app = window.app; const app = window.app;
@ -54,10 +65,14 @@ export class Http {
}); });
if (response.redirected && response.url.includes("/auth/login")) { if (response.redirected && response.url.includes("/auth/login")) {
Http.toLogin(); Http.toLogin();
return new Promise(() => {}); return Http.suspend();
} }
if (!response.ok) { if (!response.ok) {
const { message } = await Http._messageFrom(response); const { data, message } = await Http._messageFrom(response);
const gate = Http._gate(data, options, () =>
Http.sendForm(url, params, { ...options, termsRetry: true })
);
if (gate) return gate;
if (!options.silent) Http.notifyError(message); if (!options.silent) Http.notifyError(message);
throw new Error(message); throw new Error(message);
} }
@ -75,11 +90,15 @@ export class Http {
}); });
if (response.redirected && response.url.includes("/auth/login")) { if (response.redirected && response.url.includes("/auth/login")) {
Http.toLogin(); Http.toLogin();
return new Promise(() => {}); return Http.suspend();
} }
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
if (!response.ok || data.ok === false) { if (!response.ok || data.ok === false) {
const message = (data.error && data.error.message) || `request failed: ${response.status}`; const message = (data.error && data.error.message) || `request failed: ${response.status}`;
const gate = Http._gate(data, options, () =>
Http.send(url, params, { ...options, termsRetry: true })
);
if (gate) return gate;
if (!options.silent) Http.notifyError(message); if (!options.silent) Http.notifyError(message);
throw new Error(message); throw new Error(message);
} }
@ -94,10 +113,14 @@ export class Http {
}); });
if (response.redirected && response.url.includes("/auth/login")) { if (response.redirected && response.url.includes("/auth/login")) {
Http.toLogin(); Http.toLogin();
return new Promise(() => {}); return Http.suspend();
} }
if (!response.ok) { if (!response.ok) {
const { message } = await Http._messageFrom(response); const { data, message } = await Http._messageFrom(response);
const gate = Http._gate(data, options, () =>
Http.postJson(url, body, { ...options, termsRetry: true })
);
if (gate) return gate;
if (!options.silent) Http.notifyError(message); if (!options.silent) Http.notifyError(message);
throw Http._error(message, response.status); throw Http._error(message, response.status);
} }

View File

@ -0,0 +1,79 @@
// retoor <retoor@molodetz.nl>
import { Http } from "./Http.js";
export const TERMS_ACCEPTANCE_CODE = "terms_acceptance_required";
const ACCEPT_URL = "/auth/accept-terms";
const TERMS_LINKS = [
{ href: "/docs/terms.html", label: "Terms of Service" },
{ href: "/docs/community-guidelines.html", label: "Community Guidelines" },
{ href: "/docs/privacy.html", label: "Privacy Policy" },
];
export class TermsGate {
constructor(dialog) {
this.dialog = dialog;
this.prompted = null;
this.accepted = null;
}
matches(payload) {
const error = payload && payload.error;
return !!error && error.code === TERMS_ACCEPTANCE_CODE;
}
async intercept(error, retry) {
if (!(await this.confirm(error))) {
return Http.suspend();
}
if (!(await this.accept())) {
return Http.suspend();
}
return retry();
}
confirm(error) {
if (!this.prompted) {
this.prompted = this.dialog
.confirm({
title: "Terms of Service",
message: TermsGate.message(error),
links: TERMS_LINKS,
confirmLabel: "Accept and continue",
cancelLabel: "Not now",
})
.then((answer) => {
this.prompted = null;
return answer === true;
});
}
return this.prompted;
}
accept() {
if (!this.accepted) {
this.accepted = Http.sendForm(ACCEPT_URL, {}, { silent: true, termsRetry: true })
.then(() => true)
.catch(() => {
Http.notifyError("Your acceptance could not be recorded. Please try again.");
return false;
})
.finally(() => {
this.accepted = null;
});
}
return this.accepted;
}
static message(error) {
const version = error && error.terms_version;
const subject = version ? `Version ${version} of the terms` : "The terms";
return (
`${subject} is in force and your account has not accepted it yet, ` +
"so this action was not carried out. Accepting records your agreement and repeats the action. " +
"Reading the site and deleting your account are never blocked."
);
}
}

View File

@ -24,6 +24,7 @@ export class AppDialog extends Component {
'<div class="modal-header"><h3 class="dialog-title"></h3>' + '<div class="modal-header"><h3 class="dialog-title"></h3>' +
'<button type="button" class="modal-close btn-ghost btn-icon dialog-close">&times;</button></div>' + '<button type="button" class="modal-close btn-ghost btn-icon dialog-close">&times;</button></div>' +
'<p class="dialog-message"></p>' + '<p class="dialog-message"></p>' +
'<ul class="dialog-links" hidden></ul>' +
'<div class="dialog-field" hidden><label class="dialog-field-label"></label>' + '<div class="dialog-field" hidden><label class="dialog-field-label"></label>' +
'<input type="text" class="dialog-input" autocomplete="off"></div>' + '<input type="text" class="dialog-input" autocomplete="off"></div>' +
'<div class="modal-footer">' + '<div class="modal-footer">' +
@ -40,6 +41,7 @@ export class AppDialog extends Component {
this.messageEl.id = `dialog-message-${uid}`; this.messageEl.id = `dialog-message-${uid}`;
overlay.setAttribute("aria-labelledby", this.titleEl.id); overlay.setAttribute("aria-labelledby", this.titleEl.id);
overlay.setAttribute("aria-describedby", this.messageEl.id); overlay.setAttribute("aria-describedby", this.messageEl.id);
this.links = overlay.querySelector(".dialog-links");
this.field = overlay.querySelector(".dialog-field"); this.field = overlay.querySelector(".dialog-field");
this.fieldLabel = overlay.querySelector(".dialog-field-label"); this.fieldLabel = overlay.querySelector(".dialog-field-label");
this.input = overlay.querySelector(".dialog-input"); this.input = overlay.querySelector(".dialog-input");
@ -94,6 +96,7 @@ export class AppDialog extends Component {
this.cancelBtn.textContent = opts.cancelLabel || "Cancel"; this.cancelBtn.textContent = opts.cancelLabel || "Cancel";
this.cancelBtn.style.display = mode === "alert" ? "none" : ""; this.cancelBtn.style.display = mode === "alert" ? "none" : "";
this.confirmBtn.classList.toggle("dialog-danger", !!opts.danger); this.confirmBtn.classList.toggle("dialog-danger", !!opts.danger);
this.renderLinks(opts.links);
if (mode === "prompt") { if (mode === "prompt") {
this.field.hidden = false; this.field.hidden = false;
@ -116,6 +119,22 @@ export class AppDialog extends Component {
return new Promise((resolve) => { this.resolver = resolve; }); return new Promise((resolve) => { this.resolver = resolve; });
} }
renderLinks(links) {
const items = Array.isArray(links) ? links : [];
this.links.replaceChildren();
this.links.hidden = !items.length;
for (const item of items) {
const anchor = document.createElement("a");
anchor.href = item.href;
anchor.textContent = item.label;
anchor.target = "_blank";
anchor.rel = "noopener";
const row = document.createElement("li");
row.appendChild(anchor);
this.links.appendChild(row);
}
}
accept() { accept() {
const value = this.mode === "prompt" ? this.input.value : true; const value = this.mode === "prompt" ? this.input.value : true;
this.close(value); this.close(value);

View File

@ -163,7 +163,7 @@ export class AppUpload extends Component {
return true; return true;
} }
async upload(file) { async upload(file, termsRetry = false) {
this.button.classList.add("uploading"); this.button.classList.add("uploading");
this.setBusy(true); this.setBusy(true);
const body = new FormData(); const body = new FormData();
@ -179,6 +179,10 @@ export class AppUpload extends Component {
}); });
const data = await response.json().catch(() => ({})); const data = await response.json().catch(() => ({}));
if (!response.ok || data.ok === false || data.error) { if (!response.ok || data.ok === false || data.error) {
const gate = window.app && window.app.termsGate;
if (!termsRetry && gate && gate.matches(data)) {
return gate.intercept(data.error, () => this.upload(file, true));
}
const message = (data.error && (data.error.message || data.error)) || "Upload failed"; const message = (data.error && (data.error.message || data.error)) || "Upload failed";
throw new Error(message); throw new Error(message);
} }

View File

@ -52,8 +52,8 @@ The last two subagents are not reviewers.
Each subagent operates in one of two modes, chosen by how it is invoked. Each subagent operates in one of two modes, chosen by how it is invoked.
- **Report** (default): record findings only, change nothing. - **Report** (default): record findings only, change nothing.
- **Fix**: apply a minimal root-cause fix per the doctrine, then run the project - **Fix**: apply a minimal root-cause fix per the doctrine, then re-validate every
validator (`hawk .`) and confirm the build still imports. touched file with the per-language checks and confirm the build still imports.
A subagent never runs the test suite and never performs a git write. A subagent never runs the test suite and never performs a git write.

View File

@ -17,7 +17,11 @@ Each returns a Promise resolving when the user responds.
| `alert(options)` | `undefined` once acknowledged. | | `alert(options)` | `undefined` once acknowledged. |
`options`: `title`, `message`, `confirmLabel`, `cancelLabel`, `danger` (red confirm button), `options`: `title`, `message`, `confirmLabel`, `cancelLabel`, `danger` (red confirm button),
and for `prompt`: `label`, `value`, `placeholder`. `links`, and for `prompt`: `label`, `value`, `placeholder`.
`links` is an optional array of `{href, label}` rendered as a row of links between the message and
the buttons, each opening in a new tab so the dialog and the pending action survive the click. Use it
when the user is being asked to agree to something they must be able to read first.
## Usage ## Usage

View File

@ -52,11 +52,14 @@ order so nothing is dropped:
Never declare work done with a broken import or a validation error: Never declare work done with a broken import or a validation error:
```bash ```bash
python -c "from devplacepy.main import app" # must import clean python -c "from devplacepy.main import app" # must import clean
hawk . # Python, JS, CSS, templates python -m py_compile <changed .py> # Python syntax
python -m pyflakes <changed .py> # Python lint
node --check <changed .js> # JavaScript
``` ```
Tests live in Changed stylesheets are checked for brace balance and changed templates for tag
and `{% %}` balance. Tests live in
`tests/` and run with `make test-unit`, `make test-api`, and `make test-e2e`. `tests/` and run with `make test-unit`, `make test-api`, and `make test-e2e`.
## Read next ## Read next

View File

@ -5,6 +5,7 @@ import time
import requests import requests
from devplacepy.database import get_setting, get_table, refresh_snapshot, set_setting from devplacepy.database import get_setting, get_table, refresh_snapshot, set_setting
from devplacepy.routers.auth.terms import TERMS_ACCEPTANCE_CODE
from tests.conftest import BASE_URL from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"} JSON = {"Accept": "application/json"}
@ -152,3 +153,53 @@ def test_bumping_the_version_gates_writes_but_never_reads(app_server):
assert allowed.status_code == 200 assert allowed.status_code == 200
finally: finally:
set_setting("terms_version", original) set_setting("terms_version", original)
def test_the_refusal_tells_a_fetch_client_how_to_resolve_it(app_server):
session, _, response = _signup()
assert response.status_code == 200
original = get_setting("terms_version", "1") or "1"
bumped = f"{original}-contract"
set_setting("terms_version", bumped)
try:
blocked = None
for _ in range(40):
blocked = session.post(
f"{BASE_URL}/posts/create",
data={"title": "gated", "content": "This write must be gated."},
headers=JSON,
)
if blocked.status_code == 403:
break
time.sleep(0.5)
assert blocked.status_code == 403
error = blocked.json()["error"]
assert error["code"] == TERMS_ACCEPTANCE_CODE
assert error["redirect"] == "/auth/accept-terms"
assert error["terms_version"] == bumped
assert error["message"]
finally:
set_setting("terms_version", original)
def test_a_browser_form_post_is_redirected_to_the_acceptance_page(app_server):
session, _, response = _signup()
assert response.status_code == 200
original = get_setting("terms_version", "1") or "1"
set_setting("terms_version", f"{original}-redirect")
try:
blocked = None
for _ in range(40):
blocked = session.post(
f"{BASE_URL}/posts/create",
data={"title": "gated", "content": "This write must be gated."},
headers={"Accept": "text/html"},
allow_redirects=False,
)
if blocked.status_code == 303:
break
time.sleep(0.5)
assert blocked.status_code == 303
assert blocked.headers["location"] == "/auth/accept-terms"
finally:
set_setting("terms_version", original)

115
tests/e2e/auth/terms.py Normal file
View File

@ -0,0 +1,115 @@
# retoor <retoor@molodetz.nl>
import time
import requests
from playwright.sync_api import expect
from devplacepy.database import get_setting, get_table, refresh_snapshot, set_setting
from devplacepy.utils import clear_user_cache
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
def _seed_post(user):
session = requests.Session()
session.post(
f"{BASE_URL}/auth/login",
data={"email": user["email"], "password": user["password"]},
headers=JSON,
)
session.post(
f"{BASE_URL}/posts/create",
data={
"title": "Terms gate fixture post",
"content": "A post that exists so the vote control is present on the feed.",
},
headers=JSON,
)
def _accepted_version(username):
refresh_snapshot()
row = get_table("users").find_one(username=username)
return (row or {}).get("terms_version") or ""
def _restore_accepted_version(username, version):
refresh_snapshot()
row = get_table("users").find_one(username=username)
if not row:
return
get_table("users").update({"uid": row["uid"], "terms_version": version}, ["uid"])
clear_user_cache(row["uid"])
def _open_gate_dialog(page):
for _ in range(40):
page.locator(".post-card .vote-up").first.click()
dialog = page.locator(".dialog-overlay.visible")
try:
dialog.wait_for(state="visible", timeout=1500)
return dialog
except Exception:
time.sleep(0.5)
return None
def test_the_gate_dialog_accepts_the_terms_and_replays_the_action(alice, seeded_db):
page, _ = alice
username = seeded_db["alice"]["username"]
_seed_post(seeded_db["alice"])
original = get_setting("terms_version", "1") or "1"
accepted_before = _accepted_version(username)
bumped = f"{original}-e2eaccept"
set_setting("terms_version", bumped)
try:
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".post-card").first.wait_for(state="visible", timeout=10000)
dialog = _open_gate_dialog(page)
assert dialog is not None, "the terms gate dialog never appeared"
expect(page.locator(".dialog-title")).to_have_text("Terms of Service")
expect(page.locator(".dialog-links a")).to_have_count(3)
expect(page.locator(".dialog-confirm")).to_have_text("Accept and continue")
assert _accepted_version(username) != bumped
page.locator(".dialog-confirm").click()
page.locator(".dialog-overlay.visible").wait_for(state="hidden", timeout=10000)
recorded = ""
for _ in range(40):
recorded = _accepted_version(username)
if recorded == bumped:
break
time.sleep(0.5)
assert recorded == bumped
finally:
set_setting("terms_version", original)
_restore_accepted_version(username, accepted_before)
def test_declining_the_gate_dialog_records_no_acceptance(alice, seeded_db):
page, _ = alice
username = seeded_db["alice"]["username"]
_seed_post(seeded_db["alice"])
original = get_setting("terms_version", "1") or "1"
accepted_before = _accepted_version(username)
bumped = f"{original}-e2edecline"
set_setting("terms_version", bumped)
try:
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.locator(".post-card").first.wait_for(state="visible", timeout=10000)
dialog = _open_gate_dialog(page)
assert dialog is not None, "the terms gate dialog never appeared"
page.locator(".dialog-cancel").click()
page.locator(".dialog-overlay.visible").wait_for(state="hidden", timeout=10000)
assert _accepted_version(username) != bumped
finally:
set_setting("terms_version", original)
_restore_accepted_version(username, accepted_before)