Commit Graph
78 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 f5f509a99d scripts/setup is the initial install, nothing else
Two of the eight did not belong. cleanup-desktop.sh is the teardown — the inverse of an install, not part
of one. provision-user-dirs.ts runs per account at invite time, on a machine that is already set up.
Both are back at the top level, with their `../` derivations and usage strings put back.

What is left is what a fresh machine runs once: the two installers (setup.sh, setup_mac_light.sh), the
two things setup.sh calls (setup-dockers.sh, setup-desktop.sh), and the two files they deploy —
starship.toml, copied to ~/.config, and officer-set-display.sh, which setup-desktop.sh installs to
~/.local/bin as a login-time mode setter. The last one is not a setup script and does not read like one;
it is here because it is install payload, same as the toml, and setup-desktop.sh loads it by
`$(dirname $0)`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 04:05:06 +00:00
pastilhasandClaude Opus 5 9c353f5f0d move host setup into scripts/setup/
scripts/ was holding two unrelated kinds of thing: install-this-machine, and run-this-occasionally. The
eight installers now live in scripts/setup/; what stays at the top level is the build steps (gen-index,
prebuild, build/) and the two maintenance scripts (reindex-music, rebuild-soulseek-tree).

The move is not just a rename. Three of these derive the repo root from their own location:

  setup.sh:51            PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
  setup_mac_light.sh:51  same
  cleanup-desktop.sh:134 ENV_FILE="$(dirname "$0")/../.env"

Left alone, all three would now resolve to scripts/ — and nothing downstream complains. PROJECT_DIR is
where .env is written, where `bun install`, `gen:index` and `db:push` run, and what pm2 is pointed at, so
a fresh install would have quietly provisioned scripts/ and reported success. cleanup-desktop.sh fails
the other way: it would find no .env, print "No .env — skipping", and leave the real VNC_PASSWORD in the
real file. All three are now `../..` with a comment saying why the level matters.

provision-user-dirs.ts imports data-path.ts relatively; that one tsgo caught.

Also disambiguated `setup.sh` where it had become two files. app-store/templates/<name>/setup.sh is a
per-sidecar installer with its own contract, and preflight.ts + docs/sidecar-app-store.md discussed both
in the same paragraph. The host one is now spelled with its full path at those sites.

Verified: bash -n on all six shell scripts, tsgo clean, os-user tests pass, both derivations resolve to
the repo root, starship.toml still resolves from os-user-shell.ts, and provision-user-dirs.ts runs under
DRY_RUN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 04:02:19 +00:00
pastilhasandClaude Opus 5 37adc65a12 delete the spent one-shot scripts
Eight scripts in scripts/ that nothing references and that mostly can no longer run. Kept in history;
none of them is recoverable knowledge that isn't already in the code they migrated to.

Three could not run at all against the current database:

  migrate-items-to-files.ts   SELECT * FROM tasks — that table was dropped when items became files
  reset-user-data.ts          deletes chat_sessions, chat_groups, projects; none exists. It has no
                              transaction, so it would wipe user_settings, user_state,
                              user_integrations and dock_configs and THEN throw. A half-wiped account
                              is worse than no script. It also misses chat_session_events, which is
                              where chat state actually lives now.
  add-email-dock-user2.ts     one-time, hardcoded to user 2, seeds a dock containing /projects

The rest are spent migrations whose destination is now the only implementation:

  migrate-auth-to-pg.ts             JSON -> Postgres, 2026-02
  migrate-pg-to-files.ts            Postgres -> JSON, the other leg of the same abandoned round trip
  migrate-server-settings-to-pg.ts  2026-02
  migrate-emails-to-sqlite.ts       backfill into the email sidecar's store, 2026-07-31
  seed-imap-uids.ts                 the sidecar writes imap_lastuid/imap_uidvalidity itself now
                                    (sidecar/email/gmail-api.ts:533-535)

Kept, and why, since "unreferenced" was not the test: rebuild-soulseek-tree.ts is reusable by
construction — it runs the same buildTree the sidecar's ingest runs, so it answers any future change
in tree shape. reindex-music.ts is named in sidecar/music/index.ts:447. provision-user-dirs.ts shares
USER_DIRS with data-path.ts. cleanup-desktop.sh and officer-set-display.sh are called by
setup-desktop.sh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 03:58:13 +00:00
pastilhasandClaude Opus 5 ef000aaf51 pipe the installer into bash, and stop inventing a root-owned .local
Green's first provision failed three ways. host caught all three on the live
box; two are fixed here and the third is his to bisect.

THE INSTALLER IS BASH AND WE PIPED IT INTO SH. A script read on stdin never has
its shebang honoured — the interpreter you name is the one that runs it — and
install.sh declares #!/bin/bash and uses [[ =~ ]] on line 9. On Ubuntu /bin/sh
is dash, so it died with `Syntax error: "(" unexpected`, which reads like a
corrupt download rather than the wrong interpreter. scripts/setup.sh carried the
same line for the owner's own install and is fixed too.

INSTALL -D CREATED ~/.local AS ROOT. `install -d` makes missing parents but
applies -o/-g/-m only to the final component, so blessing ~/.local/dockers
invented a root:root .local inside the member's own home. Rootless Docker then
died on `mkdir …/.local/share: permission denied`, and the Claude installer
targets ~/.local/bin, so fixing the shell alone would have hit this next.

That is 71589ae for the second time — same function shape, same silent parent,
same class of consequence. Its own commit message said this surfaces "weeks
later as one tool mysteriously failing"; it took twenty minutes. Grepped the
other install -d/-D sites: os-user-shell already creates its parent explicitly,
os-user-ssh has no implicit parent.

NOT fixed: the file browser's ACL mask on a member home, where access mask is
--- while default:mask is rwx. That pattern means a chmod ran after the setfacl
and clamped only the access side, so the primitive is right and something later
is wrong. host has the live filesystem and has already half-excluded the
suspect; guessing from here would churn a working block. Noted that this commit
adds an install -d before the one he was about to bisect, so it wants a
reprovision first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 23:46:59 +00:00
pastilhasandClaude Opus 5 3bea46f2d7 rootless docker per member — provisioning works, running a container does not yet
Not finished. Committed because the diagnosis is worth more than the code.

