fix(panel): P3 optional items — urlquery, aria-live, confirm-without-JS note
- Deliveries fragment/pagination links urlquery-encode the domain/app filters instead of splicing them into the query string raw. - The four polled regions (deliveries rows, status, mail queue, system log) carry aria-live="polite" so a screen reader announces refreshes. - security.md accepted risks note that data-confirm prompts are JavaScript-only and why that is acceptable. docs/plans/code-review.md P3 fully checked off; CHANGELOG updated. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -62,6 +62,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
`field_values`, `credentials_fields`) instead of copy-pasted blocks. No
|
`field_values`, `credentials_fields`) instead of copy-pasted blocks. No
|
||||||
behaviour or visible change.
|
behaviour or visible change.
|
||||||
|
|
||||||
|
- panel (GUI, accessibility): the Deliveries fragment's `hx-get` and pagination
|
||||||
|
links now `urlquery`-encode the `domain`/`app` filters instead of splicing
|
||||||
|
them into the query string raw. The four polled regions (deliveries rows,
|
||||||
|
status, mail queue, system log) carry `aria-live="polite"` so a screen
|
||||||
|
reader announces the refreshed content.
|
||||||
|
|
||||||
|
- docs: [security.md](docs/security.md) accepted risks now note that
|
||||||
|
`data-confirm` prompts on destructive forms are JavaScript-only — with
|
||||||
|
JavaScript disabled the form submits immediately, the same as before the
|
||||||
|
prompts existed — and why that is acceptable (the prompt is a mis-click
|
||||||
|
guard, not an authorization boundary).
|
||||||
|
|
||||||
- docs: full-tree review plan ([docs/plans/code-review.md](docs/plans/code-review.md))
|
- docs: full-tree review plan ([docs/plans/code-review.md](docs/plans/code-review.md))
|
||||||
— architecture, quality, GUI, tests, licence; P0 is domain-admin send-log
|
— architecture, quality, GUI, tests, licence; P0 is domain-admin send-log
|
||||||
authorization. Roadmap queues that plan ahead of inbound-relay and records
|
authorization. Roadmap queues that plan ahead of inbound-relay and records
|
||||||
|
|||||||
@@ -551,7 +551,7 @@ roles, and security.md no longer calls the panel single-user.
|
|||||||
domain delete. Done as a confirm page (`GET/POST /users/{uid}/delete`),
|
domain delete. Done as a confirm page (`GET/POST /users/{uid}/delete`),
|
||||||
matching `domain_delete.html`.
|
matching `domain_delete.html`.
|
||||||
- [x] DNS field partial; settings credentials partial.
|
- [x] DNS field partial; settings credentials partial.
|
||||||
- [ ] Optional: `urlquery` on deliveries fragment params; `aria-live` on
|
- [x] Optional: `urlquery` on deliveries fragment params; `aria-live` on
|
||||||
polled regions; confirm-without-JS note next to the CSRF accepted risks.
|
polled regions; confirm-without-JS note next to the CSRF accepted risks.
|
||||||
|
|
||||||
**Done when:** a rate-limit validation error is visually an error; user delete
|
**Done when:** a rate-limit validation error is visually an error; user delete
|
||||||
|
|||||||
@@ -127,6 +127,19 @@ deferred item from the roadmap.
|
|||||||
either: code executing in the panel's origin sends the request itself —
|
either: code executing in the panel's origin sends the request itself —
|
||||||
against that, `html/template` auto-escaping and CSP do the work, which is why
|
against that, `html/template` auto-escaping and CSP do the work, which is why
|
||||||
templates must contain no inline scripts and no inline styles.
|
templates must contain no inline scripts and no inline styles.
|
||||||
|
- **Destructive-action confirmation (`data-confirm`) is JavaScript-only.**
|
||||||
|
Delete, regenerate-password, and clear-rate-limit forms carry a
|
||||||
|
`data-confirm` prompt handled entirely in
|
||||||
|
[panel.js](../internal/web/view/static/panel.js); with JavaScript disabled
|
||||||
|
or blocked the form submits immediately, exactly as it did before the
|
||||||
|
prompts existed. Accepted deliberately: the prompt is a mis-click guard,
|
||||||
|
not an authorization boundary — the same origin check and session/RBAC
|
||||||
|
gate every one of these `POST`s whether or not JavaScript ran. Progressive
|
||||||
|
enhancement means the panel must work with JavaScript off; a
|
||||||
|
server-rendered confirmation step would need a second page (or a `?confirm=1`
|
||||||
|
round trip) for every one of these forms, which is what
|
||||||
|
[`user_delete.html`](../internal/web/view/templates/user_delete.html) and
|
||||||
|
`domain_delete.html` already do for the two highest-blast-radius deletes.
|
||||||
- **Encrypting backups and exports is an option, not the default.** With the
|
- **Encrypting backups and exports is an option, not the default.** With the
|
||||||
checkbox cleared the file downloads in the clear, as in 1.0. Otherwise an
|
checkbox cleared the file downloads in the clear, as in 1.0. Otherwise an
|
||||||
operator with nowhere to keep a password would lose the ability to take a
|
operator with nowhere to keep a password would lose the ability to take a
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{{define "deliveries_rows"}}
|
{{define "deliveries_rows"}}
|
||||||
<div id="deliveries-rows" data-poll hx-get="/deliveries/rows?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.Page}}"
|
<div id="deliveries-rows" data-poll aria-live="polite"
|
||||||
|
hx-get="/deliveries/rows?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.Page}}"
|
||||||
hx-trigger="load" hx-swap="outerHTML">
|
hx-trigger="load" hx-swap="outerHTML">
|
||||||
{{if .Rows}}
|
{{if .Rows}}
|
||||||
<table>
|
<table>
|
||||||
@@ -14,15 +15,15 @@
|
|||||||
<td>{{.To}}</td>
|
<td>{{.To}}</td>
|
||||||
<td class="subject"><span title="{{.Subject}}">{{.Subject}}</span></td>
|
<td class="subject"><span title="{{.Subject}}">{{.Subject}}</span></td>
|
||||||
<td class="status"><span class="st st-{{.Level}}">{{.Status}}</span></td>
|
<td class="status"><span class="st st-{{.Level}}">{{.Status}}</span></td>
|
||||||
<td class="actions"><a href="/deliveries/{{.ID}}?domain={{$.FilterDomain}}&app={{$.FilterApp}}&p={{$.Page}}">Details</a></td>
|
<td class="actions"><a href="/deliveries/{{.ID}}?domain={{$.FilterDomain | urlquery}}&app={{$.FilterApp | urlquery}}&p={{$.Page}}">Details</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Page {{.Page}}{{if .LastPage}} of {{.LastPage}}{{end}}
|
Page {{.Page}}{{if .LastPage}} of {{.LastPage}}{{end}}
|
||||||
{{if .HasPrev}} · <a href="/deliveries?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.PrevPage}}">← Newer</a>{{end}}
|
{{if .HasPrev}} · <a href="/deliveries?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.PrevPage}}">← Newer</a>{{end}}
|
||||||
{{if .HasNext}} · <a href="/deliveries?domain={{.FilterDomain}}&app={{.FilterApp}}&p={{.NextPage}}">Older →</a>{{end}}
|
{{if .HasNext}} · <a href="/deliveries?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.NextPage}}">Older →</a>{{end}}
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="muted">No messages logged yet.</p>
|
<p class="muted">No messages logged yet.</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "mail_queue_body"}}
|
{{define "mail_queue_body"}}
|
||||||
<div id="mail-queue-body" data-poll hx-get="/mail-queue/body" hx-trigger="load" hx-swap="outerHTML">
|
<div id="mail-queue-body" data-poll aria-live="polite" hx-get="/mail-queue/body" hx-trigger="load" hx-swap="outerHTML">
|
||||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "status_body"}}
|
{{define "status_body"}}
|
||||||
<div id="status-body" data-poll hx-get="/status/fragment" hx-trigger="load" hx-swap="outerHTML">
|
<div id="status-body" data-poll aria-live="polite" hx-get="/status/fragment" hx-trigger="load" hx-swap="outerHTML">
|
||||||
<div class="card" id="overall">
|
<div class="card" id="overall">
|
||||||
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
|
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
|
||||||
<p class="muted">{{.OverallHeading}}</p>
|
<p class="muted">{{.OverallHeading}}</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{{define "system_log_body"}}
|
{{define "system_log_body"}}
|
||||||
<div id="system-log-body" data-poll hx-get="/system-log/body" hx-trigger="load" hx-swap="outerHTML">
|
<div id="system-log-body" data-poll aria-live="polite" hx-get="/system-log/body" hx-trigger="load" hx-swap="outerHTML">
|
||||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||||
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
||||||
{{end}}{{else}}No log lines yet.{{end}}</span>
|
{{end}}{{else}}No log lines yet.{{end}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user