Accessibility
- Repeated row buttons have no context — a screen reader hears "Remove, Remove, Remove". Needs
aria-label="Remove async function name() { return (await authUser())?.name; }":server/services.stx:64-69(6 labels × N rows),server/workers.stx:63,server/deployments.stx:104,server/ssh-keys.stx:82,server/team.stx:154,serverless/secrets.stx:92,93,serverless/alarms.stx:107,serverless/queues.stx:98,server/database.stx:112. - Confirm input has no accessible name — only
placeholder="confirm":partials/op-confirm.stx:3and the four hand-rolled bars. <label>not associated with its control (nofor, not wrapping):server/sites.stx:102-109,158-178,serverless/secrets.stx:118,122,serverless/alarms.stx:132,140,serverless/data.stx:79,83,serverless/functions.stx:151,159,163. The correct pattern already exists inserver/team.stx:107,server/database.stx:76,server/firewall.stx:99.- No label at all:
server/actions.stx:84,serverless.stx:276,server/terminal.stx:57. - No
scope="col"on any<th>across ~15 tables. - No
aria-liveon streaming/result output (server/terminal.stx:55, every<pre class="action-output">).server/team.stx:131,server/firewall.stx:104,server/ssh-keys.stx:65already userole="status"correctly. - No focus management when the confirm bar appears.
stx hygiene
CLAUDE.md states: "never write vanilla JS (var, document.*, window.*) in stx templates". Violations:
server/terminal.stx—document.getElementById('term-out')+ manualscrollTop(:15),location.protocol/location.host(:20-21), rawnew WebSocket(:21), module-scope mutablelet ws = null(:13) outside the signal graph. Would suit auseWebSocket-style composable with scrolling driven by a directive.server/sites.stx:30,54,74—location.reload()as the state-update mechanism.- Raw
setIntervalinindex.stx:133,server/logs.stx:30,serverless/logs.stx:27(they do clean up on unmount, but the composable layer has an interval primitive).