WHY ROOTLESS AND NOT THE DOCKER GROUP. `usermod -aG docker <user>` is the one-line version
and it is root: `docker run -v /:/host -it alpine chroot /host` is a root shell, which reads
.env, every other member's home and the wallet seed. Every boundary from today, bypassed by
one documented command. Rootless gives what was actually asked for — a daemon per account,
containers in that account's user namespace, images in their own home.

VERIFIED on this host: provisioning succeeds, the server reports 29.5.0, the daemon runs as
the member, `docker pull` puts 403 MB under their own home, and `docker ps -a` shows nothing
while the owner has four containers. That last line is the isolation, measured.

NOT VERIFIED: actually running a container. It failed, and the cause is an interaction
between two things built today:

  failed to copy xattrs: failed to set xattr "system.posix_acl_default" on …/volumes/…/_data

Creating a volume copies xattrs, and the DEFAULT ACLs on a member's home — added so the file
browser could read their files — are inherited by Docker's storage, where a mapped id inside
a user namespace is not a valid id to set. Both features correct alone. The fix here strips
default ACLs from ~/.local/share/docker only, leaving the access ACLs the file browser needs.

That fix is UNPROVEN. The re-test failed for a different, environmental reason: probe users
recycle uid 1001, and a stale lingering systemd user manager from a previous probe answered
`systemctl --user`, so the unit appeared not to exist. Cleaned with `loginctl terminate-user`.
Retest on a machine that has not had a uid-1001 user, or on a fresh uid.

Also worth knowing before this ships: uid reuse after deleting a member is a real hazard, not
just a test artefact — the next member gets the previous member's uid, and anything left
lingering belongs to them.

setup.sh gains uidmap and dbus-user-session as core packages; the shell template exports
DOCKER_HOST from $XDG_RUNTIME_DIR when the socket exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 20:01:17 +00:00
pastilhasandClaude Opus 5 71589aee99 a member's terminal looks like the owner's
A new Linux account opens a shell with nothing: useradd copies /etc/skel, which on Ubuntu
is a bash rc, and the account's shell is zsh — so it got no prompt, no history, no
completion, no colour. "Their own account" should not mean a worse terminal than the
owner's.

src/servers/shell-skel/zshrc is the template, and scripts/starship.toml is reused rather
than copied: setup.sh already deploys it for the owner, so one file serves both audiences
and they cannot drift. Seeded by provisionOsAccount, which means the retry button applies
it to accounts that already exist — no delete-and-recreate.

The template depends on nothing but zsh. Starship, eza, nvim, bun, deno and cargo are each
used only if present, and every path is $HOME-relative — the owner's own .zshrc has three
absolute /home/pastilhas paths in it, which is exactly what a template must not inherit.
Without starship it falls back to a zsh prompt showing the same information, because a
shell that opens with a broken prompt reads as a broken machine.

Never overwrites: written only when the file is ABSENT. ~/.zshrc.local is sourced last and
never written, so there is somewhere to put your own config that no future template can
reach.

Three fixes found by running it:

- install -D creates missing parents but applies -o/-g only to the FILE, so ~/.config came
  out root:root — readable but not writable by its owner, which would have surfaced weeks
  later as one tool mysteriously failing. The parent is now created explicitly.
- useradd took its shell from process.env.SHELL, which under PM2 is whatever PM2 was
  launched from. A member's shell depended on how the server happened to be started. Now
  chosen from what is installed: zsh, else bash.
- the pty sidecar spawned ITS $SHELL for a member, not theirs. It now execs their passwd
  shell via sh -c, so the login shell in /etc/passwd is the one they get.

starship moves out of the light-profile skip. The light profile exists to serve a file
browser, a terminal and chat — the terminal is one of its three reasons to be, and it is
what every member gets. Leaving starship out meant the fallback prompt on exactly the
installs most likely to have members. oh-my-zsh, eza and lazygit stay full-only.

Verified in a real member shell: zsh from passwd, HISTFILE in their own home, eza-backed
ll, starship active, EDITOR=nvim, and an edit to .zshrc surviving a reprovision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:43:55 +00:00
pastilhasandClaude Opus 5 d3bed0add9 the file browser can actually read a member's home, and plans is gone
"This folder is empty" was a lie. The five seeded directories were sitting there and the
platform's readdir raised EACCES: a member's home is 700 and owned by them, which is
correct for a shell and locks out the file browser, which runs inside the platform
process. /ls caught the error and returned an empty listing, so a refusal looked exactly
like data.

Two doors, two boundaries, and that is the point rather than a compromise. The terminal
and the agent RUN AS the member and the kernel is the boundary there. The file browser
acts on the member's behalf from inside the platform, which already applies its own
containment and is the owner's process on the owner's machine — it can read anything via
sudo regardless. Giving it access describes who is doing the work.

Done with named POSIX ACLs, because it has to hold in BOTH directions: a file the
platform writes must be editable by the member and vice versa. Mode bits cannot say that
— whichever party is neither owner nor group lands in "other", and widening "other"
opens the home to every account on the box. A shared group fails the same way, since both
parties would have to be in it and that puts every member in a group that can read every
other member's home. Two named entries plus `d:` defaults grant exactly two users and are
inherited by whatever either side creates, whatever their umask.

Verified: platform lists the home, member edits a platform-written file, platform edits a
member-written file, and a SECOND member is refused on both ls and cat.

/ls now distinguishes EACCES from a missing directory. An empty result is data and must
never be how a refusal looks.

acl joins the core packages in setup.sh — the alternative is an account that provisions
and then cannot list its own home.

Also: the file browser's own useTasks/useAgents fired /tasks, /agents and both category
endpoints on every render, which is where the last four 403s came from — they are the
context menu's Run Task and agent submenus, execution-only. Gated.

