Commit Graph
131 Commits
Author SHA1 Message Date
pastilhas 5daa598b63 put the browsed folder in the url on /files
the file browser's currentPath was useState, so back and forward did nothing and a
folder could not be linked to. it is `?path=` now on /files, and the breadcrumbs are
real links.

opt-in, keyed on the parsed workspace identity rather than the base path: a dashboard
can hold two file browsers and one shared param would move both, while an unscoped
panel (cwd `~`) sits on dashboards too, so `basePath === '/'` would have caught the
wrong ones.

two things the audit line did not know. `?view=` is ephemeral — useFileViewerPanels
wipes it on mount — so `path` is this screen's first durable param. and four
setSearchParams({...}) calls replaced the whole query string, which would have made
opening any file silently reset the folder to home; they go through a setViewerParams
helper now that carries `path` across.

folder rows stay buttons. cmd/ctrl/shift-click is already multi-select in FileItem and
open is double-click, so anchor semantics collide with a gesture that exists. that is a
product decision, not a defect — written up for the owner rather than guessed at.
2026-08-07 12:28:49 +00:00
pastilhasandClaude Opus 5 ec4aaaae8a put the open task log and the followed run in the url
task-logs was a clean move — the detail fetch already keyed off the id, so only
its source changed. activity needed one decision: its two row kinds stream
through different query params, so the url carries the id and the screen derives
task= or path= from the registry row. the sse effect now depends on that derived
string rather than a fresh object, so the 3s poll cannot re-open the stream. an
id that has left the registry says so instead of waiting for output forever.

/activity also had no page-title rule and read 'Officer'.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:19:47 +00:00
pastilhasandClaude Opus 5 a55ea1882a put the open capability in the url
/tasks, /skills and /processes are one component, so one route pair each and the
rows become links. drops the auto-select-items[0] effect: the bare route is the
list with nothing open, which is a real state. editing and the just-created flag
move to ?edit=1 / ?new=1 — a link row cannot reset them on the way out, and
deriving them means navigating to another item clears them for free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:16:11 +00:00
pastilhasandClaude Opus 5 aef8619c6c put the music library location in the url
/music?path=<rel> replaces the music:cwd channel. Each panel reads the param
itself through useMusicCwd(), so MusicBrowser, MusicDetail and FavoritesView
no longer tell each other where they are, and every drill-in is a <Link>:
library rows, folder rows, album/artist cards, both "up" affordances, the
favorites rows, and the dock's now-playing tile. Track rows stay buttons —
they play, which is a mutation.

A query param rather than a nested route because the location is only one of
the things this screen holds (the lyrics split and the favorites view are the
others), and a splat has to be a route's last segment.

MusicPlayerHost is mounted outside <Routes> and used to write the channel and
then navigate('/music') to make the write visible — the audit's only
navigate-with-a-side-effect. That collapses to one <Link>.

music:resync (a refresh signal) and music:favorites (a view of one panel) stay
channels, deliberately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:08:04 +00:00
pastilhasandClaude Opus 5 374140d3a6 put the previewed browser tab in the url
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:56:45 +00:00
pastilhasandClaude Opus 5 6c47cbeb74 make the email url the selection instead of a mirror of it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:51:45 +00:00
pastilhas fd923bb9be give the soulseek workspace a url
The section is /soulseek/:section — nav entries are NavLinks, the view panel reads the same
URL instead of being told, and the dashboard's tiles and recent searches are real links (a
recent search now opens that search, not the search screen's front page).

The peer went in `?user=<name>` rather than the /soulseek/users/:name the audit sketched: a
second path segment would need a nested route just to keep the nav highlight, and `?search=`
had already set the convention there. That deletes the `soulseek:user` channel and with it a
`{username, nonce}` request the Users panel consumed-once and cleared — the nonce existed so
asking for the same peer twice counted twice. A link is idempotent, so there is nothing to
consume and nothing to disambiguate.

`soulseek:refresh` stays: it is a signal, which is what channels are for.
2026-08-07 11:45:33 +00:00
pastilhas 00332e275a put the monitor scope in the url
/system-monitor/:scope, the same shape as /photos: route pair, one Navigate guard after the
hooks, scope list as react-router NavLinks, and both panels reading useParams instead of
agreeing over a `monitor:scope` channel. The Dock's isActive is a startsWith, so its
highlight survives the redirect off the bare route.
2026-08-07 11:41:16 +00:00
pastilhas 2502c33804 put the settings section in the url
Five settings pages moved from a `*_SELECTED` global to `/settings/:page/:section`. The
sidebar entry is a react-router `<NavLink>` rather than a button holding the key in its
onClick closure, so a section is linkable, cmd-clickable and gets its active state from the
router; each page renders one `SettingsRoute` guard that canonicalises both the bare route
and a section that does not exist.

