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.ts compiles to dist/main/index.js (tsconfig.main.json, NodeNext module settings inherited from the base tsconfig.json).
  • Window factory: src/main/window/createMainWindow.ts centralizes BrowserWindow options (800×700 default, 600×500 minimum, title Tropical Update Uploader, sandbox + context isolation).
  • Path resolution: Source uses import.meta.urlfileURLToPathdirname so compiled layout under dist/main/** resolves dist/renderer/index.html and assets/icon.ico correctly.
  • npm start: npm run build:main && electron dist/main/index.jsdoes not rebuild the renderer; run npm run build:renderer before production smoke tests so loadFile finds the Vite output.
  • npm run dev: Builds main once, then runs vite and electron . together: wait-on tcp:5173 gates Electron until the dev server listens, and cross-env NODE_ENV=development ensures the main process uses the loadURL dev path (not production loadFile).
  • 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.ts exports initMainLogging(), LOG_ROTATION_MAX_FILES (5), and LOG_MAX_SIZE_BYTES (5 × 1024 × 1024, i.e. 5 MiB per project-spec.md §29.1).
  • Boot order: initMainLogging() runs first inside main() in src/main/index.ts, before pre-ready diagnostic hooks and before app.whenReady() creates windows. Renderer log bridging is out of scope for 2A (main process only).
  • Paths: Primary file log is {userData}/logs/main.log, where userData comes from app.getPath('userData'). Packaged installs use Electron’s per-user app data directory; dev runs use the same API (path differs by OS and app.setName / appId). Opening this folder from Settings/About is deferred to a later session; the path rule above is the contract.
  • Rotation: electron-log transports.file.maxSize is set to 5 MiB. When the file exceeds that size, a custom archiveLog handler renames main.logmain.log.1, shifts .1.2 … and drops .5, retaining five numbered archives (plus the new empty main.log).
  • Levels: File transport uses debug when NODE_ENV === 'development', otherwise info (production default per §29.1). Console transport behavior follows electron-log defaults unless a later session narrows it.
  • Failure modes: If app.getPath('userData') throws before app.isReady(), initialization is deferred to app.whenReady(). If mkdir for logs/ 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 (PathService class + createPathService, exported defaults DEFAULT_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/posts is fixed as the git-relative segment (project-spec.md §8 — forward slashes for git add). tropicalUpdatesPath falls back to the master plan §2 iCloud template with %USERPROFILE% expansion on Windows.
  • POSIX invariant: getFullIncomingPath() returns an absolute path built with path.posix.join after normalizing the repo root to forward slashes, so the incoming folder path is suitable for the same / convention used in incoming/posts/{filename} staging (spec §8).
  • Boot logging (P6): src/main/index.ts calls log.debug once after app.whenReady(), with home directory segments redacted to ~ for operator samples.
  • UNC / network paths: On Windows (process.platform === 'win32'), tropical paths use path.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 (no throw from PathService on 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: BrowserWindow loads the Vite dev server URL (default http://localhost:5173/, matches VITE_DEV_SERVER_URL in 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 built index.html under dist/renderer/, resolved relative to the compiled main bundle (e.g. from dist/main/index.js → sibling dist/renderer/index.html).
  • Rationale: Packaged app has no dev server; static assets must use relative URLs.
  • Vite setting: base: './' in vite.config.ts so asset paths resolve under file:// when loaded via loadFile.

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.

  • Vite entry: index.htmlsrc/renderer/main.tsx
  • Tailwind content scan: tailwind.config.ts./src/renderer/**/*
  • Renderer typecheck: tsconfig.renderer.json (strict inherited from base tsconfig.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.json pins New York style, cssVariables: true, Tailwind entry src/renderer/styles/globals.css, aliases under @/src/renderer (mirrored in vite.config.ts and tsconfig.renderer.json paths).
  • Primitives: src/renderer/components/ui/{button,input,dialog}.tsx plus src/renderer/lib/utils.ts (cn()).
  • Dark mode: tailwind.config.ts uses darkMode: ['class']. Toggle by adding/removing dark on document.documentElement via ThemeToggle + bootstrapRendererChrome (Session 7C). Theme tokens live in styles/globals.css as :root / .dark HSL 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, coerceReducedMotionFlag for useReducedMotion() nullability.
  • Layout: src/renderer/motion/RouteTransitionLayout.tsx — parent route element wrapping Outlet with AnimatePresence mode="wait" so rapid navigation does not stack overlapping route tweens.
  • Integration: AppShell nests primary routes under RouteTransitionLayout; Sidebar width transition uses duration-[var(--motion-duration-medium)] (Session 7C tokens, including reduced-motion collapse).
  • Demo list: SettingsPlaceholder applies staggered motion.ul / motion.li for 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), ThemeToggle on the right. Uses topBarModel.ts for ⌘ 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 via useGitStatus, network stub). Git failures render a non-silent role="alert" strip with Retry; a polite aria-live summary mirrors the strip for screen readers.
  • Pure helpers: statusBarModel.ts formats branch labels and builds the live-region sentence so Vitest can lock copy without mounting Framer or the full shell.
  • Shell wiring: AppShell wraps TopBar + main#app-main + StatusBar in a flex column beside Sidebar; theme toggle moved out of the sidebar header to avoid duplicate controls.