And plans is deleted: router, screen, routes, dock tile, hook, page title and its
capability. It read markdown from <repo>/plans, which does not exist. Fresh-install
Permissions is now Files alone, with Terminal to come.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:29:36 +00:00
pastilhasandClaude Opus 5 69a31051ac the owner can create accounts
POST /api/users plus an Add-account form in Settings > User management. Until now
createUser had one call site — bootstrap, gated on an empty user table — so every
non-owner account anywhere had been inserted into Postgres by hand.

Created accounts are Active. The column defaults to Unverified and signin refuses
anything else with a bare UNAUTHORIZED, which is exactly what made the hand-INSERT
route look like a wrong password.

Also closes a hole found while reading the write path: a second Super Admin was
storable. The CHECK constraint pins user 1's role but cannot see other rows, and
getOwnerUser() was LIMIT 1 with no ORDER BY, so two holders would have made "who owns
this server" a question the query plan answered — and that answer feeds the agent
sidecar's identity, vault access and origin scoping. Both write paths now refuse the
role and getOwnerUser() orders by id.

USER_DIRS and provisionUserDirs move into data-path.ts so the create handler and
scripts/provision-user-dirs.ts cannot disagree about what an account's skeleton is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:15:09 +00:00
pastilhasandClaude Opus 5 7b16e88b2f scripts: give every user a root under DATA_PATH
The three non-owner accounts had no directory of their own — only the owner did,
grown organically as features created what they needed. Adds an idempotent script
that creates the skeleton, and runs it for those three.

  DATA_PATH/<email>/{home,attachments,cache,dashboards,email_accounts,
                     general_chat_sessions,logs,sidecar}

Most of those are also created on demand by whichever feature owns them, so
pre-creating buys legibility rather than function: the tree now shows the shape a
user has without having to use it first. `home` is the exception and the reason
this exists — nothing creates it today, because getOwnerHomeDir returns
process.env.HOME_DIR whenever it is set, which it always is on a real install. It
is where a non-owner's sessions will run.

Takes emails as arguments rather than reading the user table. The layout does not
depend on the database, keeping the DB out means it runs with nothing else up,
and at invite time the caller already knows the email. It refuses arguments that
are not email addresses, since a stray one would create a junk directory sitting
next to real user roots and looking like one.

Deliberately NOT a revival of the provision.ts deleted two commits ago. That one
was ~90% per-container scaffolding — shell rc files, a generated CLAUDE.md and
settings.json describing the container — wrapped around the two useful lines this
keeps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:00:33 +00:00
pastilhasandClaude Opus 5 e661e3738f scripts: make the light installs localhost-native, and stop relaxing guards for it
A light install is reached at localhost on the machine running it, so PUBLIC_URL
has exactly one right answer. setup.sh required it with a re-prompt loop; under
the light profile it now defaults to http://localhost:$PORT. The macOS installer
already did this — this is parity, and it removes the one prompt in a light run
whose answer a non-technical user could not be expected to produce.

setup_mac_light.sh also wrote PUBLIC_BUILD_ENV="development", justified in a
comment as "what makes plain http://localhost work". That is no longer true, and
the cost of it is not small. IS_DEV_BUILD gates exactly three things:

  origin validation   already off regardless — ALLOW_ANY_ORIGIN defaults to true
  password rules      validatePassword is skipped entirely on change-password
  rate limiting       the limiter returns next() before doing anything

So the only live effects were losing the last two, for a benefit that another
default already provided. It now writes "production", matching setup.sh. Nothing
about localhost needed relaxing: browsers treat http://localhost as a secure
context, so passkeys, getUserMedia and the clipboard all work over plain HTTP,
and passkeys in particular derive their RP ID from the request origin rather
than a configured domain.

That last point is the boundary worth knowing: http://192.168.x.x is NOT a
secure context, so reaching a light install from another device means putting
an HTTPS proxy in front of it. Recorded in the comments at both prompts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 13:01:54 +00:00
pastilhasandClaude Opus 5 0a1766768d mac: name the laptop build a profile, and derive it like the others
Renamed for parity now that Linux has a light profile too:
  scripts/setup_mac.sh       -> scripts/setup_mac_light.sh
  ecosystem.mac.config.cjs   -> ecosystem.mac.light.config.cjs

The macOS process list was still a hand-copied subset, which is the shape that
broke it: written 2026-07-28, within days it was running the Anthropic proxy
under the name officer-claude with nothing spawning `claude`, and pointing at a
pty entry point that had moved. Both silent. It now declares names and reasons
and reads script/args from ecosystem.config.cjs, so a launch change on the host
reaches it for free.

The include/exclude checks moved into ecosystem.profile.cjs rather than being
copied into the second profile — duplicating the guard rails would have repeated
the mistake they exist to catch. Both profiles were re-tested against a mutated
host ecosystem: renaming an included app and adding an unclassified sidecar each
throw in both, and an unmodified host loads five apps in both.

Kept as two files rather than collapsed into one, even though they currently
produce identical output. The exclusions do not mean the same thing: on macOS
officer-vnc CANNOT run, there being no Xorg; on a Linux light install it could
run fine and you have chosen not to. Merging them would lose that, and they
diverge the moment one profile gains something the other cannot have.

setup_mac_light.sh's verification loop now reads app names with node instead of
grepping for `name:` — the derived profile has no literal keys, so the grep
would have silently listed no services at all, which reads the same as a healthy
install with nothing configured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:47:27 +00:00
pastilhasandClaude Opus 5 1f4dbbb810 scripts: add a light install profile
OFFICER_PROFILE=light installs the same thing the macOS build does, on Linux:
the file browser, the terminal, and Claude/opencode chat. It skips the archive
extras, the sudoers entry and auto-suspend disabling, Go, Rust, PulseAudio,
cliamp, Neovim, the shell tooling and yt-dlp, brings up Postgres alone of the
five Docker services, and starts ecosystem.light.config.cjs. Unset or `full`
behaves exactly as before.