Integrations needed more than the shared factory. It builds its own sidebar, and it kept the
Enterprise/Personal tab in a second global — which is why a deep link to a Personal section
could never have worked: the link set the section, the tab stayed on Enterprise, and the
content pane said "Select a section" about a section that existed. The tab is derived from
the section key now.

Also removes the `/settings/resources` menu item (audit M8) and its two locale keys: there
has never been such a route, so it bounced to the catch-all and out to `/`.
2026-08-07 11:38:21 +00:00
pastilhas 98ba61f135 record the sweep commit 2026-08-07 11:26:39 +00:00
pastilhas b419af32de sweep the dead code in section 8
Each item re-verified before deleting; three of the ten entries were stale
and are corrected in place rather than silently fixed.

- WorkspaceLayout's isMobile/mobilePanelId/onMobileBack: none of its ten
  callers set them, so the mobile collapse they fed was permanently off in
  that renderer. WorkspaceView passes the same props to WorkspaceRenderer
  itself, where they are live.
- fixedHeight on AppRegistryEntry, and getFixedHeight with it: no app has
  ever declared one, so it only contributed undefined. The flex-column
  branch it shared with fitContent stays, keyed on fitContent alone.
- getDefaults: getAllDashboardState already folds the defaults row into the
  one payload the client fetches, which is why it never got a caller.
- upsertScreen's terminals/hostTerminals: never read is right, never
  written was not — it inserted them, which is why all 15 rows hold {}.
  The columns are left in place; dropping them needs a db:push, and this
  tree holds another agent's uncommitted schema file.
- SELECTED_DASHBOARD_KEY: H2 (01365cb) replaced it with ?selected= four
  months ago and it has had no reader since.
- ui/sidebar.tsx and the stray ui/hooks/ beside it. use-mobile was not
  orphaned as claimed — the sidebar imported it — and the use-toast in
  there was a near-identical copy of the live one.
- findChildById's unreachable duplicate condition, and the doc comment that
  described the wrong behaviour rather than the code being wrong.

Left deliberately: DragOverlay/LayoutEditor (gated on 5.3, an owner
decision) and the two chat-owned channels, whose docs are fixed here even
though the publishers are not mine to delete.
2026-08-07 11:26:33 +00:00
pastilhasandClaude Opus 5 f2ae10bd36 mark 5.10 resolved
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:15:00 +00:00
pastilhasandClaude Opus 5 30fcab2bd3 mark 5.6 resolved, and stop three docs claiming a channel that has no publisher
The todo entry said the file-viewer registration was dead; tracing it confirmed that and turned up the
reason it looked alive — the ephemeral file viewer is a different mounting path entirely. Recorded, with
what was checked in the database before deleting anything.

CLAUDE.md, navigation-audit.md and workspace-panels.md all listed FILE_VIEWER_CHANNEL among the
legitimate refresh/signal channels. It never had a publisher, and no longer exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 11:07:36 +00:00
pastilhasandClaude Opus 5 04ccb05b2c mark the effect-hygiene items resolved, and correct two of them
Two entries in 5.7 were wrong. HostTerminalWrapper was fixed in c92b51c, when
all three wrappers moved onto useTerminalSession — the item had simply not been
re-read since. And useTaskRunner does not abandon a running task: `stop` is sent
from the modal's Stop button, and closing the socket kills the process tree
server-side. Both were written from the hook alone without following the call
into the modal or the executor.

