Tropical Update Publisher — Architecture Notes (stub)
Tropical Update Publisher — Architecture Notes (stub)
This document is a living stub started in Session 1B; later sessions expand it with IPC diagrams, service boundaries, and ADRs.
Main process (Session 1C)
- Entry:
src/main/index.tscompiles todist/main/index.js(tsconfig.main.json, NodeNext module settings inherited from the basetsconfig.json). - Window factory:
src/main/window/createMainWindow.tscentralizesBrowserWindowoptions (800×700 default, 600×500 minimum, title Tropical Update Uploader, sandbox + context isolation). - Path resolution: Source uses
import.meta.url→fileURLToPath→dirnameso compiled layout underdist/main/**resolvesdist/renderer/index.htmlandassets/icon.icocorrectly. npm start:npm run build:main && electron dist/main/index.js— does not rebuild the renderer; runnpm run build:rendererbefore production smoke tests soloadFilefinds the Vite output.npm run dev: Builds main once, then runsviteandelectron .together:wait-on tcp:5173gates Electron until the dev server listens, andcross-env NODE_ENV=developmentensures the main process uses theloadURLdev path (not productionloadFile).- DevTools: Opened only when
NODE_ENV === 'development'(after a successful dev-server load), never automatically in production.
Main logging — electron-log (Session 2A)
- Module:
src/main/logging/logger.tsexportsinitMainLogging(),LOG_ROTATION_MAX_FILES(5), andLOG_MAX_SIZE_BYTES(5 × 1024 × 1024, i.e. 5 MiB perproject-spec.md§29.1). - Boot order:
initMainLogging()runs first insidemain()insrc/main/index.ts, before pre-ready diagnostic hooks and beforeapp.whenReady()creates windows. Renderer log bridging is out of scope for 2A (main process only). - Paths: Primary file log is
{userData}/logs/main.log, whereuserDatacomes fromapp.getPath('userData'). Packaged installs use Electron’s per-user app data directory; dev runs use the same API (path differs by OS andapp.setName/appId). Opening this folder from Settings/About is deferred to a later session; the path rule above is the contract. - Rotation:
electron-logtransports.file.maxSizeis set to 5 MiB. When the file exceeds that size, a customarchiveLoghandler renamesmain.log→main.log.1, shifts.1→.2… and drops.5, retaining five numbered archives (plus the new emptymain.log). - Levels: File transport uses debug when
NODE_ENV === 'development', otherwise info (production default per §29.1). Console transport behavior followselectron-logdefaults unless a later session narrows it. - Failure modes: If
app.getPath('userData')throws beforeapp.isReady(), initialization is deferred toapp.whenReady(). Ifmkdirforlogs/fails (e.g. disk full), the file transport is disabled (level = false) and a console error explains that file logging is unavailable — the app continues to start.
Path resolution — PathService (Session 2B)
- Module:
src/main/services/paths/PathService.ts(PathServiceclass +createPathService, exported defaultsDEFAULT_ICLOUD_TROPICAL_PATH_TEMPLATE,DEFAULT_INCOMING_POSTS_RELATIVE,PATH_UNRESOLVED_CODE). - Ownership (P2): All filesystem path resolution for publish workflows stays in main; the renderer will consume snapshots later via IPC (not direct
fs). - Defaults until Session 4B:
incoming/postsis fixed as the git-relative segment (project-spec.md§8 — forward slashes forgit add).tropicalUpdatesPathfalls back to the master plan §2 iCloud template with%USERPROFILE%expansion on Windows. - POSIX invariant:
getFullIncomingPath()returns an absolute path built withpath.posix.joinafter normalizing the repo root to forward slashes, so the incoming folder path is suitable for the same/convention used inincoming/posts/{filename}staging (spec §8). - Boot logging (P6):
src/main/index.tscallslog.debugonce afterapp.whenReady(), with home directory segments redacted to~for operator samples. - UNC / network paths: On Windows (
process.platform === 'win32'), tropical paths usepath.win32.isAbsolute, so well-formed UNC roots (\\server\share\...) match production Electron semantics and are accepted. On other platforms, POSIX absolute rules apply (macOS/Linux dev runs). Mount availability is validated later when the folder is opened (nothrowfromPathServiceon import or resolve).
Resolution order (read top → bottom)
app.getAppPath() --> TOOL_ROOT (Electron app / package directory)
|
+-- path.resolve(TOOL_ROOT, '..', '..') --> REPO_ROOT (WxManBran site repo per master plan §2)
|
+-- incoming segment: constant "incoming/posts" (POSIX, git-relative)
|
+-- posix.join(REPO_ROOT as POSIX, "incoming/posts") --> fullIncomingPath (debug / future IPC)
config tropical string (or empty)
|
+-- if empty: DEFAULT_ICLOUD_TROPICAL_PATH_TEMPLATE
|
+-- expand %USERPROFILE% (case-insensitive) + trim
|
+-- reject if empty, unresolved %VAR%, or not absolute --> { code: PATH_UNRESOLVED }
|
+-- path.resolve(expanded) --> tropicalUpdatesPath string
For non-coders: the website’s git checkout is two folders above the installed app folder; incoming files always live under incoming/posts inside that checkout; your iCloud briefing folder is a separate absolute Windows path (default template until Settings saves a real value in Session 4B).
Renderer load strategy (Electron)
Development
- Mechanism:
BrowserWindowloads the Vite dev server URL (defaulthttp://localhost:5173/, matchesVITE_DEV_SERVER_URLin main). - Condition:
process.env.NODE_ENV === 'development'(wired in Session 1C). - Rationale: HMR and fast iteration; avoids
file://origin quirks while developing UI.
Production
- Mechanism:
mainWindow.loadFile(...)pointing at the builtindex.htmlunderdist/renderer/, resolved relative to the compiled main bundle (e.g. fromdist/main/index.js→ siblingdist/renderer/index.html). - Rationale: Packaged app has no dev server; static assets must use relative URLs.
- Vite setting:
base: './'invite.config.tsso asset paths resolve underfile://when loaded vialoadFile.
Security posture (renderer)
Per project-spec.md: nodeIntegration: false, contextIsolation: true, privileged work stays in main / preload (preload lands in later sessions). Session 1B does not change that model.
Build outputs
| Output | Producer | Purpose |
|---|---|---|
dist/main/ |
tsc -p tsconfig.main.json |
Electron main process |
dist/renderer/ |
vite build (npm run build:renderer) |
Packaged renderer SPA |
Installer artifacts from electron-builder target release/ (see Session 1A report) to avoid colliding with compile outputs.
Related paths in this package
- Vite entry:
index.html→src/renderer/main.tsx - Tailwind content scan:
tailwind.config.ts→./src/renderer/**/* - Renderer typecheck:
tsconfig.renderer.json(strictinherited from basetsconfig.json)
ADR — Tailwind CSS major version (Session 1E)
Context: shadcn/ui historically targets Tailwind v3 (tailwind.config.ts, PostCSS, tailwindcss-animate). Tailwind v4 moves configuration toward CSS-first APIs and the @tailwindcss/vite plugin.
Decision: Stay on Tailwind CSS 3.4.x with PostCSS (postcss.config.js) until a dedicated migration ticket updates shadcn’s Vite recipe, styles/globals.css, and CI visual checks together.
Consequences: Lowest friction for copied shadcn components and animate-in utilities from tailwindcss-animate. A future v4 migration must be one atomic PR (no mixed v3 config + v4-only imports).
Renderer UI — shadcn/ui baseline (Session 1E)
- Config:
components.jsonpins New York style,cssVariables: true, Tailwind entrysrc/renderer/styles/globals.css, aliases under@/→src/renderer(mirrored invite.config.tsandtsconfig.renderer.jsonpaths). - Primitives:
src/renderer/components/ui/{button,input,dialog}.tsxplussrc/renderer/lib/utils.ts(cn()). - Dark mode:
tailwind.config.tsusesdarkMode: ['class']. Toggle by adding/removingdarkondocument.documentElementviaThemeToggle+bootstrapRendererChrome(Session 7C). Theme tokens live instyles/globals.cssas:root/.darkHSL variables (spec §23.2 palette + shadcn semantic map).
Pipeline note — session prompts vs automated agents
Some session prompts ask the implementer to stop and wait for human approval on each “best judgment” proposal. state/current-prompt.md runs in non-interactive agent mode: when no human is available, list proposals in the session report and continue implementation rather than blocking the pipeline. Treat that as the authoritative override for audits so this behavior is not mistaken for prompt drift.
Renderer motion — Framer presets (Session 7D)
- Module:
src/renderer/motion/presets.ts— normative 200ms route fade and 50ms list stagger constants (§23.4), pure variant factories,coerceReducedMotionFlagforuseReducedMotion()nullability. - Layout:
src/renderer/motion/RouteTransitionLayout.tsx— parent route element wrappingOutletwithAnimatePresence mode="wait"so rapid navigation does not stack overlapping route tweens. - Integration:
AppShellnests primary routes underRouteTransitionLayout;Sidebarwidth transition usesduration-[var(--motion-duration-medium)](Session 7C tokens, including reduced-motion collapse). - Demo list:
SettingsPlaceholderapplies staggeredmotion.ul/motion.lifor operator verification of the 50ms feel.
Renderer chrome — status + top bars (Session 7E)
- Top bar:
src/renderer/layout/TopBar.tsx— spec §23.3 identity row, centered command-palette affordance (noop until Session 9E),ThemeToggleon the right. UsestopBarModel.tsfor ⌘ vs Ctrl+K labeling. - Status bar:
src/renderer/layout/StatusBar.tsx— persistent footer with §23.3 left-to-right order (monitor placeholder, AI label, last push placeholder, storms placeholder, git branch viauseGitStatus, network stub). Git failures render a non-silentrole="alert"strip with Retry; a politearia-livesummary mirrors the strip for screen readers. - Pure helpers:
statusBarModel.tsformats branch labels and builds the live-region sentence so Vitest can lock copy without mounting Framer or the full shell. - Shell wiring:
AppShellwrapsTopBar+main#app-main+StatusBarin a flex column besideSidebar; theme toggle moved out of the sidebar header to avoid duplicate controls.