The profile changes which processes start, not which code ships — every API
route stays mounted, so features whose sidecars are absent report themselves
unavailable rather than disappearing.

ecosystem.light.config.cjs DERIVES its apps from ecosystem.config.cjs rather
than copying them, because the hand-copied Mac list was broken within days of
being written by a sidecar split in two and a pty entry point that moved, and
both failures were silent. Here a script/args change on the host propagates for
free, and two consistency checks turn the silent cases loud:

- a name the profile needs that the host no longer defines throws at load
- an app added to the host that is in neither the include list nor the annotated
  exclusion list throws, so a new sidecar cannot default to "not in the profile"
  without someone deciding

Both were tested against a mutated copy of the host ecosystem: renaming
officer-agent and adding an unclassified sidecar each throw, and the unmodified
file loads five apps.

The verification block now reads app names with node instead of grepping for
`name:` — the derived file has no literal keys to match, so a grep would have
silently verified nothing — and skips the checks for tools the profile did not
install, so a clean light run does not report Go and cliamp as missing.

Full-profile behaviour is unchanged by construction: every guard wraps the
original code in an else branch. The preamble was tested across unset, full,
light and an invalid value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:41:47 +00:00
pastilhasandClaude Opus 5 37ff7c3103 mac: bring the laptop ecosystem back in step with the architecture
ecosystem.mac.config.cjs was written on 2026-07-28 and broken within days by two
changes it never caught up with, leaving two of its four processes dead:

- it ran `officer-claude` against src/servers/sidecar/claude/index.ts. That file
  is the Anthropic credential proxy; the process that actually spawns `claude`
  is user-instance.ts, which was never started. Chat had a credential holder and
  nothing driving it. Split into officer-anthropic-proxy + officer-agent to
  match the host, and retired the `officer-claude` name that caused it.
- officer-pty pointed at src/servers/api/terminal/pty-sidecar.mjs, which no
  longer exists; the sidecar moved to src/servers/sidecar/pty/index.mjs. The
  terminal could not start at all.

No startup ordering is needed between the proxy and the agent: the agent reads
the proxy secret from disk and, when it is not there yet, warns and re-reads
before the next spawn.

Five sidecars have been added since the file was written (memos, caldav, photos,
notify, wallet). All are excluded, and every exclusion is now listed with its
reason so the next reader can tell "not applicable on macOS" from "forgotten" —
which is the failure this commit is fixing.

setup_mac.sh needed no path corrections; its verification loop derives the
process names from the ecosystem file, so it picks the new list up on its own.
Two gaps closed there:

- it called `pm2 save` without `pm2 startup`, writing a process list that
  nothing reads — a reboot left the machine with nothing running. Added the
  launchd equivalent behind SETUP_BOOT, optional because a laptop is not a
  server, and non-fatal because pm2's launchd integration can want a prompt.
- the closing "not installed on macOS" note listed four omissions when there are
  now fourteen.

Both files parse and every ecosystem entry point resolves to a file that exists.
The launchd path is the one thing not verified here — it cannot be exercised
from the Linux host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:31:06 +00:00
pastilhasandClaude Opus 5 0e893cc292 scripts: stop creating the /usr/local/bin/claude symlink
The symlink existed because the Claude sidecar hardcoded that path, and that
hardcoding came from the bwrap-sandboxed architecture: the jail ro-bound /usr
and could not see the installer's real target in ~/.local/bin. The sandbox is
gone, and claude-manager.ts now resolves the CLI itself — $CLAUDE_BIN, then
PATH, then ~/.local/bin/claude, /usr/local/bin/claude, /opt/homebrew/bin/claude.

Verified before removing rather than assumed:

- the only references left in the tree are the resolver's own fallback list and
  this step; nothing in capabilities, no systemd unit, no crontab, no ecosystem
  file and no shell rc mentions the path
- the agent sidecar's PATH under pm2 contains ~/.local/bin ahead of
  /usr/local/bin, so Bun.which resolves to the installer's target and the
  symlink is never consulted
- replaying the resolver in that exact environment with the symlink treated as
  absent returns the same path, so it is not load-bearing
- resolveClaudeBin runs at claude-manager module scope, which ES import ordering
  puts before user-instance.ts reassigns process.env.HOME — so the homedir()
  candidate is evaluated against the real home, not the managed one

The install-and-verify step above is untouched, so a failed claude-code install
is still reported. Only the sudo-owned link into /usr/local/bin goes, a
directory macOS does not ship at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 12:22:16 +00:00
pastilhasandClaude Opus 5 6b50ad978e merge the macos branch: setup script, mac pm2 ecosystem, claude binary resolution
Three files, all additive — nothing on master is modified by this beyond the
CLAUDE_BIN change below, and no file is deleted. The branch predates master by
about 180 commits, but it touches nothing master has touched since, so the
merge is clean.

The part that matters beyond macOS is claude-manager.ts. CLAUDE_BIN was pinned
to /usr/local/bin/claude, which dated from the bwrap-sandboxed architecture:
the jail ro-bound /usr and saw nothing else, so the installer's real target
(~/.local/bin/claude) had to be symlinked somewhere the sandbox could reach.
That sandbox is gone, and the hardcoded path left the sidecar unrunnable on any
host without it. It now resolves an explicit CLAUDE_BIN pin, then PATH, then the
locations Anthropic's installer actually writes to — mirroring how OPENCODE_BIN
is already resolved in the opencode sidecar.

ecosystem.mac.config.cjs is deliberately a trimmed set of processes rather than
a mac port of the full ecosystem. It is also stale in two specific ways, left
as-is here and worth fixing separately: it names officer-claude, which master
renamed to officer-anthropic-proxy, and its officer-pty runs
src/servers/api/terminal/pty-sidecar.mjs, which moved to
src/servers/sidecar/pty/index.mjs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 13:01:26 +01:00
pastilhasandClaude Opus 5 a8335bfdd6 scripts: validate the sudoers entry before installing it
The setup wrote /etc/sudoers.d/officer-service with tee and then chmod'd it.
Two problems, both with the same worst case: a malformed or wrongly-permissioned
file there breaks sudo completely, and you cannot sudo to repair it — on a
remote machine that means physical access or a rescue boot.

