audit: cycle copy-close tidy — reconcile spec to shipped structure

Cycle-close audit for the case-page copy+close cycle (spec 0002, plan
0003, feat 0308245). Architect drift review + scripts/check.sh.

Regression gate (scripts/check.sh): exit 0 — 0 failures across 19
stages (common/server/clients-desktop/experiments fmt+clippy+build+test,
wearos assembleDebug+lintDebug+testDebugUnitTest), 74s. The single
`wearos :: lintDebug` WARN is pre-existing and untouched by this
frontend-only cycle; not introduced here, carried forward.

Architect findings and resolution:
- [high] template emits two separate close forms, not the spec's single
  #copy-close-form with a has_document label switch. Resolved as a SPEC
  defect, not a code defect: the spec's single-form snippet was
  infeasible — #doc-body exists only in the has-document match arm, so a
  form anchored after it can never serve the documentless case, and it
  must precede the copy <script> to be bound. The shipped two-form split
  is canonical; spec 0002 §Concrete code shapes + §Data flow reconciled
  to it (this commit).
- [high] the spec's `if (!getElementById("doc-body")) return` guard is
  absent from the shipped handler. Not a defect: the no-document form
  carries no id, so the handler (which binds #copy-close-form) never
  attaches to it — the guard is structurally unnecessary. Spec script
  block + prose updated to state this mechanism.
- [medium] the documentless form's distinctness was unpinned. Added an
  assertion to case_page_empty_case_shows_placeholder that the empty
  case renders no #copy-close-form (it is the bare close form).
- [low] plan counter 0003 vs spec counter 0002: not drift — the naming
  policy is per-directory counters (stable_per_directory_4digit over
  [docs/specs, docs/plans]); independent sequences are expected.
- [note] deferred case-list half of #14 leaves a coherent state
  (refs #14, my_cases.html untouched). No drift.

Recommendation: carry-on. No fix iteration needed; the high items were
spec-vs-code reconciliation (docs) and a test guard, both done here.

refs #14
This commit is contained in:
2026-06-01 14:40:06 +02:00
parent 0308245524
commit 54a48691d1
2 changed files with 40 additions and 10 deletions
+33 -10
View File
@@ -41,7 +41,18 @@ server code moves.
What renders directly under the document for an **open** case. For a
case *with* a document the button copies then closes; for an open case
*without* a document it is a bare close (nothing to copy):
*without* a document it is a bare close (nothing to copy).
**Two physically separate forms, not one with a label switch**
(reconciled to the shipped implementation at audit). `#doc-body` only
exists in the `{% when Some %}` (has-document) arm of the template's
`{% match document_html %}`, so a single form anchored after it could
never serve the documentless case — and it must be emitted *before* the
copy `<script>` so the submit handler can bind it. The two cases
therefore live in their two respective match arms:
In the has-document arm, after `#doc-body` — the combined control,
wired to `copyDocText()`:
```html
<div id="doc-body">{{ html|safe }}</div>
@@ -49,11 +60,7 @@ case *with* a document the button copies then closes; for an open case
<div class="doc-footer-actions">
<form id="copy-close-form" method="post" action="/cases/{{ case_id }}/close">
{% call csrf::field(csrf_token) %}
{% if has_document %}
<button type="submit" class="copy-close-btn">Kopieren + Schließen</button>
{% else %}
<button type="submit" class="copy-close-btn">Schließen</button>
{% endif %}
</form>
<p id="copy-close-error" class="copy-error" role="alert" hidden>
Kopieren fehlgeschlagen — Fall nicht geschlossen.
@@ -62,6 +69,21 @@ case *with* a document the button copies then closes; for an open case
{% endif %}
```
In the no-document arm — a bare close form. It carries **no** `id`, so
the submit handler below (which binds `#copy-close-form`) never attaches
to it and it submits normally (nothing to copy):
```html
{% if !is_closed %}
<div class="doc-footer-actions">
<form method="post" action="/cases/{{ case_id }}/close">
{% call csrf::field(csrf_token) %}
<button type="submit" class="copy-close-btn">Schließen</button>
</form>
</div>
{% endif %}
```
The inline script, with the two-path clipboard write extracted into a
shared function used by both the icon copy button and the close form:
@@ -115,11 +137,12 @@ shared function used by both the icon copy button and the close form:
// the user-gesture context before the form navigates, and a failed
// copy aborts the close so the user never loses a document they
// believed was on the clipboard.
// Only the has-document form carries id="copy-close-form", so this
// handler binds only there; the bare no-document "Schließen" form is
// never matched and submits normally (no copy).
const closeForm = document.getElementById("copy-close-form");
if (closeForm) {
closeForm.addEventListener("submit", async (e) => {
// Bare "Schließen" (no document) submits normally, no copy.
if (!document.getElementById("doc-body")) return;
e.preventDefault();
if (await copyDocText()) {
closeForm.submit(); // close → 303 → /cases
@@ -184,9 +207,9 @@ No new template variables: `is_closed`, `has_document`, `case_id`, and
5. On `false`: `#copy-close-error` is unhidden; no submit; case stays
open.
The bare **Schließen** button (open case, no document) has no
`#doc-body`, so the handler returns early and the form submits
normally → same close → `/cases`.
The bare **Schließen** button (open case, no document) lives in a
separate form with no `id`, so the submit handler never binds it; it
submits normally → same close → `/cases`.
## Error handling