rojects/workspace/history/2026/07-July/14-Monday/index.html

192 lines
8.1 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Monday, 14 July 2026 — Roject</title>
<link rel="stylesheet" href="../../../../_assets_/styles.css">
<link rel="stylesheet" href="../../../../_assets_/nav.css">
</head>
<body>
<div class="page">
<header>
<p class="date">Monday, 14 July 2026</p>
<h1>Electron desktop app shell</h1>
<p class="subtitle">
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.
</p>
</header>
<section>
<h2>What we built</h2>
<div class="card">
<h3>Electron main process (<code>electron/main.ts</code>)</h3>
<p>
The Express server starts in-process inside Electron's main process via a
compiled <code>startServer(port)</code> export. A <code>BrowserWindow</code>
points at <code>http://localhost:3000</code>. The server's static file,
data, and locale paths all previously relied on <code>__dirname</code>
relative to the TypeScript source; a new <code>source/server/rootDir.ts</code>
module resolves these using <code>process.env.ROJECT_ROOT</code> (set by
Electron before starting the server) with a fallback that preserves the
existing ts-node behaviour.
</p>
<div class="tags">
<span class="tag">electron/main.ts</span>
<span class="tag">startServer(port)</span>
<span class="tag">source/server/rootDir.ts</span>
<span class="tag">ROJECT_ROOT</span>
</div>
</div>
<div class="card">
<h3>Electron login window</h3>
<p>
A minimal <code>electron/login.html</code> form collects email and password.
Credentials are sent to the main process via IPC (<code>contextBridge</code> +
<code>ipcRenderer.invoke</code>). The main process calls
<code>POST https://account.rokojori.com/api/auth/login</code> directly using
Node.js <code>https</code> — no browser redirect, no CORS, no cookie.
Tokens are written to <code>userData/tokens.json</code> and reloaded on
the next app start.
</p>
<div class="tags">
<span class="tag">electron/login.html</span>
<span class="tag">electron/preload.ts</span>
<span class="tag">contextBridge</span>
<span class="tag">tokens.json</span>
</div>
</div>
<div class="card">
<h3>Authorization header injection</h3>
<p>
<code>session.defaultSession.webRequest.onBeforeSendHeaders</code> intercepts
every request from the BrowserWindow to <code>http://localhost:3000/*</code>
and adds <code>Authorization: Bearer &lt;accessToken&gt;</code>. The
existing <code>extractToken</code> middleware already checked this header
(designed for Electron from the start), so the frontend required zero changes.
</p>
</div>
<div class="card">
<h3>Token refresh + navigation guard</h3>
<p>
Server-side <code>302</code> redirects to
<code>/api/auth/refresh-session</code> are intercepted by
<code>will-redirect</code> — the main process calls
<code>POST /api/auth/refresh</code> with the refresh token, updates the
stored tokens, and reloads the original URL. A <code>will-navigate</code>
guard prevents the BrowserWindow from ever leaving <code>localhost</code>:
any external navigation (e.g. the app's login link pointing to
<code>account.rokojori.com</code>) is intercepted and replaced with the
Electron login window.
</p>
</div>
<div class="card">
<h3>Build pipeline additions</h3>
<p>
Three new tsconfigs: <code>tsconfig.electron.json</code> (compiles
<code>electron/</code><code>build/electron/</code>),
<code>tsconfig.electron-server.json</code> (compiles
<code>source/server/</code><code>build/server/</code> for Electron's
in-process require). New npm scripts: <code>electron:build</code>,
<code>electron:dev</code>, <code>electron:dist</code>.
<code>scripts/copy-electron-assets.js</code> copies <code>login.html</code>
into <code>build/electron/</code>.
<code>scripts/launch-electron.js</code> spawns the Electron binary with
<code>ELECTRON_RUN_AS_NODE</code> 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).
</p>
<div class="tags">
<span class="tag">electron-builder</span>
<span class="tag">scripts/launch-electron.js</span>
<span class="tag">ELECTRON_RUN_AS_NODE workaround</span>
</div>
</div>
</section>
<section>
<h2>Key decisions</h2>
<div class="decision">
<strong>Express runs in-process, not as a child process</strong>
<p>
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
<code>require(serverPath)</code> call.
</p>
</div>
<div class="decision">
<strong>ELECTRON_RUN_AS_NODE is set by the VS Code environment</strong>
<p>
When this env var is set, the Electron binary runs as plain Node.js: no GUI,
no Electron API, <code>require('electron')</code> 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.
</p>
</div>
<div class="decision">
<strong>rootDir.ts centralises all server path resolution</strong>
<p>
Six server files used <code>path.join(__dirname, '..', '..', ...)</code>
relative to the TypeScript source depth. Compiled output is one level deeper
(<code>build/server/server/</code>), breaking all paths. A single
<code>rootDir.ts</code> module exposes <code>ROOT</code> that all files
import, using <code>ROJECT_ROOT</code> when set (Electron) or the
<code>__dirname</code> fallback (ts-node).
</p>
</div>
<div class="decision">
<strong>.env loaded by the Electron main process</strong>
<p>
The Express server reads <code>JWT_SECRET</code> from the environment.
When started via <code>npm start</code> this is provided by the shell; when
started from Electron there is no shell. A small inline <code>loadEnv()</code>
function in <code>electron/main.ts</code> parses the project-root
<code>.env</code> file and sets missing variables before <code>startServer</code>
is called.
</p>
</div>
</section>
<section>
<h2>What's next</h2>
<div class="card">
<p>
Local filesystem access — extend the file tree to browse arbitrary directories
on the host machine using Node.js <code>fs</code> rather than the server's
JSON-backed project storage.
</p>
<p style="margin-top:0.75rem">
Remote projects in Electron — allow the Electron app to connect to a remote
Roject server (<code>roject.rokojori.com</code>) and list projects hosted
there alongside local ones. The JWT is already available; it's a matter of
pointing a request (or a BrowserView panel) at the remote URL with the token.
</p>
</div>
</section>
<footer>
Roject &mdash; session history
</footer>
</div>
<script>var NAV_ROOT = '../../../../';</script>
<script src="../../../../_assets_/nav-data.js"></script>
<script src="../../../../_assets_/nav.js"></script>
</body>
</html>