Monday, 14 July 2026

Electron desktop app shell

Roject packaged as a standalone Windows desktop app with JWT-based auth, token persistence, Authorization header injection, and a local Express server running in-process.

What we built

Electron main process (electron/main.ts)

The Express server starts in-process inside Electron's main process via a compiled startServer(port) export. A BrowserWindow points at http://localhost:3000. The server's static file, data, and locale paths all previously relied on __dirname relative to the TypeScript source; a new source/server/rootDir.ts module resolves these using process.env.ROJECT_ROOT (set by Electron before starting the server) with a fallback that preserves the existing ts-node behaviour.

electron/main.ts startServer(port) source/server/rootDir.ts ROJECT_ROOT

Electron login window

A minimal electron/login.html form collects email and password. Credentials are sent to the main process via IPC (contextBridge + ipcRenderer.invoke). The main process calls POST https://account.rokojori.com/api/auth/login directly using Node.js https — no browser redirect, no CORS, no cookie. Tokens are written to userData/tokens.json and reloaded on the next app start.

electron/login.html electron/preload.ts contextBridge tokens.json

Authorization header injection

session.defaultSession.webRequest.onBeforeSendHeaders intercepts every request from the BrowserWindow to http://localhost:3000/* and adds Authorization: Bearer <accessToken>. The existing extractToken middleware already checked this header (designed for Electron from the start), so the frontend required zero changes.

Token refresh + navigation guard

Server-side 302 redirects to /api/auth/refresh-session are intercepted by will-redirect — the main process calls POST /api/auth/refresh with the refresh token, updates the stored tokens, and reloads the original URL. A will-navigate guard prevents the BrowserWindow from ever leaving localhost: any external navigation (e.g. the app's login link pointing to account.rokojori.com) is intercepted and replaced with the Electron login window.

Build pipeline additions

Three new tsconfigs: tsconfig.electron.json (compiles electron/build/electron/), tsconfig.electron-server.json (compiles source/server/build/server/ for Electron's in-process require). New npm scripts: electron:build, electron:dev, electron:dist. scripts/copy-electron-assets.js copies login.html into build/electron/. scripts/launch-electron.js spawns the Electron binary with ELECTRON_RUN_AS_NODE deleted from the environment (VS Code / Claude Code set this variable, which otherwise makes Electron behave as plain Node.js with no GUI or API).

electron-builder scripts/launch-electron.js ELECTRON_RUN_AS_NODE workaround

Key decisions

Express runs in-process, not as a child process

The outline originally described the server as a child process. Running it in-process is simpler, removes IPC overhead, and is equivalent for the first iteration. The compiled server JS is required at runtime via a dynamic require(serverPath) call.

ELECTRON_RUN_AS_NODE is set by the VS Code environment

When this env var is set, the Electron binary runs as plain Node.js: no GUI, no Electron API, require('electron') returns the binary path string. The launcher script deletes it before spawning so the binary initialises as a real Electron app. This was discovered by writing debug output to a log file from inside the Electron process.

rootDir.ts centralises all server path resolution

Six server files used path.join(__dirname, '..', '..', ...) relative to the TypeScript source depth. Compiled output is one level deeper (build/server/server/), breaking all paths. A single rootDir.ts module exposes ROOT that all files import, using ROJECT_ROOT when set (Electron) or the __dirname fallback (ts-node).

.env loaded by the Electron main process

The Express server reads JWT_SECRET from the environment. When started via npm start this is provided by the shell; when started from Electron there is no shell. A small inline loadEnv() function in electron/main.ts parses the project-root .env file and sets missing variables before startServer is called.

Workspace boards system

Task boards (workspace/boards/)

A new Boards section added to the workspace with three boards: Tasks (current sprint, To Do / In Progress / Done), Bugs (Critical and Visual/UI/UX lanes), Backlog (MVP / Nice To Have / To The Moon). Each board uses custom HTML elements — <task-item>, <task-title>, <task-content> — styled to match the workspace visual language (CSS variables, same spacing rhythm, lane headers matching section h2). Clicking a title toggles the content open/closed via hide-content class. Shared styles extracted to workspace/_assets_/boards.css.

workspace/boards/ task-item boards.css

Outline shortened

workspace/outline/index.html was rewritten to remove the long numbered to-do list and the "Smaller open improvements" bullet list. Those items were distributed across the three boards. The outline now covers only: what Roject is, what exists now (condensed into named cards), a one-card pointer to the boards, and the technical implementation reference. About half the original length.

Workspace index improved

workspace/index.html gained a Boards card and a "Quick orientation" section with separate guidance for humans and agents: read Outline → Guides → Reference before making changes; check Boards and the most recent History entry for current context.

Project home redesign

Backend: user settings & server cleanup

Added UserSetting interface and userSettings CRUD module to source/server/db.ts (persisted in user_settings.json). Created source/server/routes/userSettings.ts with GET / PUT /api/user/settings (requires auth). Cleaned up source/server/index.ts: removed the groups router and explicit / redirect; added /edit/editor.html redirect and wired the new userSettings route.

user_settings.json /api/user/settings groups router removed

project-home component

New source/pages/index.html entry point loads Barlow 900 Italic from Google Fonts and the project-home component. project-home checks auth: logged-out → full-screen dark hero with brand text and login link; logged-in → fetches user settings, reads settings.theme, and dynamically imports the matching theme component (project-list-default by default; project-list-italic-neon planned).

source/pages/index.html project-home dynamic import

project-list-default component (default theme)

Full default theme: dark radial-gradient background, fixed nav with Roject logo (webp, inner area positioned at 70% scale) and a user-group element where hover slides in "Log out" beneath the email — clicking anywhere triggers logout. Each project row: 7em×7em rounded badge with project colour (hue from UUID via 31-hash), radial-gradient fill (centre at 51% alpha), Barlow 900 Italic skewed name in project colour. Hover reveals delete (×) and members buttons over the badge corners. A "New Project…" dashed-outline card sits first in the list and opens a name dialog on click. Rows animate in staggered on load. Back arrow in the editor updated to /.

project-list-default hueFromId (31-hash) Barlow skew matrix new-project dialog
Groups removed from the UI

The redesign spec removed groups entirely from the frontend and server routes. The data layer (groups / groupMembers in db.ts) is retained in case groups return later via rokojori-auth; only the router and all UI references were deleted.

Theme preference stored server-side, not in localStorage

JWT settings from rokojori-auth are read-only. A local user_settings.json DB file with a generic Record<string, unknown> value map provides per-user persistence for any app setting (starting with theme) without requiring changes to the auth service.

What's next

Italic Neon theme (batch 2): skewed cards, warm→cold gradient, hand-crafted SVG pattern icons (13 files already drawn), daily auto-rotation + user toggle. See Tasks board for the full sprint.