Generate into a temp file, gate on `visudo -c`, and only then install. Use
install(1) rather than tee+chmod so the content and the 0440 mode land in one
step; tee creates at the default umask first, and sudo refuses to read a sudoers
file with loose permissions, so the old ordering had a window where sudo could
reject its own configuration.

The re-run guard also grepped for the username anywhere in the file, so a
comment mentioning it counted as configured. Match the actual rule instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:34:33 +00:00
pastilhasandClaude Opus 5 593bcc1575 scripts: make setup.sh converge instead of trusting proxies
Two guards that checked something other than the state they were protecting.

Section 17 skipped the entire remote desktop setup when `dpkg -s ubuntu-desktop`
succeeded, treating one package being present as proof that seven steps of
configuration had run. A host can have ubuntu-desktop and still be missing GDM
auto-login, the forced Xorg session, the captured EDID and its kernel command
line, and the login-time mode setter — which is exactly what this machine was
on 2026-08-02, while the guard cheerfully reported "skip". setup-desktop.sh is
idempotent throughout, so the guard bought nothing and cost a converged host.

The starship step had the opposite bug: it cp'd over ~/.config/starship.toml on
every run, so a customised config was silently destroyed. The nvim step two
sections down already guards on its config's existence; this now matches, and
distinguishes "absent" (deploy) from "identical" (skip) from "yours differs"
(keep, and say how to take ours).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:32:14 +00:00
pastilhasandClaude Opus 5 16f5175205 scripts: rewrite the desktop teardown for the mirror-based setup
cleanup-desktop.sh still described the era when Officer installed a desktop of
its own — XFCE on a TigerVNC Xvnc — so tearing the remote desktop down meant
deleting a desktop nobody else used. That stopped being true when the setup
moved to mirroring the machine's existing session with x11vnc, and the script
was left actively dangerous: it purged dbus-x11 and Brave, deleted ~/.vnc, and
reinstalled gnome-keyring, all of which the current GNOME setup depends on or
deliberately removes. Running it today broke the desktop rather than cleaning
it up.

Rewritten as the actual inverse of setup-desktop.sh:

- removes what Officer added — x11vnc, ~/.vnc, the GDM auto-login and forced
  Xorg keys, the forced EDID and its kernel command line, the login-time mode
  setter, the legacy officer-vnc unit, the VNC entries in .env
- keeps ubuntu-desktop, gdm3 and dbus-x11, which are the machine's own desktop
  and not Officer's to delete
- still purges XFCE and TigerVNC when present, since a host set up by the older
  script carries them and they are precisely what this undoes
- Brave is opt-in behind --purge-brave: setup installs it, but by teardown time
  it is usually just the user's browser with their profile in it

The GRUB and GDM edits were checked against copies of the real files: stripping
the EDID parameters leaves other kernel arguments intact wherever they sit in
the line, and the GDM revert does not disturb the commented examples that ship
in custom.conf. The package matcher names each xfce-family prefix rather than
globbing '^libxf', which would have taken libxfixes, libxft and libxfont with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 11:02:16 +00:00
pastilhasandClaude Opus 5 67d6a9702a scripts: name the desktop setup for what it installs
Section 17 was still labelled "XFCE + VNC" while the step it runs installs
ubuntu-desktop and is guarded on it, so the heading described a setup the
script had already stopped producing.

Also spell out why setup-desktop.sh disables lightdm: it is not a display
manager this script ever installs, it is residue on hosts set up by an earlier
version that did install XFCE, and left enabled it beats GDM to the seat.

Comments and one echo string; no behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 00:32:27 +00:00
pastilhasandClaude Opus 5 a83f9cd378 delete the orphaned build script, and fix the second copy of the memo rule
scripts/build/runtime.ts had no caller after the build:editor scripts went. Its own usage
text gives away where it came from: --experiments, --tracking, --editor-setup, the same
phantom domain as the docs and examples cleaned up earlier. Nothing else references it —
the `./runtime` export in src/workspaces/types/package.json points at a different file,
which is untouched. helpers.ts stays; dashboard.ts and web.ts import it.

APP_CONVENTIONS.md carried its own copy of "React 19's compiler handles memoization. Never
use useCallback or useMemo." Correcting only CONVENTIONS.md would have left the two
contradicting each other, which is worse than either. Both now say the same thing and one
points at the other for the reasoning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 18:12:19 +00:00
pastilhasandClaude Opus 5 af56eb36ff email: move the mail store and every route into the sidecar
Email was the one sidecar built inside out. The platform held ~1,800 lines — the per-account
SQLite store, all 14 HTTP routes, account CRUD, resync, IMAP validation — while the 314-line
sidecar was a scheduler that reached BACK into the platform to do anything
(`import { performResync } from '../../api/email/resync'`).