VideoPlayer and the remaining VideoPlayer-shaped case are left alone on purpose,
with the reason written down rather than the item deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:57:10 +00:00
pastilhas e2017cbbf2 correct 5.2: the key was never the cause 2026-08-07 10:46:31 +00:00
pastilhas 34b40cb094 mark preserve-sibling-sizes resolved 2026-08-07 10:41:32 +00:00
pastilhas 4ce7839b31 mark the panel-id and registry-key items resolved 2026-08-07 10:38:59 +00:00
pastilhasandClaude Opus 5 3ab57a5839 measure what a handoff actually survives
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:32:18 +00:00
pastilhasandClaude Opus 5 df00f6d7a5 tier b has no unblocked work left
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:16:09 +00:00
pastilhasandClaude Opus 5 49b635a489 drop §5.4's default-layout collapse, and say why
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:15:58 +00:00
pastilhasandClaude Opus 5 d546717676 note the unguarded screens are done too
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:10:32 +00:00
pastilhasandClaude Opus 5 99ee9dbb88 record §5.4's first item as landed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:07:33 +00:00
pastilhas 2c84bb34be say which shells nobody is looking at
The sidecar has always sent `clients` on each session and this list has always dropped it, so a shell
you are typing into and a shell nothing is attached to rendered identically. It is now on the type,
shown as "N attached", and a zero earns the row an orphan badge — the count only helps if you do not
have to read it to notice.

Also records §5.1 and the whole of §1 in the todo, including that the diff-the-layout implementation
§5.1 used to propose is struck and why.
2026-08-07 09:58:04 +00:00
pastilhas fed2badd28 record the close of §5.9 2026-08-07 09:43:30 +00:00
pastilhas b1e0bac1e0 record the first three items of §5.9 2026-08-07 09:36:27 +00:00
pastilhas b1ec1e19a7 record the close of §4 2026-08-07 09:24:23 +00:00
pastilhas 2efd7ffba3 record §5.5 and the close of tier a 2026-08-07 09:19:59 +00:00
pastilhasandClaude Opus 5 d24f3faa5f record §4: the boundary, validate-on-read, and the layout default
Tier A now has only §5.5 left — the resize debounce and the stale cache.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:09:22 +00:00
pastilhasandClaude Opus 5 30eef86972 record what landed: A1, A2, the tests, and what tier A has left
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:57:13 +00:00
pastilhasandClaude Opus 5 ecc7fb90d8 rank the panel defects against the objective, not against severity
The re-rank the north star deferred, done now that the MVP is built and
running — so it is ranked against what the mechanism turned out to need.

The finding is that most of the list is not on this path. The mechanism is
server-side and a panel is a pointer to it, so a remount, a re-render or a
drag costs a replay, not a session. Section 5.2 and 5.3 are large downgrades;
5.3 was on the critical path when the north star was written and is disarmed
by resolving identity by name.

What is left is small and mostly one defect wearing four hats: a write that
silently does not land. Panel identity lives in the layout jsonb now, so the
swallowed persist catch, the dispatcher's missing else and the two debounce
lost-updates each become a panel that forgets which agent it is — invisibly,
for exactly as long as nobody is looking.

Also corrects two items the MVP made stale, and promotes layout-utils tests:
e588524 put agent identity inside those mutators and shipped them untested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 08:40:33 +00:00
pastilhasandClaude Opus 5 7c99429872 write down the north star: agents coordinating with each other
docs/agent-coordination.md is the objective the workspace/panel work serves —
several agents on one dashboard handing work to each other instead of routing
every step through the human, with the human authoring the workflow at the top.
Written from the owner's own words during the 2026-08-07 conversation; where a
section records a decision, that decision is his.

It settles the questions that were blocking: sessions may be reaped and resumed
from the durable sessionKey→claudeSessionId map (no heartbeat), the address is
the human-assigned panel name rather than the panel id, roles are prompts rather
than features, and the protocol is turn-boundary-only by construction — which
routes around the mid-output restart failure instead of fixing it.

Cross-referenced from CLAUDE.md, workspace-panels.md and workspace-panel-todo.md
so it is findable from any of them. The todo is still ordered by defect severity
and now says so; the re-rank against the objective is deferred, not forgotten.

Also corrects two items dated 2026-08-08 to the day they were actually found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 06:58:01 +00:00
pastilhas ce7968ac90 document how the workspace/panel framework works 2026-08-07 04:10:36 +00:00
pastilhasandClaude Opus 5 575b4a5966 add a living todo for the workspace/panel framework
the framework has no tests, no error boundaries and a handful of known
defects that keep resurfacing mid-feature. write them down once, ranked,
so they can be picked off in the context of whatever is being built.

