Skip to content

Multi-account: one badge, every OneDrive account - #4

Merged
salemsayed merged 5 commits into
salemsayed:mainfrom
mikebenner:main
Sep 14, 2026
Merged

salemsayed merged 5 commits into
salemsayed:mainfrom
mikebenner:main

Conversation

@mikebenner

@mikebenner mikebenner commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Closes #3 — this implements the design proposed there: discovery from each unit's own ExecStart rather than a naming convention, worst-of-N aggregation on the badge, per-account actions behind a selector, and a single-account install kept byte-for-byte on today's behaviour.

OmaOneDrive becomes multi-account while keeping a one-account machine byte-for-byte on today's behaviour.

What it does

  • onedrive-status.py --list-accounts discovers every OneDrive systemd unit — the plain onedrive.service and any onedrive@<instance> template units — reading each unit's real --confdir out of its own ExecStart (allowlisted to LoadState=loaded with a non-empty ExecStart, so a masked/error/stub unit can never alias onto the default account's token and cache).
  • The widget polls accounts round-robin on one shared budget with a startup ramp, aggregates them worst-first into the existing badge vocabulary, and shows per-account tabs in the panel (hidden with one account).
  • Every action — pause, timed pause with its own omaonedrive-resume@<instance> timer, resume, reauth, resync repair, open folder, storage check — runs against the selected account's own service, config directory, and resume unit. Cloud checks are serialized fleet-wide (one 30s check at a time), deduplicated against both the queue and the check in flight.
  • One polling burst produces one notification; grouped popups name their accounts and the click opens the account it is about, via the account carried in the persisted --exec hint (built on 1.5.5/1.5.6's mechanism — new openAccount/repairAccount IPC functions receive it). Scripts get accounts and selectAccount IPC.
  • Hardening that fell out of review: every spawned process settles exactly once (exit, failure-to-start, or watchdog — real Quickshell never emits exited for an executable that cannot start), so a missing python3, a wedged helper, or a hung systemctl can no longer freeze polling or disable Pause; a pause holds its optimistic state until a poll started after the control confirms it; every helper reply is stamped with the confdir it actually read and mismatches are refused, so the startup poll can never attach the default account's data to another unit's name; the cache survives any malformed content.

Compatibility

  • With no template units, discovery returns exactly the plain service and the widget sends exactly the commands it sends today (pinned by test).
  • The legacy omaonedrive-resume timer name is kept for the plain service, so an in-flight pause survives the upgrade.
  • Upstream 1.5.5/1.5.6 are merged in full: durable exec-hint clicks, plain-text rendering at every inherited boundary (extended to the multi-account tooltip), your CI workflow, CHANGELOG discipline (1.6.0 entry included), and the reliable timeout regression test.

How it's verified

  • 125 node tests, plus a 300-check headless QML harness that drives the real Service.qml/Account.qml against stub Quickshell types (scheduler, cloud semaphore, discovery reconciliation, notification broker, watchdogs, per-account command vectors).
  • A contract check feeds real --list-accounts output through the real QML and executes every produced command against the real helper.
  • Mutation-hardened: every function in Service.qml/Account.qml fails the suite when emptied; every if condition there and in Model.js fails when inverted.
  • tests/run skips the Qt-dependent layers cleanly, so your CI (node + jq, no Qt) runs green.
  • Running in production on a 3-account machine (personal + two orgs) — installed via omarchy plugin add, exercised over IPC and by hand.

Full development history (5 adversarial review passes, ~40 findings, all fixed and pinned) is on the fork: mikebenner/omaonedrive.

🤖 Generated with Claude Code

mikebenner and others added 5 commits August 30, 2026 23:20
onedrive-status.py gains --confdir, to report status for one account by
config directory, and --list-accounts, to enumerate the accounts on the
machine. Each account's config directory is read out of its own systemd
unit's ExecStart argv[], never guessed from the instance name, so an
instance pointed at an unrelated directory still resolves correctly.

Enumeration is systemctl list-units plus a sweep of the enablement
symlinks, because list-units reports only loaded units and list-unit-files
never expands template instances. With no systemd at all it falls back to
the single default account.

Each account gets its own state directory, so the lock is per-account as
well as the cache, keyed on the service and the canonicalized config
directory. The default account's JSON output, cache path and lock path are
unchanged, so today's single-account behaviour is byte-identical.

The QML layer is unchanged and does not use the new flags yet; it is
redesigned separately.

Reviewed through the full gauntlet at Tier 2: /code-review high plus a
convergence pass, /security-review (no high or medium findings),
/simplify, three persona agents, and cross-family passes from codex and
grok. 34 confirmed findings fixed. See the reports on PR #1.

Known follow-ups, both flagged on the PR: --resume-unit is required by the
QML design and does not exist yet, and docs/ARCHITECTURE.md's read-surface
list is now stale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--resume-unit names the transient systemd resume unit to inspect, so
resumeAt can be correct per account instead of every account reporting the
one fixed timer. It defaults to the legacy omaonedrive-resume for the plain
service, so an in-flight timer survives an upgrade; without the flag every
other account reports no resume time rather than borrowing that one. The
value is validated as a systemd unit name, capped at 255 bytes, and a
trailing .timer is normalised rather than refused.

Neither a service nor a resume unit name may begin with '-'. Both are passed
to systemctl as positional arguments, so such a value was read as its
--machine or --host option, the latter making systemctl attempt an outbound
connection. Confirmed against the real binary; the service gate had the same
hole beforehand. Both gates now share one character class and one length
rule, and --list-accounts no longer returns before validation runs.

A config directory whose own name contains ' - ' is now rejoined correctly
rather than resolving to a shorter path that happened to exist, which was
another account's directory.

docs/ARCHITECTURE.md states the helper's read surface as a closed list and
rests its privacy claim on that completeness. The list was missing the config
file the helper parses directly, the onedrive --version call, the
OMAONEDRIVE_UNIT_ROOTS override that replaces the whole unit-directory list,
and the account's own status-cache.json. Seven further claims across that file
and TESTING.md overstated what the code guarantees; all are corrected.

Reviewed over three rounds: /code-review xhigh, /security-review twice (no
high or medium findings), /simplify, a docs-accuracy pass, and two rounds of
cross-family review from codex and grok, the second re-reviewing the first
round's fixes. 27 findings fixed. Seven further confirmed findings against
already-merged code are listed on the PR as follow-ups.

The no-flag JSON output remains byte-identical to the pre-multi-account
version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar becomes multi-account: the helper discovers every onedrive systemd
unit (plain and template instances), and the widget polls them round-robin,
aggregates them worst-first into one badge, and drives every action -- pause,
resume, timed pause, reauth, resync repair, folder, storage -- against the
selected account's own service, config directory and resume timer. One
account keeps exactly the single-account behaviour, commands and all.

Coordination: one status poll at a time across the fleet, with a startup ramp
that gives unpolled accounts priority without letting a broken one
monopolise; one cloud check at a time, deduplicated against both the queue
and the check in flight; desktop notifications coalesced per polling burst,
grouped popups opening the worst account. IPC gains accounts() and
selectAccount(); every gesture and notification click selects the account it
is about before acting. Worst-first is the user's stated rule for the badge,
pause included: every account must be working before the bar looks normal.

Hardening that fell out of five adversarial review passes: every process
settles exactly once per invocation -- on exit, on failure to start (real
Quickshell never emits exited for those), or by watchdog -- so a missing
python3, a wedged helper or a hung systemctl cannot freeze the fleet or kill
Pause for the session. A pause holds its optimistic state until a poll
STARTED after the control confirms it. Every helper reply is stamped with the
config directory it actually read, so the startup seed poll can never attach
the default account's data to another unit's name. The helper allowlists
LoadState=loaded with a real ExecStart, and survives any malformed cache.

Verification: 124 node tests, 263 harness checks driving the real
coordinator headless, a QML-to-real-helper contract check, helper suites,
and a qmllint gate for the two files nothing else parses. Every function in
Service.qml and Account.qml fails the suite when emptied; every condition in
Model.js fails when inverted. Full finding ledger on the branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErMhXzfZ86BcJuhhgb242
…miss (#3 follow-up)

The invert-every-if sweep over Service.qml and Account.qml finally ran to
completion. Eleven of 117 conditions survived; none were dead code, and all
are now pinned by harness checks: the settings NaN-guard in both directions,
error precedence on the facade, the discovery failure-to-start hook firing
only on failure to start, the timed pause on an already-stopped account, the
settle loop's persistence under a blocked slot, the hung (not failed)
systemd-run watchdog and its final recovery message, all three cloud-check
completion messages plus the routine-poll silence gate, and the account-level
deferred cloud request.

Test-only: 299 harness checks (was 263), no runtime file changed. Final sweep
state -- zero of 117 conditions in the QML and zero of 97 in Model.js invert
undetected; all 65 QML functions fail the suite when emptied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErMhXzfZ86BcJuhhgb242
Brings salemsayed/omaonedrive releases 1.5.5 and 1.5.6 into the fork:
durable notification clicks via omarchy-notification-send's persisted --exec
hint (extended so the click carries its ACCOUNT as an argv element, received
by the new openAccount/repairAccount IPC functions), plain-text rendering at
every inherited boundary including the multi-account tooltip, the CI
workflow, the CHANGELOG (with this fork's 1.6.0 entry on top), the 4.0.1 VM
test report, and the reliable partial-output timeout regression test.

The merge was assembled by hand-porting each upstream hunk into the
multi-account structure and verified by the full suite (125 node tests, 300
harness checks, contract check), eight targeted mutations of the ported
mechanism, and an adversarial four-auditor review of the port against
upstream's own diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErMhXzfZ86BcJuhhgb242
@salemsayed salemsayed added the enhancement New feature or request label Sep 14, 2026
@salemsayed
salemsayed merged commit 2f987bd into salemsayed:main Sep 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-account support (onedrive@<account>.service instances)

2 participants