The sidecar now serves its own HTTP listener and announces `email:server`, and
/api/email/* on the platform is createSidecarProxy like every other one: 1,801 lines down
to 22, with no mail knowledge left in it — not a message, not a folder, not a credential.

The routes moved verbatim, Hono and all. http.ts only reconstructs what the platform's
middleware used to provide: `user` on the context, from the X-Officer-User header the proxy
injects (trusted because this server binds loopback), and an error handler that turns
custom-errors into status codes.

The /email/events SSE stream went with them, which removes a whole round trip: the IDLE
watcher used to send `email:new` over the registration socket so the platform could push to
its SSE clients. Those clients are here now, so it calls broadcastEmailNew in-process and
`email:new` is gone from the wire protocol.

DELIBERATELY NOT DONE YET, and left backwards on purpose rather than half-moved:

- The two sync handlers (email-sync 381 lines, gmail-sync 712) still run in the platform's
  queue and now import the store from its new home — a platform → sidecar import, which is
  the wrong direction and is temporary. Moving them is option (A) from the plan: the sidecar
  schedules its own syncs, independent of the platform Jobs list.
- accounts.ts still imports queue/init to enqueue a sync and to report sync status, and
  index.ts still carries the queue-over-WS shim that inversion needs.
- The three channel handlers still open the mail store directly rather than asking over HTTP.

Two things worth knowing while testing: a from-scratch sync holds a proxied request open
well past the 60s idle default, hence timeoutSeconds on the proxy; and `gmail-sync` is
hardcoded in all three channel handlers even though the only account is provider=gmail with
auth_type=password, which routes to IMAP — so "sync emails" from a chat channel is
almost certainly already broken, and folds into the next stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:10:35 +00:00
pastilhasandClaude Opus 4.8 1159c0787d soulseek: render cached shares as a tree, not a path list
slskd's browse response is flat: every folder is a full backslash-delimited
path. Rendered as-is, a filter for "pogues" gave 26 rows that all began with
the same 31 characters, and the real hierarchy — which is the only way to tell
an artist folder from an album folder — was invisible.

The shape is now derived once, at ingest, in the sidecar: buildTree() links
each path to its parent, synthesizes any ancestor slskd omitted (measured:
exactly one missing across ~30k folders on two real peers, but a single gap
would strand a whole subtree), and rolls subtree file counts and sizes up
bottom-up. A parent's own files are usually just cover art, so the number
worth showing on a collapsed row is the subtree's.

Storing the shape rather than recomputing it is what lets the UI open one
level at a time. Levels are still paged, because fan-out is brutal — the
widest folder measured has 1,181 children.

Filtering keeps the tree instead of falling back to a list: the search route
returns matches plus every ancestor, and the UI renders that skeleton
pre-expanded, so you see where a hit lives. Matching runs against the whole
path, so a matched folder implies its descendants match too and a matched
subtree arrives complete. The match cap is reported in the payload and shown
in the UI rather than passed off as the whole answer.

The two existing snapshots were backfilled by scripts/rebuild-soulseek-tree.ts,
which runs the same buildTree + finishSoulseekBrowse the ingest path runs — no
second implementation to drift, and no peer contact needed. Kept for the next
time the tree shape changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 01:05:22 +00:00
pastilhasandClaude Opus 5 35142df5a7 add a macos setup script and a trimmed pm2 ecosystem
setup.sh targets an ubuntu server and is left untouched. this is the
laptop equivalent: file browser, claude/opencode chat, terminal. no go,
rust, cliamp, pulseaudio, neovim, shell dotfiles, vnc desktop, sudoers
grant or power management — 510 lines against 1033.

every step is optional, prompted, and presettable non-interactively with
SETUP_* variables, so it also works as a repair tool for one piece.

nothing calls sudo. node@22 goes on PATH with brew link --force, pm2 into
~/.local, and the claude cli no longer needs a /usr/local/bin symlink now
that the sidecar resolves it.

postgres is detected before anything is installed — a server already
listening on 5432 (docker) is used as-is.

notes on the differences from setup.sh:
- set -e is on, but every optional step is guarded, so a failure warns and
  the run continues to a summary instead of aborting mid-way.
- .env is written 0600 and the generated JWT_SECRET is length-checked
  before use, since jwt.ts throws on anything under 32 chars.
- PUBLIC_BUILD_ENV=development, which is what lets plain http://localhost
  work with no reverse proxy in front.
- xcode command line tools are not required to build: node-pty and argon2
  both ship darwin prebuilds. they still matter for git on a fresh mac.

ecosystem.mac.config.cjs drops officer-vnc (x11vnc needs xorg),
officer-email and officer-music, and pins cwd on every app so bun picks
up .env wherever pm2 is started from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 14:15:01 +01:00
pastilhasandClaude Opus 4.8 9bf9b708cb reindex-music: show discography count in the report
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 09:40:58 +00:00
brunorezioandClaude Opus 5 592cc72f85 setup-desktop: configure the headless display
x11vnc mirrors :0, but with no monitor attached the connector has no EDID and no
CRTC, so GNOME renders nothing and the remote desktop is black. Reproduced the
hard way on this box: the desktop only worked because the session had started
while a screen was plugged in, and survived exactly until the next restart.

The step captures a connected monitor's EDID, installs it as
drm.edid_firmware with video=<connector>:1920x1080e so the connector reports
permanently attached, and adds a login-time hook to raise the resolution — the
replayed EDID's *preferred* mode is the captured panel's native one, which can be
tiny, and GNOME picks preferred. monitors.xml is the documented override but its
monitor matching did not take.

The EDID can only be captured from a screen that is plugged in, so a headless run
skips with instructions rather than pretending to succeed. Re-running is safe:
GRUB is left alone once the argument is present, and the mode setter no-ops when
the mode is already right.

officer-set-display.sh discovers the output and the largest mode within a cap
rather than hardcoding either, since setup runs before X exists and cannot know
them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 06:02:10 +01:00
brunorezioandClaude Opus 5 02662fc780 setup.sh: actually start the services, and verify they are running
setup.sh installed pm2 but never ran anything with it, so a fresh install
finished with every dependency in place and nothing listening. That is not
cosmetic: /desktop returns 503 until officer-vnc is connected, and chat needs
officer-claude.

Adds a step that runs `pm2 startOrRestart ecosystem.config.cjs`, saves the
process list, and enables the boot unit when it is not already there. Using
startOrRestart rather than start means apps added to the ecosystem since the last
run get picked up — officer-music is in the ecosystem on this box but was never
running, for exactly that reason.

The verification block now reports which services are up, with the names read
from ecosystem.config.cjs so the list cannot drift as sidecars are added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 05:19:27 +01:00
brunorezioandClaude Opus 5 7556c9ed00 fix /desktop: break the VNC password deadlock, drop the vncpasswd dependency
The desktop page has never worked on a fresh install. Two faults, both fatal.

The password could never be created. DesktopView fetches /desktop/vnc-password
before opening the WebSocket, but ensureVncPassword ran only from startSession,
which only the WebSocket triggers — so the endpoint answered "not configured",
the UI stopped, and the socket that would have provisioned it was never opened.
A new vnc:ensure-password sidecar command provisions it directly; the endpoint
asks for it instead of returning 500.

The rfbauth file could never be written either. ensureVncPassword shelled out to
tigervnc's `vncpasswd -f`, which is not installed — and, contrary to the comment
in setup-desktop.sh, is not in tigervnc-common, which ships only tigervncconfig.
The failure was swallowed because only a zero exit wrote the file, so x11vnc got
-rfbauth pointing at nothing. x11vnc writes that format itself with -storepasswd,
so the dependency is gone and a failure now throws.

Verified on the box: the endpoint returns a password, .vnc/{passwd,password} are
written 0600, and the sidecar reports mirroring :0 on 5900 with x11vnc using the
generated rfbauth file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 05:15:46 +01:00
pastilhasandClaude Opus 4.8 6224f6be51 music: SSE reindex progress stream + reindex-music CLI
Adds a live progress channel for the library index:
- indexer.ts: progress subscribers (onIndexProgress) + throttled emit during the
  walk, and buildReport() for a final summary.
- sidecar: GET /reindex/stream (SSE) — triggers a build if idle (?trigger=0 to
  watch only), streams `progress` events, ends with a `done` event carrying the
  report; auto-proxied at /api/music/reindex/stream for the app. Sidecar also
  writes DATA_PATH/music/.server (its port) for local tooling.
- scripts/reindex-music.ts: CLI that reads the port file, follows the SSE, prints
  live progress + a final report. Run: bun scripts/reindex-music.ts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 03:18:41 +00:00
brunorezioandClaude Opus 5 1d0842cc01 setup.sh: generate index.gen.html and apply the schema
A fresh clone has neither: index.gen.html is gitignored and built from
PUBLIC_URL, and the database schema is applied with push rather than migrations.
Without both, setup finishes on a checkout that cannot serve a page or reach a
table.

Runs after .env is written, since both depend on it. Failures warn rather than
abort so the rest of the verification still reports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 01:49:17 +01:00
brunorezioandClaude Opus 5 52ddf7df0e generate index.html's absolute URLs from PUBLIC_URL
index.html hardcoded the deployment's domain in eight places, so every instance
had to carry its own edit of the file — the only thing separating the rezio
branch from master.

The tags genuinely need absolute URLs. OpenGraph is fetched standalone by
crawlers, and Bun's HTML bundler treats a root-relative href as an asset to
resolve on disk, failing the build with "Could not resolve: /favicon.ico" —
external URLs are the only form it passes through untouched.

Bun's HTML import offers no substitution hook, so scripts/gen-index.ts swaps
__PUBLIC_URL__ for the value in .env and writes index.gen.html, which the server
imports. index.html is the tracked template and is now identical on every
deployment; index.gen.html is gitignored. predev/prestart run the generator, and
it is idempotent so --watch does not loop.

Substituting also fixes the manifest: an absolute URL puts its fetch in CORS
mode, which failed whenever the hardcoded domain was not the serving origin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 01:49:17 +01:00
brunorezioandClaude Opus 5 044aacf4d5 remove the dead multi-user surface
Officer is single-user: the server owner is the only account, created once by
/auth/bootstrap. Everything that existed to serve additional users was
unreachable, so it is gone rather than left looking like it does something.

Accounts: drop the invite / resend-invite / delete / list-users routes and the
Users settings screen, the inert /auth/signup handler, and the account
verification chain it fed (verify, resend-verification, VerifyScreen, the
UserInvite + VerifyAdmin + VerifyRegistration templates). /auth/verify-token
survives for password resets only, and now requires a reset-password token
rather than accepting any signed JWT.

Roles: drop the users.role column and the four-value USER_ROLES enum. The
permissions table granted every role identical methods, and every
role === 'Super Admin' check was permanently true. The JWT no longer carries a
role claim.

Sandbox: remove sidecar/sandbox.ts and its five call sites. bwrap was selected
only for non-Super-Admin users, so it never ran. It was also not a usable agent
jail as written — --share-net, the project root (with .env) bound read-only,
and runuser dropping to the server's own uid. Rebuilding it for agent
containment would be a different construction, and git history keeps this one.

getHomeDir keeps its DATA_PATH meaning; the new getOwnerHomeDir resolves the
owner's real login home, which is what terminals, chats and task runs use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:30:20 +01:00
pastilhasandClaude Opus 4.8 92de996412 setup.sh: symlink claude into /usr/local/bin
The Claude sidecar execs /usr/local/bin/claude (claude-manager.ts), but the
Anthropic installer only puts the CLI in ~/.local/bin — so on a fresh host that
path doesn't exist and claude chat fails with
"ENOENT … posix_spawn '/usr/local/bin/claude'". Symlink ~/.local/bin/claude →
/usr/local/bin/claude after install (idempotent; tracks Claude's self-updates).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:22:26 +00:00
pastilhasandClaude Opus 4.8 f226542de3 setup.sh: prompt for OFFICER_ITEMS_DIR in .env generation
The item store location wasn't written to .env, so a fresh server fell back to
<repo>/officer-items and booted with an empty store. Prompt for it (default: a
sibling of the repo) and write it to .env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:45:59 +00:00
pastilhasandClaude Opus 4.8 f7b8cbe3f3 setup-desktop: install tigervnc-common for vncpasswd
The VNC sidecar builds its .vnc/passwd rfbauth file with `vncpasswd -f`
(vnc-manager.ts) — x11vnc alone doesn't ship vncpasswd. The GNOME-on-Xorg
rewrite dropped tigervnc, so the mirror couldn't create its password and
/desktop failed with "VNC password not configured". Add tigervnc-common back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:26:31 +00:00
pastilhasandClaude Opus 4.8 1e789b4c44 setup: Ubuntu GNOME-on-Xorg desktop + setup.sh hardening
setup-desktop.sh now installs ubuntu-desktop + gdm3 + x11vnc and forces the
Xorg session (WaylandEnable=false) with auto-login — x11vnc can only mirror an
Xorg :0, not Wayland. vnc-manager.ts resolves the X authority from the GDM
per-session path (/run/user/<uid>/gdm/Xauthority) with a ~/.Xauthority fallback.

setup.sh fixes:
- desktop step gates on `dpkg -s ubuntu-desktop` (was the decommissioned
  officer-vnc service, which never matched so setup-desktop re-ran every time)
- remove Pi (install, --list-models validation, verification check)
- export GOPATH before the cliamp build so `go install` lands where it's checked
  even when Go was already present this run
- write PUBLIC_BUILD_ENV=production and quote all .env values
- guard the interactive .env block behind a TTY check so non-interactive runs
  skip cleanly instead of aborting on read EOF under set -e
- restart systemd-logind only when a key actually changed
- sed prefix-strip instead of `tr -d` (which deletes characters, not a prefix)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:32:28 +00:00
pastilhasandClaude Opus 4.8 33a0bb4578 email: store emails.db per account under email_accounts/<account>/
Reorganizes email storage: the DB moves from DATA_PATH/<user>/emails.db to
DATA_PATH/<user>/email_accounts/<accountEmail>/emails.db, with a shared
email_accounts/attachment_cache/ (was Gmail/emails/attachments). openEmailDb now
takes (owner, account); a new openUserEmailDb(owner, userId) resolves the user's
configured account (first enabled) for read paths. Threads the account through
email.ts, accounts, resync, queue sync, channel handlers, and the email_db MCP
tool path. Drops the dead getUserEmailDir helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:34:29 +00:00
pastilhasandClaude Opus 4.8 f3492512ba unify agent items into a flat file-based store, drop the marketplace
Replace the marketplace service dependency and the native/global/user
scope tiers with a single external directory ($OFFICER_ITEMS_DIR) holding
skills, tools, tasks, processes and extensions as plain files.

- tasks move from Postgres to TASK.md files (new file-backed task layer);
  task editing now works, which the DB path never supported
- skills/tools/processes collapse into one shared file router (single dir)
- remove the marketplace client (sync-marketplace/sync-version) and the
  boot-time sync; pi-bridge/pi-manager/sandbox point at the flat store
- drop the dead tasks + vestigial skills/tools/processes/extensions +
  item_chats tables (migration 0004)
- one-time migration script exports DB tasks and consolidates disk items

Migration verified: all 6 tasks round-trip through the runtime parser
identically to their DB rows (pipeline steps, triggers, script impls and
agentic bodies all intact).

NOTE: not yet functionally tested end-to-end — every item (each task mode,
tool, skill, extension) still needs to be run/exercised in the app before
this is trusted. To be done manually.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:39:17 +00:00
pastilhasandClaude Opus 4.6 24ac7ac796 replace gmail resync with Gmail REST API, remove mbsync dependency
First sync still uses IMAP with app password. Subsequent syncs use
Gmail API history.list + messages.get with OAuth for faster, more
reliable incremental sync. Dispatch gmail-sync handler for gmail
accounts instead of generic email-sync. Show sync button for synced
accounts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:24:44 +00:00
pastilhasandClaude Opus 4.6 ea31014d72 user-local installs for claude and pi, fix sandbox mounts
Move claude and pi from sudo global installs to ~/.local. Claude
binary is copied to /usr/local/bin for sandbox visibility, pi runs
via node from ~/.local/lib (ro-mounted). Fix bwrap intermediate dir
traversal by setting 0755 perms on auto-created HOME dirs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:08:45 +00:00
pastilhasandClaude Opus 4.6 56f8da8907 remove seed directory, clean up provisioning and sync modules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 00:07:50 +00:00
pastilhas 6c4595279a fine tuning 2026-03-07 00:52:20 +00:00
pastilhasandClaude Opus 4.6 d88fe3cac7 task logs: migrate from filesystem to postgresql; refactor sidecars into submodules
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 11:49:39 +00:00
pastilhasandClaude Opus 4.6 daf5580c39 vnc sidecar: per-user desktop sessions via sidecar architecture
replaces the single hardcoded systemd VNC service with a dynamic
sidecar that manages per-user VNC sessions on demand. any authenticated
user can now access their own desktop, not just Super Admin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:49:53 +00:00
pastilhas 6cfa40bad1 Merge remote-tracking branch 'origin/email-imap' 2026-03-06 07:27:45 +00:00
pastilhasandClaude Opus 4.6 7bbcccabf1 wip: remove opencode, searxng, resources; fix user settings read
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 07:27:19 +00:00
pastilhasandClaude Opus 4.6 170bd6d41b wip: email sync via imap with status tracking and auto cron
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 04:57:30 +00:00
pastilhasandClaude Opus 4.6 28f5cefb5b gmail sync: locale-agnostic folder mapping and import all mail
[Google Mail] locale variant was not matched by hardcoded [Gmail] paths,
so Sent/Starred/Important/Drafts were never labeled. All Mail was skipped
entirely, losing ~9k archived emails. Now normalizes the prefix, imports
everything with proper labels, and processes All Mail last so specific
folder labels take priority.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:29:22 +00:00
pastilhasandClaude Opus 4.6 b7a017d8e2 docker compose setup and .env generation in monorepo scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 04:23:49 +00:00
pastilhasandClaude Opus 4.6 e966a71180 isolate user data with personal group ownership
- chown user dirs to pastilhas:<username> instead of pastilhas:officerdev
  so users cannot access each other's data
- chmod 2770 (setgid) gives only the owning user terminal access
- setup.sh: ensure home dir is traversable (o+x) for provisioned users

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:44:44 +00:00
pastilhasandClaude Opus 4.6 3df47bb48d shared group provisioning, upload context menu, go path fix
- provision linux users with pastilhas:officerdev ownership so server
  can always read/write, terminal users get group access
- add officerdev shared group setup to setup.sh
- move go install to ~/.local/go with GOPATH at ~/.local/go-path
- add upload file/folder items to file browser context menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 02:34:38 +00:00