Commit Graph
6 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 cd209483e3 fix the clipboard over http, and audit the rest
navigator.clipboard is secure-context only, like crypto.randomUUID before it —
over plain http on a tailnet address the object does not exist. Twenty call
sites across eighteen files, in three states that all looked fine in review:
bare calls that threw and killed the handler, optional-chained calls that
silently did nothing, and one carrying the comment "Officer is always behind
HTTPS", which it is not.

The optional-chained ones are the worst of the three: a copy button that reports
success and copies nothing is indistinguishable from a working one until someone
pastes.

helpers/clipboard.ts falls back to document.execCommand('copy') over an
off-screen textarea — deprecated, and it works on any origin because it predates
the secure-context rule. Off-screen rather than hidden, because display:none and
visibility:hidden elements cannot be selected and the copy fails silently.

Reading the clipboard has no equivalent: execCommand('paste') was never permitted
from script. The file browser's paste-a-file path now checks canReadClipboard()
and explains itself instead of throwing.

docs/http-secure-context-audit.md is the full sweep the owner asked for: what was
fixed, what cannot be, and what was checked and found clear. crypto.subtle is
used nowhere in the frontend, which was the one worth confirming since it has no
cheap fallback. Notification's six matches are type names, not the API.
geolocation and navigator.share are already guarded. getUserMedia is in four
files and is being removed — but QrTransfer uses it for the CAMERA, not a
microphone, so "remove audio" does not cover it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:56:57 +00:00
pastilhasandClaude Opus 5 74b4c7908a fix the crash over http: crypto.randomUUID is secure-context only
Chat took the whole page down at the end of every turn with
`TypeError: crypto.randomUUID is not a function`.

`crypto.randomUUID()` is SECURE-CONTEXT ONLY — over plain http on anything that
is not localhost it is not defined at all. Officer is reached at
http://officer-dev:9000, which is neither, so all eighteen call sites in the
frontend were throwing. The stack shows why it was fatal rather than merely
broken: it was called inside a `useState` initialiser, so the throw happened
during render and unmounted the tree. The assistant message that has no id yet
is created at the end of a turn, which is exactly when it fired.

No TLS needed. `crypto.getRandomValues()` carries no such restriction — it is on
`Crypto`, not `SubtleCrypto`, and works in an insecure context. helpers/random-id
uses randomUUID when it exists and otherwise assembles a v4 from the same CSPRNG:
same 122 bits, same version and variant bits. Verified both paths produce a UUID
matching the v4 pattern, including with randomUUID deleted.

`crypto.subtle` is not used anywhere in the frontend, so randomUUID was the whole
of the problem. Audio recording is a different matter — getUserMedia genuinely
requires a secure context and cannot be polyfilled.

Nine files, eighteen call sites. The vendored hls.mjs is left alone.

Two of my own mistakes on the way, both caught by parsing rather than by reading:
the rewrite added an import of the helper TO the helper, and inserted another one
inside a multi-line import block — the same trap as the officerdb move earlier
tonight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:23:31 +00:00
pastilhasandClaude Opus 5 ba664dc5c8 per-panel content zoom for every panel
soulseek had a zoom control wired into its own panel header, persisted
under its own screens/ key. move it into the framework so every panel has
it, and drop the soulseek-specific copy (its header was then identical to
the default, so that goes too, along with the orphan db row).

the factor lives on the LayoutPanel node rather than in its own
useDashboardState key: the layout is already persisted per panel, and a
separate key would seed a server row per panel on mount. absent at 1, so
an untouched panel adds nothing to the stored layout.

uses css zoom, not transform: scale. a transform repaints at a different
size without re-laying out, so the panel keeps its 100% geometry and
anything anchored or percentage-sized lands wrong — chat's composer made
that obvious. zoom scales used lengths instead: children reflow, h-full
still resolves to the panel, and rem-based tailwind text scales with it.

@container moves onto the zoomed element so container queries respond to
the effective width, the way they would in a genuinely narrower panel.

zoomable: false opts out the terminals (xterm measures its own cell grid)
and remote desktop (novnc does its own scaling and pointer mapping).

fixes chat's virtualiser under zoom: it measured bubbles with
getBoundingClientRect (rendered px) but positions them with translateY
(layout px), so at 70% every bubble was placed too early and they stacked.
new helpers/measure-zoomed divides the element's currentCSSZoom back out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 02:58:25 +00:00
pastilhasandClaude Opus 5 9eb8fa1c17 say something when the chat fails silently
read-aloud swallowed every error: the spinner stopped, the speaker icon came back, and a
TTS service that was simply down looked identical to a button that did nothing. Both the
synthesis failure and the playback failure now say so.

errorText moves to helpers — useClient rejects with a plain { status, message } object
rather than an Error, so the reflex instanceof check reports every API failure in the app
as "unknown error". Two callers now, and it is the wrong thing to reimplement per file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:33:10 +00:00
pastilhas 9870fa7ae8 /workspaces refactor 2026-02-19 16:07:00 +00:00
pastilhas 9ab0940ca4 first 2026-02-16 19:34:35 +00:00