rojects/workspace/history/2026/07-July/18-Friday/index.html

243 lines
12 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Friday, 18 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">Friday, 18 July 2026</p>
<h1>Session History</h1>
<p class="subtitle">CodeMirror syntax highlighting system, smart file-tree open, rokojori-auth login fix. Electron local dev fixes: stale cookie auth, clock tolerance, quit-on-login, credential persistence. Nav z-index fix.</p>
</header>
<section>
<h2>What we built</h2>
<div class="card">
<h3>File tree: single-click smart file open</h3>
<p>
Clicking a file in the file tree now opens it intelligently rather than always
targeting any available panel:
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li>If the file is already open in any panel, that panel's tab is focused — no
duplicate open, no reload.</li>
<li>Otherwise, the next available editor panel of the correct type that is not
pinned and not dirty is used; a new panel is created in the active section
if none qualifies.</li>
<li>Pinned editors (<code>_pinned</code> property on the panel element) are
excluded from the available pool entirely.</li>
</ul>
</div>
<div class="card">
<h3>CodeMirror vendor syntax modes</h3>
<p>
Four minified CodeMirror 5 mode files added to <code>source/vendor/</code> and
loaded in <code>editor.html</code>:
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li><code>cm-mode-clike.min.js</code> — C/C++/Java; used for GLSL and GDShader</li>
<li><code>cm-mode-python.min.js</code> — Python; used for GDScript (<code>.gd</code>)</li>
<li><code>cm-mode-shell.min.js</code> — shell scripts (<code>.sh</code>)</li>
<li><code>cm-mode-yaml.min.js</code> — YAML and YML</li>
</ul>
<p style="margin-top:0.75rem">
Extension mappings in <code>code-panel</code>'s <code>_resolveMode</code>:
<code>.yaml</code>/<code>.yml</code><code>yaml</code>,
<code>.sh</code><code>shell</code>,
<code>.gd</code><code>python</code>,
<code>.glsl</code>/<code>.gdshader</code>/<code>.gdshaderinc</code><code>clike</code>,
<code>.cs</code><code>rokojori-cs</code>.
</p>
</div>
<div class="card">
<h3>CodeMirror lexer mode system — BrowserLexer + CodeMirrorLexerMode</h3>
<p>
A custom, self-contained browser lexer and CodeMirror mode wrapper built in
<code>source/components/code-panel/</code>:
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li><code>BrowserLexer.ts</code> — zero external imports. Inlines
<code>makeSticky()</code> (adds <code>/y</code> flag to regexes).
<code>BrowserMatcher</code> uses sticky regex + <code>lastIndex</code> for
positional matching. <code>BrowserLexer</code> holds named mode lists of
matchers. Exports a <code>cLikeLexer()</code> factory with matchers for all
C-like token types (comments, strings, numbers, operators, keywords,
identifiers, etc.).</li>
<li><code>CodeMirrorLexerMode.ts</code> — wraps any <code>BrowserLexer</code>
into a CodeMirror 5 mode object. Supports multi-line block definitions (start
regex → end regex → CSS class; state preserved across lines). Supports named
keyword sets: sets of words that override the base CSS class for a given token
type (e.g. mapping C# keywords from <code>CWORD</code><code>keyword</code>).
Keyword sets are mutable at runtime — add/remove/update without recreating
the mode. <code>refresh(cm)</code> forces CodeMirror to re-tokenize by
re-setting the mode option.</li>
<li><code>CSharpMode.ts</code> — creates <code>csharpMode</code> using
<code>cLikeLexer()</code> with a multi-line <code>/* ... */</code> comment
block and a keyword set of ~70 C# keywords. Registered in CodeMirror as
<code>'rokojori-cs'</code>.</li>
</ul>
<p style="margin-top:0.75rem">
The browser-only design (no <code>library-ts</code> dependency) avoids the
<code>moduleResolution: "bundler"</code> / <code>ts-node</code> conflict:
library-ts compiles without <code>.js</code> extensions (works for
<code>ts-node</code>), while browser ES modules require explicit extensions.
Keeping the lexer self-contained in <code>code-panel/</code> eliminates the
tension entirely.
</p>
</div>
<div class="card">
<h3>rokojori-auth: remove broken refresh-session redirect</h3>
<p>
The page-level middleware in <code>rokojori-auth/source/server/index.ts</code>
was redirecting expired <code>accessToken</code> requests to
<code>/api/auth/refresh-session?redirect=...</code> — a route that no longer
exists. This blocked login entirely (redirect loop on first visit after token
expiry). Fixed by replacing the entire error branch with <code>next()</code>:
the middleware now passes through on any token error. Transparent refresh for
Roject API calls is handled server-side by Roject's own
<code>jwtMiddleware</code>.
</p>
</div>
</section>
<section>
<h2>Key decisions</h2>
<div class="card">
<p>
<strong>Self-contained BrowserLexer instead of reusing library-ts CLikeLexer.</strong>
Importing from library-ts pulled in extensionless relative imports that break the
browser ES module loader (<code>NS_ERROR_CORRUPTED_CONTENT</code>). Adding
<code>.js</code> extensions to library-ts imports broke <code>ts-node</code>
(CommonJS cannot remap <code>.js</code><code>.ts</code>). The cleanest fix
was a purpose-built, dependency-free browser lexer duplicating only what the
code editor needs.
</p>
</div>
<div class="card">
<p>
<strong>Single-click open, not double-click.</strong> The board task said
double-click, but single-click is more natural for an IDE file tree (matches
VS Code, JetBrains). The smart-targeting logic (focus existing, skip pinned)
makes single-click safe — it never disrupts an intentionally pinned panel.
</p>
</div>
</section>
<section>
<h2>Session 2 — Electron local dev fixes</h2>
<div class="card">
<h3>Mobile: nav z-index fix</h3>
<p>
<code>.pld-nav</code> in <code>project-list-default.css</code> has
<code>position: fixed</code> but no <code>z-index</code>. Stacking contexts
created by <code>position: relative</code> project rows on mobile buried the nav
underneath them. Fixed with <code>z-index: 10</code>. Overlays remain above at
<code>z-index: 200</code>.
</p>
</div>
<div class="card">
<h3>Bearer-before-cookie in <code>extractToken</code></h3>
<p>
The root cause of Electron auth failures: <code>extractToken()</code> in
<code>source/auth-connector/source/server/auth.ts</code> was checking the
<code>accessToken</code> cookie before the <code>Authorization</code> header.
Electron's Chromium session had a stale <code>accessToken</code> cookie that took
priority over the fresh Bearer token injected via
<code>session.defaultSession.webRequest.onBeforeSendHeaders</code>. Fixed by
reversing the check order: Bearer header wins, cookie is the fallback.
</p>
<p style="margin-top:0.75rem">
Additionally, <code>createMainWindow()</code> now calls
<code>session.defaultSession.clearStorageData({ storages: ['cookies'] })</code>
before creating the window, preventing the stale cookie from accumulating across
Electron restarts.
</p>
</div>
<div class="card">
<h3>JWT clock skew — <code>JWT_CLOCK_TOLERANCE</code></h3>
<p>
Fresh tokens issued by the production auth server (1 h TTL) appeared expired
immediately on the Windows dev machine because the local clock was ~65 minutes
ahead of the server. Every <code>jwt.verify</code> call returned
<code>TokenExpiredError</code> seconds after login.
</p>
<p style="margin-top:0.75rem">
Fix: <code>jwt.verify</code> accepts a <code>clockTolerance</code> option. An env
var <code>JWT_CLOCK_TOLERANCE</code> (integer, seconds; default 0) is now read and
passed as <code>clockTolerance</code> when non-zero. Set to <code>7200</code> in
<code>.env</code> for local development. The underlying audit task (time must never
depend on the user's clock) is on the board.
</p>
</div>
<div class="card">
<h3>Electron startup token refresh + quit-on-login fix</h3>
<p>
Two issues fixed in <code>electron/main.ts</code>:
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li><strong>Startup refresh:</strong> on launch with saved tokens, the app now
calls <code>POST account.rokojori.com/api/auth/refresh</code> before opening
the main window. Fresh tokens are saved; if refresh fails the login window is
shown instead. This prevents using expired access tokens on startup.</li>
<li><strong>Quit-on-login race:</strong> <code>createMainWindow()</code> was made
<code>async</code> (to await the cookie clear), but <code>loginWindow?.close()</code>
was called before awaiting it. Zero open windows → <code>window-all-closed</code>
<code>app.quit()</code>. Fixed by:
<code>createMainWindow().then(() =&gt; loginWindow?.close())</code>.</li>
</ul>
</div>
<div class="card">
<h3>Electron login: credential persistence + remember-me</h3>
<p>
The login window (<code>electron/login.html</code>) was extended:
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li><strong>Remember me</strong> checkbox (checked by default): when checked,
email and password are saved to <code>userData/last-email.txt</code> and
<code>userData/last-password.txt</code> on successful login; when unchecked, any
saved files are deleted.</li>
<li><strong>Clear saved</strong> button: calls
<code>auth:clear-credentials</code> IPC, wipes the fields, and disables itself.
Hidden when no credentials are saved.</li>
<li>On load, both fields are pre-filled from saved values; focus goes to the Sign
in button if both are filled, the password field if only email is saved, or the
email field if nothing is saved.</li>
</ul>
<p style="margin-top:0.75rem">
IPC surface added to <code>electron/preload.ts</code>:
<code>lastEmail()</code>, <code>lastPassword()</code>, <code>clearCredentials()</code>.
</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>