email leaves the platform for plugins/email

the sidecar, the router, the table, the screens and the account manager are
gone from src/ entirely - deleted, not commented, because a commented mount is
what assertPermissionTotality cannot see.

three reverse imports had to go first, and they are what decided the boundary.
core may not import a plugin's queries, and one of these had already proved
why: getEmailAccounts was awaited at the top level of the agent sidecar, so
when the table went away on 2026-08-13 the rejection took officer-agent into a
pm2 restart loop with every live chat session inside it (fbb6d6c7).

both survivors now read the DISK instead. findEmailAccountName() in
data-path.ts picks the newest emails.db under an account's email_accounts/
directory - no import, no query, and nothing to throw when the plugin is not
installed. the layout was already the platform's, so this costs no coupling.
the agent keeps its email_db tool and an email-context chat still opens in the
right mailbox.

the google oauth callback no longer appends /email to the dock: a plugin's
route is the plugin's business.

app-store/catalogue.ts entry deleted, and that is the whole point rather than
tidying. permissionAvailability derives from sidecar_installs and a plugin only
ever gets a plugin_installs row, so a leftover entry makes the permission
permanently unavailable - denied route, no dock tile - on a machine where the
plugin is installed and healthy. it has bitten headscale and nearly music.

two app-store tests used email as their config-only fixture; repointed at
notify, which is the same shape.

google-auth stays in core and the plugin imports it via @@/. that direction is
allowed and google-auth knows nothing about mail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 00:28:13 +00:00
co-authored by Claude Opus 5
parent 4655d374ec
commit 98c37c4ce9
44 changed files with 58 additions and 5583 deletions
+2 -13
View File
@@ -374,19 +374,8 @@ export const CATALOGUE: CatalogueEntry[] = [
// place to type the same URL and API key.
// ── Nothing to reach: configuration only ───────────────────────────────────────────────────────────
{
id: 'email',
ui: { name: 'Email', icon: 'Mail', color: '#ef4444', rootRoute: '/email', routes: ['/email'] },
process: 'officer-email',
label: 'Email',
summary: 'Your IMAP accounts, synced and searchable',
members: 'none',
modes: ['config'],
permission: 'email',
// Deliberately empty: accounts are added from /email, which already has a working multi-account
// form. Duplicating it here would be a second place to maintain the same credentials.
configFields: [],
},
// Email was here until 2026-08-18, when it became `plugins/email/`. Removing it is not tidying — it is
// the headscale bug two comments up, exactly, and it would have fired on the plugin's first install.
// Music was here until 2026-08-15, when it became `plugins/music/`. Removing it was not tidying — it
// was the headscale bug above, exactly, and it would have fired on the first install.
//
+2 -2
View File
@@ -8,7 +8,7 @@ import { byId } from './catalogue';
const photos = byId('photos')!; // existing|provisioned, members: 'accounts'
const transmission = byId('transmission')!; // existing|provisioned, members: 'none'
const email = byId('email')!; // config only
const configOnly = byId('notify')!; // config only
/** Records what was actually called, so a test can assert on absence as well as presence. */
function spyEffects(over: Partial<InstallEffects> = {}) {
@@ -51,7 +51,7 @@ describe('planSteps', () => {
});
it('has nothing to connect for a config-only install', () => {
expect(planSteps(email, 'config')).toEqual(['preflight', 'schema', 'assets', 'process']);
expect(planSteps(configOnly, 'config')).toEqual(['preflight', 'schema', 'assets', 'process']);
});
it('always starts with preflight', () => {
+1 -1
View File
@@ -17,6 +17,6 @@ describe('preflight is per mode, not per entry', () => {
});
it('allows a config-only sidecar regardless', async () => {
expect(await preflight(byId('email')!, 'config')).toEqual({ ok: true });
expect(await preflight(byId('notify')!, 'config')).toEqual({ ok: true });
});
});