notable: the dashboards PATCH dispatcher silently drops any key family it
has no branch for, and three in use today (tmux, nvim, claude-code, all
from Terminal's statePrefix) match nothing — so that state never persists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 02:58:16 +00:00
pastilhasandClaude Opus 5 5134501a2f walkthrough: the tab-name clone check was a guess
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 02:27:12 +00:00
pastilhasandClaude Opus 5 66fca28927 retry a turn the agent restart cut off
the cut-off notice is its own event now rather than an error: nothing is broken
and nothing is lost but the turn, so the row says what happened and offers the
one action that fixes it. the conversation is already durable — the claude
session id is written through to disk and passed back as resume: — so retry
just resends the prompt on a session the fresh agent picks up with full
context. read back out of the transcript, so a second window on the same
session can offer it too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 02:00:16 +00:00
pastilhasandClaude Opus 5 876b39b301 end a turn whose agent has gone
restarting officer-agent takes every persistent session with it and nothing
downstream notices: the browser's socket is healthy, officer's subscription is
a bus filter, and there is simply never another event. the spinner ran forever
and a refresh didn't help, because the transcript has no ending to read.

keyed off the agent *registering*, not disconnecting — a disconnect fires on
every `pm2 restart officer`, when the turn is fine. a registration socket dies
with its process, so an agent appearing on it is a new one. covers the sitting
tab; the reconnect path covers the rest, with the client now sending its belief
that a turn is in flight and officer checking it against the agent over a new
claude:is-generating. the check fails toward alive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:43:01 +00:00
pastilhasandClaude Opus 5 f283ebcba3 code block copy button is always visible
hover-revealed means most people never find it, and touch has no hover at all.
70% white on the dark block, brightening on hover; the pre reserves right
padding so a long first line scrolls up to the button instead of under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:35:50 +00:00
pastilhasandClaude Opus 5 6a682fae98 copy button on every code block
the bubble's copy button copies the whole reply, which is the wrong unit when
the reply is prose ending in one command to run. fenced blocks get their own
button; inline code doesn't. text read from textContent at click time rather
than the markdown ast, trailing newline stripped so a pasted command doesn't
run itself. the positioned wrapper takes the vertical margin, or the pre's own
margin collapses through it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:22:57 +00:00
pastilhasandClaude Opus 5 29fc9722c1 interrupted by user, not an error
pressing stop ended the turn with "Claude Code returned an error" — the agent
sdk reports interrupt() as an ordinary failed result, indistinguishable from a
real fault downstream. the sidecar now flags the session it interrupted and
rewrites that event to the existing durable 'stopped', which opencode already
emitted. escape stops the turn (bound to the chat subtree, not the document),
and the prompt comes back to the composer verbatim unless you've started typing
something else. history parity: claude files [Request interrupted by user] as a
user message, so the transcript reader maps those exact strings to the same
role instead of replaying them as something you typed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 01:18:57 +00:00
pastilhasandClaude Opus 5 7700e8b540 make the editable tab name survive refreshes
sessionStorage is the per-tab store — separate per tab, survives reload and
navigation, dies with the tab. The route title is derived rather than assigned,
so navigating no longer wipes a name you typed.

Duplicating a tab clones sessionStorage, so a `navigate` that arrives already
holding a name is treated as a clone and drops it; fails soft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:59:29 +00:00
pastilhasandClaude Opus 5 47894702ac merge a /clear chain into one conversation
The list collapses each chain to its newest link — the only one that can be
resumed — carrying the root's title and start time, the summed message count
and a part badge. The detail splices the chain's transcripts oldest-first with
a divider between parts, server-side, so the client's index-window pagination
needed no change.

The divider says "context cleared — nothing above this is in memory", because
the whole risk of merging is that the history reads as continuous when the
agent's context is not. Delete cascades the chain and the confirm says how many.

Supersedes the "continues X" line from the previous commit: there is nowhere to
link to once the parent is scrolled up above you.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:44:33 +00:00
pastilhasandClaude Opus 5 942cc2b61d link cleared sessions back to the conversation they continue
/clear starts a new claude session and the list showed it as an unrelated
conversation. claude records no parent link anywhere — not in compactMetadata,
logicalParentUuid, summary.leafUuid, the per-session slug, or the live process
registry — so infer it: a cleared transcript opens with /clear, and /clear
happens inside one process, so the parent is the conversation in the same group
that was writing to disk at the instant this one began (4ms apart, measured).

matching is on per-minute activity rather than updatedAt, because resuming a
parent moves its end time past its child's birth and lost the link entirely for
two of the three cleared sessions here. the window is symmetric because clearing
makes claude summarise the conversation it is ending, so the parent's final
record can land after the child's first. ambiguity fails closed — the wrong
parent also renames the conversation.

read-only: nothing is written back to claude's store, and an explicit title
always wins.

also: cleared sessions were titled "<command-name>/clear</command-name>" because
claude does not set isMeta on slash commands; and the chat header was hardcoded
to undefined, so it read "New chat" above every conversation you opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 00:22:28 +00:00
pastilhasandClaude Opus 5 236541fa2a chat urls: a group is a path, a session decides its own directory
the chat group moves from ?cwd= to a path suffix behind a g/ discriminator
(/chat/g/home/me/project), and a session url carries no group at all.

the real fix is not the spelling. a session's working directory was read back
off the query string to decide where the agent executes, so the address bar was
the authority on where code runs. a pasted or refreshed /chat/<id> arrives with
no ?cwd= at all, so a turn sent before the resolve landed ran in the default
general_chat_sessions dir instead of the project; and a hand-edited ?cwd= could
name a group the session doesn't belong to, with nothing to reconcile them.

loadClaudeSessionById already resolves a session's cwd from the id alone, so the
id is the only source of truth there. it now travels on SelectedSession.cwd,
which is what the composer reads. the url can no longer contradict it.

the vocabulary lives in apps/ChatHistory/chat-routes.ts so a link built in a
panel and one built in a screen cannot drift.

also: startAgentRun no longer returns a literal chatUrl — it returns cwd and
AgentRunnerModal builds the link, so the server holds no copy of the frontend
url shape. and the post-turn permalink strips a stale ?cwd= instead of carrying
it forward onto the new session's url.

walkthrough doc gains item 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:59:39 +00:00
pastilhasandClaude Opus 5 203f65d03f add a walkthrough for the chat ui changes
twelve items in click-through order, each with where to look and what the old
behaviour was — several are only visible if you know what was broken. states
plainly at the top that none of it has been rendered in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:34:44 +00:00
pastilhasandClaude Opus 5 88652910d7 add the interface half of the duck suite design language
DuckSuite_Design_Language.md covers icons only — mascot geometry, lighting,
one-idea-per-icon, readable at 64px. It says nothing about type, density or
data, which is why the chrome looks considered and every data view does not.
This is the companion document plus the primitives that enforce it.

The diagnosis it is written against, from grepping two apps: 11 uses of
text-[10px], 7 of text-[11px], 52 of text-xs and 3 of text-base, with no rule
about which meant what; six radius values; hand-picked emerald/amber/red/purple
next to unused --success/--warning/--destructive tokens; text-black/50 and
ring-black/5, which are invisible in dark mode. None of that was a bad decision,
it was the absence of one thirty times over — so the fix is to remove the choice
rather than to have better taste.

docs/design-language-interface.md sets four type ranks with a 12px floor, one
focal point per row, five state tones, three radii and a spacing rhythm. The
principles are lifted from the icon language rather than invented, because
"one focal point, reads instantly, no unnecessary decorations, if it needs
explanation it is too complicated" is already the right rule for a dense list.

components/Data/ is how you spend that vocabulary: DataRow/DataList/RowMeta,
StatusPill/StatusIcon, LoadingBlock/ErrorBlock/EmptyBlock, RelativeTime. Rules
you have to remember are rules thirty views already broke, so the shape encodes
them — DataRow takes exactly one title and everything else is meta, RowMeta
puts separators between items so a trailing dot cannot appear, RelativeTime
carries the absolute timestamp as a hover title.

Adds --info (violet) as the fifth semantic tone, light and dark. "Merged" and
"in progress" are neither good news nor bad, and painting them with --success
makes a merged PR and an open one look like the same thing.

Entirely additive: nothing imports these yet, so no existing view changes and
there is no collision with the other agent working in this tree. Typechecks
clean. Not yet rendered in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:16:42 +00:00
pastilhasandClaude Opus 5 e54d71da71 api: five reads that were declared as writes are now GET
The permission model being built reads the HTTP method to decide whether a
non-owner may make a call: safe methods are reads, everything else is a write.
That only works if the method tells the truth. These five read something and
returned it while announcing themselves as writes, so a member would have been
denied a read they are entitled to because of a habit in how the route was
declared.

  /api/file-browser/video-info         POST {url}  -> GET ?url=
  /api/file-browser/video-playlist     POST {url}  -> GET ?url=
  /api/server-settings/ocr/models      POST {url}  -> GET ?url=
  /api/transmission/_officer/port-test POST        -> GET
  /api/jellyfin/_config/:id/test       POST|GET    -> GET only

The last one already answered to both, which is worse than either: a method that
means nothing cannot be the thing authorisation reads.

Deliberately stops at five. A sweep of all 100 mutating routes found many more
reads wearing POST, and they are staying, for two reasons that are not going
away: some need a request body GET cannot carry (/stt takes multipart audio;
/tts, /ocr, /transcribe take payloads), and some carry a credential, where a
query string is the wrong place — access logs, shell history and Referer headers
all capture those, request bodies do not (/tts/voices takes an apiKey, the four
/test endpoints take connection secrets, /local-providers/probe takes auth).

So the method alone can never carry the permission model, and the registry will
need an explicit per-route classification regardless. Converting these five is
worth it because it is free; converting the rest would be a breaking change
across 117 mobile call sites that buys nothing.

Web callers updated in the same commit; the sidecar contract comments now match.
Mobile has exactly one caller to change — transmissionPortTest in
packages/core/src/services/transmission.ts — and no shim was added, because an
endpoint answering to both methods is the problem this commit exists to fix.

docs/api-method-changes-2026-08-06.md is the handoff for the mobile team: what
changed, the one line to edit, what deliberately did NOT change and why, and how
to verify.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:38:49 +00:00
pastilhasandClaude Opus 5 0799ec7325 commit the mobile dav correspondence
the letter that asked for the provisioning endpoint and the reply that answers
it. both lived only in an untracked COMMS/ directory on the dev box, which is
where the reasoning behind an api contract goes to be lost.

the feedback comes with it: a reply that answers a letter nobody can read is
half a record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:30:25 +00:00
pastilhasandClaude Opus 5 3f57cec551 one-tap ios dav provisioning
mints a dav app password, renders a configuration profile carrying both the
caldav and carddav payloads, and parks it behind a single-use five-minute token
that safari can fetch without a session.

one profile with both payloads is not a convenience: ios keys accounts by
server+username, so adding carddav separately gets folded into the existing
caldav account and contacts silently never appear.

the profile holds the password in plaintext, so it is held in memory only —
persisting it would falsify createDavAppPassword's "not stored" guarantee.

signing is opt-in via DAV_PROFILE_SIGN_CERT/_KEY/_CHAIN and off by default;
this box has no tls certificate, tls terminates upstream. signed at mint time
reading the cert from disk, so a renewal needs no restart and no hook.

the download route is registered before the /dav mount because hono matches in
registration order and the sync door's /* would otherwise demand http basic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:20:52 +00:00
pastilhasandClaude Opus 5 d743d03fa2 add mobile DAV provisioning handoff doc
One-tap calendar/contacts setup for the iOS and Android apps: the server
contract that already exists, and specs for the two platform mechanisms
that don't yet — a signed .mobileconfig for iOS and a DAVx5 intent for
Android.

Payload keys, intent identifiers and install flows are researched against
Apple's device-management reference and davx5-ose source rather than
recalled; the doc marks what is verified against a running deployment and
what is still specification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:46:38 +00:00
pastilhasandClaude Opus 5 bc52ce6361 docs: phone photo backup contract, and the file-sync decision
photo sync turned out to be mostly built already. officer proxies immich through
officer-photos, and that sidecar's allow-list already permits the `assets`
resource for GET/POST/PUT/DELETE — so immich's own upload and bulk-upload-check
endpoints are already reachable with officer's auth in front and the immich key
never leaving the sidecar. the deliverable is therefore the contract, not a new
ingest service.

three gaps are written down rather than papered over:
  - immich is not currently connected in officer (_health says configured:false),
    so none of it could be verified live. the api key moved out of .env and was
    never re-entered in the ui. owner action.
  - upload bodies are buffered twice, once in createSidecarProxy and once in the
    photos sidecar. nothing fails at phone-photo sizes; a video library would be
    unpleasant. fixing it touches the shared factory, so it is a decision.
  - no resumable upload. immich's own app has the same limitation.

file sync is design-only, as agreed. the recommendation is syncthing supervised
as a sidecar rather than reimplementing nextcloud's sync protocol — a mount is
not a sync, and the local-first replica is the whole feature.

the iOS answer is stated plainly because it changes the design: continuous
background sync is not possible there, which is why nextcloud's own iOS app is
manual too. if iOS matters, webdav becomes first-class rather than optional, and
that is the owner's call to make before any code is written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 03:27:01 +00:00