rojects/workspace/outline/index.html

627 lines
35 KiB
HTML
Raw 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>Roject — Developer Documentation</title>
<link rel="stylesheet" href="../_assets_/styles.css">
<link rel="stylesheet" href="../_assets_/nav.css">
</head>
<body>
<div class="page">
<div style="width:100%;height:400px;overflow:hidden;background:#000;margin-bottom:2rem;border-radius:8px;">
<img src="../_assets_/roject.svg" alt="Rojects"
style="width:100%;height:100%;object-fit:cover;object-position:center;">
</div>
<header>
<p class="date">Project Documentation</p>
<h1 style="font-size: 300%;">Roject</h1>
<p class="subtitle">Developer reference for human and agent contributors. Keep this file up to date as the project evolves.</p>
</header>
<section>
<h2>What it is</h2>
<div class="card">
<p>
Roject is a self-hosted, agent-based IDE for working with files and projects.
It provides specialised editors for different file types — a WYSIWYG HTML editor,
a CodeMirror-backed code editor, and more to come — all within a multi-panel,
tab-based workspace. Projects can be hosted on a Roject server (remote) or opened
directly from the local filesystem (local), making it usable both as a lightweight
self-hosted CMS and as a full desktop development environment.
Designed for individual developers or small teams; no external database dependency.
</p>
</div>
</section>
<section>
<h2>What exists now</h2>
<div class="card">
<h3>Auth &amp; accounts</h3>
<p>
User accounts are managed by <strong>rokojori-auth</strong> at
<code>account.rokojori.com</code> — registration, login, JWT issuance, refresh
token rotation with a grace window, roles, and account deletion.
2026-07-17 11:37:48 +00:00
</p>
<p style="margin-top:0.75rem">
<strong>Refresh token grace window (rokojori-auth):</strong>
<code>refreshTokens</code> are soft-deleted — <code>usedAt</code> and
<code>replacedBy</code> are stamped on first use rather than the row being deleted.
A concurrent second refresh within <code>REFRESH_GRACE_TTL</code> (10 s) resolves
to the same replacement pair instead of 401ing. This is the baseline correctness
guarantee against races; client-side leader election is an optimisation on top.
</p>
<p style="margin-top:0.75rem">
<strong>TokenUpdater (<code>source/auth/TokenUpdater.ts</code>):</strong>
Central browser auth state machine. State enum: <code>valid | refreshing | expired | network-error</code>,
exposed via <code>EventSlot</code>. Triggers: a 5-minute periodic timer and
<code>ActivityAnalyser.onActive</code> (fires on mouse, touch, focus, and tab-visibility
restore). Each trigger pings <code>GET /api/auth/me</code>; <code>jwtMiddleware</code>
handles proactive cookie rotation server-side when within 15 min of expiry
(access token is <code>httpOnly</code>, so expiry is unreadable client-side).
Web Locks leader election: the first tab acquires <code>roject-token-updater-leader</code>
exclusively and broadcasts state to followers via <code>BroadcastChannel</code>.
Leader handoff is automatic when the holder tab closes.
</p>
<p style="margin-top:0.75rem">
<strong>GuardedCall (<code>source/auth/GuardedCall.ts</code>):</strong>
Singleton wrapper for all API calls. Three tiers —
<code>user</code>: no retry, throws on failure;
<code>editor</code>: 3 retries at 1 s / 3 s / 8 s, then throws;
<code>silent</code>: same delays, never throws, <code>console.warn</code> on failure.
Pre-flight: blocks when state is <code>expired</code>; waits up to 6 s when
<code>refreshing</code>. Wired into <code>Editor.ts</code> (save = user, open = editor)
and <code>editor-shell.ts</code> (layout save/load = silent).
</p>
<p style="margin-top:0.75rem">
<strong>Token extraction order (<code>extractToken</code> in <code>auth.ts</code>):</strong>
Bearer header is checked before the <code>accessToken</code> cookie. An explicit
<code>Authorization: Bearer …</code> header always wins — critical for Electron
(which injects tokens via <code>onBeforeSendHeaders</code>) and any context where a
stale cookie might shadow a fresh token.
</p>
<p style="margin-top:0.75rem">
<strong>Clock skew — <code>JWT_CLOCK_TOLERANCE</code>:</strong>
<code>jwt.verify</code> accepts a <code>clockTolerance</code> option (seconds).
Set <code>JWT_CLOCK_TOLERANCE=7200</code> in <code>.env</code> for local dev to
absorb skew between the Windows dev machine and the production auth server. Never
set this in production — fix the clock instead.
</p>
<p style="margin-top:0.75rem">
<strong>New-session endpoint (<code>POST /api/auth/new-session</code>):</strong>
<code>requireAuth</code>-guarded; mints a fresh independent token pair via
<code>issueTokenPair</code> for the authenticated user. Used by Electron to start
a new independent session from a running instance's heartbeat without re-login.
</p>
</div>
<div class="card">
<h3>Projects &amp; storage</h3>
<p>
Projects with member management (viewer / editor / admin roles).
Each project gets a real directory on disk at <code>storage/&lt;uuid&gt;/root/</code>
with a default <code>index.html</code> on creation. All data lives as JSON files
in <code>build/data/db/</code> — no external database. Groups exist in the data
layer but have been removed from the UI; they may return later via rokojori-auth.
</p>
</div>
<div class="card">
<h3>Project home &amp; theme system</h3>
<p>
The root <code>/</code> serves a new <code>index.html</code> entry point that
loads the <code>project-home</code> component. Logged-out users see a full-screen
dark hero; logged-in users get the active theme component dynamically imported.
The default theme (<code>project-list-default</code>) renders a dark
radial-gradient page with a Roject logo nav, per-project coloured badges (hue
from UUID via 31-hash, <code>hsl(h, 95%, 45%)</code>), Barlow 900 Italic
uppercase project names, hover-revealed delete / member buttons, and a dashed
"New Project" card that opens a name dialog on click. Theme preference is
persisted server-side in <code>user_settings.json</code> via
<code>GET / PUT /api/user/settings</code>. A second theme (Italic Neon) is
planned. <code>/edit</code> redirects to the existing <code>/editor.html</code>.
Barlow is loaded via <code>@import</code> from <code>styles.rokojori.com</code>
(weights 100, 400, 700, 900 including italics) — no Google Fonts dependency.
</p>
</div>
<div class="card">
<h3>Ecosystem — tunnel.rokojori.com</h3>
<p>
A user-based local tunneling service live at <code>tunnel.rokojori.com</code>
(source: <code>C:\rokojori\projects\web-projects\tunnel</code>). Exposes local
services — LLMs, Stable Diffusion, language servers — to authorised rokojori
users over the internet, routed through a relay server via a persistent WebSocket
agent connection. Each tunnel is owned by a rokojori account and carries metadata
(name, purpose tag, description, access mode, allowed users).
</p>
<p style="margin-top:0.75rem">
The <strong>Electron Tunnel Agent</strong> (<code>electron-agent/</code>) is a
Windows system-tray app: login via account.rokojori.com, a tunnel list with
Start / Stop / Delete / Create, and a green status dot when the agent WebSocket
2026-07-17 11:37:48 +00:00
is connected. Tokens are persisted in <code>userData/tokens.json</code> and
automatically refreshed — any 401 from the tunnel API triggers
<code>POST /api/auth/refresh</code> before retrying; if the refresh token is
also expired the app returns to the login screen. A logout button in the window
header and the tray menu clears tokens and stops all agents. The
<code>TunnelAgent</code> uses a <code>getToken()</code> getter instead of a
static token so every WebSocket reconnect picks up the current access token.
</p>
<p style="margin-top:0.75rem">
The relay uses a three-message streaming protocol over the agent WebSocket:
<code>res_start</code> (status + headers), <code>res_data</code> (base64 chunk),
<code>res_end</code> — so SSE responses from local LLMs stream through the relay
without buffering. Roject integrates via a <em>Browse Tunnels</em> button in
<code>rojo-settings-panel</code> and forwards LLM chat through the selected
tunnel. The TUNNEL_SERVER_URL env var controls the target in all environments.
Phase 2 (multi-user allowed list enforcement, public access mode) is still in progress.
</p>
</div>
<div class="card">
<h3>Ecosystem — styles.rokojori.com</h3>
<p>
A shared asset hosting service for all rokojori projects, live at
<code>styles.rokojori.com</code>. Currently serves self-hosted fonts downloaded
from Google Fonts on demand via a <code>GET /get-font</code> public endpoint that
returns a dynamic CSS file with <code>@font-face</code> rules. Font files are
stored as <code>storage/fonts/&lt;family&gt;/&lt;weight&gt;.woff2</code> and
served statically. CORS is restricted to <code>*.rokojori.com</code> origins.
Access to the management UI (<code>/list-fonts</code>, <code>/add-fonts</code>)
is gated by the shared JWT cookie via <code>requireAccess</code> middleware
(role: admin, or role: user + product: styles / premium).
</p>
<p style="margin-top:0.75rem">
Planned: shared HTML components / Web Components, CSS presets, and binary assets
(images, sounds, video) — making styles.rokojori.com the single place to manage
any reusable front-end resource across the rokojori ecosystem.
</p>
</div>
<div class="card">
<h3>Editor</h3>
<p>
A full editor page (<code>/editor.html</code>) with a 3-panel resizable layout
(Left / Center / Right). Each panel holds one or more sections side by side;
each section holds a <code>&lt;tab-container&gt;</code> with drag-and-drop tabs.
The Left panel shows the file tree (create, rename, delete).
A <code>FileEditorRegistry</code> routes files to the correct panel by extension:
<code>.page</code><code>page-editor-panel</code> (structured page editor,
see card below); all other text formats → <code>code-panel</code>
(CodeMirror 5, syntax highlighting, dark theme). Godot file types
(<code>.gd</code>, <code>.gdshader</code>, <code>.gdshaderinc</code>,
<code>.tscn</code>, <code>.tres</code>, <code>.res</code>) are pre-registered.
An optional project-level <code>workspace/editor/file-editors.json</code>
overrides the defaults.
</p>
<p style="margin-top:0.75rem">
Clicking a file in the file tree opens it with smart panel targeting: if the file
is already open in any panel, that panel's tab is focused. Otherwise, the next
available unpinned non-dirty editor of the correct type is used; a new panel is
created in the active section if none qualifies. Pinned panels are never overwritten.
Right-clicking a file shows a context menu titled with the filename (truncated to
20 chars with a leading <code>...</code> if longer). Files with registered
alternative editors show an <em>Open &gt;</em> submenu. Directories show
<em>As Root Directory</em> to set a sub-root without double-clicking.
Open-directory state is preserved across tree refreshes.
</p>
<p style="margin-top:0.75rem">
<strong>Tab container context menu:</strong>
<em>Add &gt;</em> opens a panel-type submenu. <em>Duplicate</em> clones the active
tab. <em>Split &gt;</em> offers <em>↔ Horizontally</em> (new section side by side)
and <em>↕ Vertically</em> (new tab-container stacked inside the same section).
<em>Close Container</em> removes the container and its adjacent resize handle; it is
hidden when the container is the last one in its slot. If any tab has unsaved changes,
a confirm dialog (<em>Don't Close</em> / <em>Close Without Saving</em>) appears first.
Tabs can also be closed by middle-mouse click.
</p>
<p style="margin-top:0.75rem">
<strong>Panel interfaces</strong><code>source/editor/editor-panel.ts</code> defines
two interfaces for Web Component panels. <code>EditorPanel</code> (all panels) requires
<code>__interfaces__: string[]</code> and <code>addContextMenuEntries()</code>.
<code>FileEditorPanel extends EditorPanel</code> (file-editing panels only) adds
<code>hasUnsavedChanges(): boolean</code>, replacing the old <code>TabEntry.dirty</code>
flag. Each interface has a companion Definition class
(<code>EditorPanelDefinition</code>, <code>FileEditorPanelDefinition</code>) with a
<code>static readonly type</code> string; use <code>implementsInterface(el, Def)</code>
for runtime checks.
</p>
<p style="margin-top:0.75rem">
<strong>EditorConsole</strong> (<code>source/editor/EditorConsole.ts</code>) is a
standalone singleton — separate from <code>Editor</code> — that holds a capped ring of
500 <code>ConsoleMessage</code> objects (<code>text</code>, <code>type</code>:
<code>'info'|'error'|'hint'</code>, <code>timestamp</code>) and dispatches them via
<code>onMessage: EventSlot</code>. <code>editor-shell</code> bridges
<code>Editor.onFileTypeUnknown</code> to <code>EditorConsole</code> and shows the
latest message in a <code>.es-info</code> header element (opacity 0→1, auto-hides after
5 s; portrait: fixed bottom bar). The <code>console-panel</code> tab renders the full
message log using custom elements (<code>conp-header</code>, <code>conp-list</code>,
<code>conp-entry</code>, <code>conp-time</code>, <code>conp-text</code>) and is
available from the tab container <em>Add &gt;</em> menu.
</p>
<p style="margin-top:0.75rem">
<strong><code>openDocumentIn</code></strong> derives <code>editorTag</code> from
<code>panelElement.tagName.toLowerCase()</code> — not from
<code>FileEditorRegistry</code>. The caller already chose the target panel; the
dispatch must honour that choice so alternative editors (e.g. code-panel opening
a .page file) receive and display the document correctly.
</p>
<p style="margin-top:0.75rem">
<strong>Layout persistence</strong> — the full tab tree is saved per project per
device to <code>.roject/layout-&lt;deviceId&gt;.json</code> inside the project
directory. Remote projects write to
<code>storage/&lt;id&gt;/root/.roject/</code>; local Electron projects write to
<code>&lt;localRoot&gt;/.roject/</code>; remote projects opened via the Electron
proxy use the centralized <code>build/data/storage/layouts/</code> keyed by
device + remote project ID. <code>deviceId</code> is a UUID in
<code>localStorage</code> — Firefox, Chrome, and Electron each get a distinct ID
automatically. The serialized format records
<code>panels → sections → tabContainers → tabs</code>, with each tab carrying
<code>{ id, label, panelType, tag, openFile }</code>.
<code>FileEditorPanel</code> was extended with
<code>getCurrentFile(): string | null</code> (implemented by <code>code-panel</code>
and <code>page-editor-panel</code>) so the serializer can read the open file from
each panel element. On editor load, <code>editor-shell</code> restores the saved
structure; if no layout is found it falls back to the hard-coded default
(file tree left, page editor centre). <code>.roject/</code> is filtered out of
both the remote and local file tree listings server-side.
<code>GET / PUT /api/layout</code> in <code>source/server/routes/layout.ts</code>.
</p>
</div>
<div class="card">
<h3>Page Editor Panel (<code>page-editor-panel</code>)</h3>
<p>
A structured authoring editor for <code>.page</code> files — Roject's custom
documentation format. A <code>.page</code> file is a full HTML document whose
<code>&lt;body&gt;</code> must follow a fixed structure:
</p>
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">&lt;page-header&gt;&lt;/page-header&gt;
&lt;page-root&gt;
&lt;page-block&gt;
&lt;page-area&gt;&lt;/page-area&gt;
&lt;/page-block&gt;
&lt;/page-root&gt;
&lt;page-footer&gt;&lt;/page-footer&gt;</pre>
<p>
The <code>&lt;head&gt;</code> may contain links to CSS/JS asset bundles;
these will load inside the editor iframe.
Pages that do not follow the required body structure fall back to
<code>code-panel</code> for plain-text editing.
</p>
<h4 style="margin-top:1rem">Validation</h4>
<p>
Format validation is handled by a single replaceable function
(<code>validatePageFormat(doc): boolean</code>) in
<code>source/components/page-editor-panel/page-editor-panel.ts</code>.
<strong>Currently a placeholder that always returns <code>true</code></strong>
— swap for real DOM inspection when the format is stable.
Required structure when implemented: exactly one <code>&lt;page-header&gt;</code>,
one <code>&lt;page-root&gt;</code>, and one <code>&lt;page-footer&gt;</code>
as direct children of <code>&lt;body&gt;</code>; no other elements at that level.
<code>&lt;page-root&gt;</code> may be empty or contain any number of
<code>&lt;page-block&gt;</code> children.
</p>
<h4 style="margin-top:1rem">Auto-template for new/empty files</h4>
<p>
When a <code>.page</code> file is opened and its content is empty (or all
whitespace), format validation is bypassed and the standard template is injected
automatically. The document is marked dirty so the user must save to persist the
initial structure. This is the intended flow for newly created <code>.page</code>
files — no "Init" button exists.
</p>
<h4 style="margin-top:1rem">JS safety — iframe sandbox</h4>
<p>
The editor iframe uses <code>sandbox="allow-same-origin"</code>, which blocks
script execution inside the rendered page. This is intentional: user-authored
<code>&lt;script&gt;</code> tags must not run in the editor context.
To change sandboxing behaviour, adjust the <code>sandbox</code> attribute on
<code>.pep-frame</code> in <code>page-editor-panel.ts</code>.
</p>
<h4 style="margin-top:1rem">Editor CSS injection</h4>
<p>
Block and area layout styles (<code>page-block</code>, <code>page-area</code>,
etc.) are injected into the live iframe <code>&lt;head&gt;</code> after load via
a <code>&lt;style id="pep-editor-injected"&gt;</code> element. This element is
never part of <code>srcdoc</code> and is stripped from the captured HTML before
saving, keeping the saved file clean. To update the editor-side layout styles,
edit the <code>PEP_EDITOR_STYLES</code> constant in
<code>page-editor-panel.ts</code>.
</p>
<h4 style="margin-top:1rem">Block registry</h4>
<p>
Available block templates are defined in a static table
(<code>PAGE_BLOCK_REGISTRY</code>) in
<code>source/components/page-editor-panel/page-editor-panel.ts</code>.
Each entry has a <code>name</code>, optional CSS-based <code>preview</code>
markup (a small layout sketch), and an <code>html</code> snippet inserted into
<code>&lt;page-root&gt;</code> when the block is added. Blocks without a preview
show their name as a text label.
</p>
<p style="margin-top:0.5rem">Current standard blocks:</p>
<ul style="line-height:1.9;margin-top:0.5rem">
<li><strong>Full Width</strong> — one <code>&lt;page-area&gt;</code> spanning
the full container width.</li>
<li><strong>Two Columns</strong> — two equal <code>&lt;page-area&gt;</code>
elements side by side on landscape; stacked (left above right) on portrait
via a CSS media query.</li>
</ul>
<h4 style="margin-top:1rem">Toolbar modes</h4>
<p>
Two icon buttons in <code>.pep-toolbar</code> (pushed to the right by a
<code>.pep-toolbar-sep</code> spacer) switch between modes:
</p>
<ul style="line-height:1.9;margin-top:0.5rem">
<li><strong>Blocks mode</strong> — a horizontal scrollable list of block
templates. Each entry shows a small CSS layout preview (or text name) above
the block name. Clicking a block appends it to <code>&lt;page-root&gt;</code>.</li>
<li><strong>Areas mode</strong> — a formatting toolbar that acts on the current
selection inside a <code>&lt;page-area&gt;</code>. See rich text below.</li>
</ul>
<h4 style="margin-top:1rem">Rich text editing in areas</h4>
<p>
Each <code>&lt;page-area&gt;</code> inside the iframe is
<code>contenteditable</code>. Formatting is applied via the Selection / Range
API — <strong>no <code>execCommand</code></strong>. The shared helper
<code>wrapSelection(range, tagName, attributes?)</code> in
<code>page-editor-panel.ts</code> uses <code>Range.extractContents()</code>
to pull out the selected fragment, wraps it in the target element, and
re-inserts via <code>Range.insertNode()</code>. This handles both fully-contained
elements (wrapped outside) and boundary intersections (text nodes split
automatically by the Range API, wrapped inside the outer element).
Semantic tags are preferred: <code>&lt;b&gt;</code>, <code>&lt;i&gt;</code>,
<code>&lt;u&gt;</code>; <code>&lt;span style="..."&gt;</code> for colour /
font-family.
</p>
<p style="margin-top:0.5rem">
<strong>Known limitation (future work):</strong> after wrapping, adjacent
identical elements (e.g. two consecutive <code>&lt;b&gt;</code> tags) are not
merged. A cleanup pass is not implemented yet.
</p>
</div>
<div class="card">
<h3>CodeMirror syntax highlighting</h3>
<p>
Vendor modes bundled: <code>clike</code> (C/C++/Java/GLSL/GDShader),
<code>python</code> (GDScript), <code>shell</code>, <code>yaml</code>.
Extension → mode mappings in <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>
<p style="margin-top:0.75rem">
<strong>Custom C# mode</strong> is built on a browser-only lexer stack in
<code>source/components/code-panel/</code> (no <code>library-ts</code> dependency —
avoids the <code>ts-node</code> / browser-extension import conflict):
</p>
<ul style="line-height:1.9;margin-top:0.75rem">
<li><code>BrowserLexer.ts</code> — zero imports. <code>BrowserMatcher</code> uses
sticky regexes (<code>/y</code> flag + <code>lastIndex</code>) for positional
matching. <code>cLikeLexer()</code> factory returns a full C-like token set.</li>
<li><code>CodeMirrorLexerMode.ts</code> — wraps a <code>BrowserLexer</code> into a
CodeMirror 5 mode. Supports multi-line blocks (start/end regex + CSS class, state
persisted across lines) and named keyword sets that override the base CSS class for
matching token types at runtime. <code>refresh(cm)</code> forces re-tokenization.</li>
<li><code>CSharpMode.ts</code><code>csharpMode</code> registered as
<code>'rokojori-cs'</code>; ~70 C# keywords mapped from <code>CWORD</code>
<code>keyword</code>.</li>
</ul>
</div>
<div class="card">
<h3>Project access control</h3>
<p>
<code>source/server/projectAccess.ts</code> is the single point of truth for
ownership and membership checks. <code>GET /api/projects</code> filters to
projects the requesting user owns or is a member of. Every file route
(tree, read, write, rename, delete) calls <code>checkAccess()</code> before
touching the filesystem. Delete and member-management routes verify ownership.
Members are currently stored by email address (Option B interim);
<code>memberMatchesUser()</code> is the one line to change when migrating to
user-ID-based lookup via rokojori-auth.
</p>
</div>
<div class="card">
<h3>AI chat &amp; utilities</h3>
<p>
A <code>rojo-chat-panel</code> provides a streaming AI chat interface backed by
LangChain + OpenAI-compatible models. A reusable <code>&lt;confirm-dialog&gt;</code>
component replaces browser <code>confirm()</code> for destructive actions.
An <code>EmailService</code> facade (<code>source/server/email/</code>) provides
a <code>sendEmail()</code> entry point backed by a swappable <code>EmailSender</code>
interface; the default is <code>SMTPEmailSender</code> (Nodemailer).
<code>EmailService.reportEmail</code> is a static field holding the admin
notification address; Roject sends emails on server startup and on each
verified deploy request.
</p>
</div>
<div class="card">
<h3>Electron desktop app</h3>
<p>
Roject runs as a standalone Windows desktop application. The Express server starts
in-process inside Electron's main process. A custom login window calls the auth
API directly; tokens are stored in <code>userData/tokens.json</code> and re-used
across sessions. All requests to <code>localhost</code> have
<code>Authorization: Bearer</code> injected automatically via
<code>session.webRequest.onBeforeSendHeaders</code>. Run with
<code>npm run electron:dev</code>.
</p>
<p style="margin-top:0.75rem">
<strong>Electron token updater:</strong> runs in <code>electron/main.ts</code>.
Reads the access token's <code>exp</code> directly from the JWT payload (token is
held in the main process, not behind an <code>httpOnly</code> cookie). Uses a
server clock offset derived from the <code>Date</code> response header of the first
auth-server call (<code>_serverClockOffsetMs</code>) for all expiry comparisons.
Refreshes proactively when within 15 min of expiry; checks every 5 min. Network
errors on refresh are silently retried next tick; auth failures (expired/revoked
refresh token) close the main window and show the login screen. Multiple Electron
instances each run their own updater with their own independent session — no
single-instance lock.
</p>
<p style="margin-top:0.75rem">
<strong>Heartbeat session sharing:</strong> a running instance writes
<code>{ accessToken, timestamp }</code> to <code>userData/session-heartbeat.json</code>
every 10 s. A newly-starting instance reads it on launch; if ≤ 30 s old, it calls
<code>POST /api/auth/new-session</code> (requireAuth-guarded) to mint its own
independent token pair — skipping the login screen. If the heartbeat is stale or the
call fails, it falls through to normal login. Replaces the old plaintext
<code>last-password.txt</code> auto-login.
</p>
<p style="margin-top:0.75rem">
<strong>Local filesystem access:</strong> when <code>ROJECT_ELECTRON=true</code>,
the server mounts <code>/api/local/</code> routes backed by Node.js <code>fs</code>
(no project storage). The project-list-default <em>This PC</em> tab lets the user
pick any host directory; the editor opens with a <code>localRoot</code> URL param
and all file-tree operations route through <code>/api/local/</code>.
</p>
<p style="margin-top:0.75rem">
<strong>Remote project proxy:</strong> <code>/api/remote/**</code> is a catch-all
that strips the prefix, prepends <code>/api</code>, and forwards the request to
<code>roject.rokojori.com</code> over HTTPS. The <code>Authorization</code> header
is already injected by <code>onBeforeSendHeaders</code>, so no extra IPC channel is
needed. The project-list-default <em>Online</em> tab fetches from
<code>/api/remote/projects</code> and opens the editor with a <code>remoteProject</code>
URL param.
</p>
</div>
<div class="card">
<h3>Deployment &amp; CI</h3>
<p>
Live at <code>https://roject.rokojori.com</code> on Server A. nginx handles TLS
and reverse-proxies to a Node.js process managed by systemd. Auto-deploy on push
to <code>main</code> via a Gitea webhook calling <code>POST /api/deploy</code>
(HMAC-SHA256 verified). The deploy script runs detached so it survives the
<code>systemctl restart</code> that kills the parent process.
</p>
</div>
</section>
<section>
<h2>What's next</h2>
<div class="card">
<p>
Current tasks, known bugs, and longer-horizon features are tracked on the
<a href="../boards/index.html">Boards</a>.
</p>
</div>
</section>
<section>
<h2>Technical Implementation</h2>
<div class="card">
<h3>Backend</h3>
<p>
Node.js + Express, TypeScript compiled on the fly with <code>ts-node</code>.
Auth is handled by <code>rokojori-auth</code>; Roject verifies the shared
<code>accessToken</code> JWT cookie using <code>jsonwebtoken</code> +
<code>cookie-parser</code>. All entity IDs are UUIDs via
<code>crypto.randomUUID()</code>. Start the server with <code>npm start</code>.
</p>
<div class="tags">
<span class="tag">Node.js</span>
<span class="tag">Express</span>
<span class="tag">ts-node</span>
<span class="tag">jsonwebtoken</span>
<span class="tag">cookie-parser</span>
<span class="tag">UUID IDs</span>
</div>
</div>
<div class="card">
<h3>Frontend — Editor Singleton &amp; Client/Server Split</h3>
<p>
The <code>Editor</code> singleton (<code>src/editor/Editor.ts</code>) is the
central hub of the frontend — it owns all open document state, the
<code>FileEditorRegistry</code>, and the five events that panels and tab containers
subscribe to (<code>onDocumentOpened</code>, <code>onDocumentDirty</code>,
<code>onDocumentSaved</code>, <code>onFilesChanged</code>,
<code>onFileTypeUnknown</code>). For the full event reference and the TypeScript
compilation split between client and server, see the
<a href="../reference/editor-singleton/index.html">Editor Singleton reference</a>.
</p>
</div>
<div class="card">
<h3>Frontend</h3>
<p>
Vanilla HTML, raw CSS (no Tailwind, no framework). Every UI component is a
custom element with its own <code>.ts</code> and <code>.css</code> file in
<code>source/components/&lt;name&gt;/</code>. CSS uses the element tag as root
selector with <code>display: block</code>. TypeScript compiles to
<code>build/app/components/</code> via <code>tsconfig.client.json</code>
(<code>module: ESNext</code>, no bundler). HTML pages live in
<code>source/pages/</code> and are copied to <code>build/app/</code> by
<code>scripts/copy-pages.js</code>. Build with <code>npm run build</code>.
</p>
<div class="tags">
<span class="tag">Web Components</span>
<span class="tag">raw CSS</span>
<span class="tag">module: ESNext</span>
<span class="tag">no bundler</span>
<span class="tag">tsc --build</span>
</div>
</div>
<div class="card">
<h3>Shared Library</h3>
<p>
A personal TypeScript library lives as a git submodule at
<code>source/library-ts/</code>. It has two parts: <code>browser/</code>
(DOM-capable) and <code>node/</code> (Node.js only). The browser part is
compiled separately via TypeScript project references into
<code>build/app/library-ts/browser/</code>. The node part is included by
<code>tsconfig.ts-node.json</code>.
</p>
<p style="margin-top:0.75rem">
<strong>Import extension convention:</strong> all relative imports in
<code>browser/</code> use explicit <code>.js</code> extensions
(e.g. <code>from "../events/EventSlot.js"</code>). Roject's client code is served
as unbundled browser-native ESM — no bundler resolves paths at build time, so the
browser fetches each module by its literal URL. Extension-less imports 404 because
Express static only serves the actual <code>.js</code> files. TypeScript with
<code>moduleResolution: "bundler"</code> accepts <code>.js</code> extensions in
source even when the source file is <code>.ts</code>.
</p>
<div class="tags">
<span class="tag">git submodule</span>
<span class="tag">source/library-ts/</span>
<span class="tag">project references</span>
<span class="tag">composite: true</span>
<span class="tag">.js extensions required</span>
</div>
</div>
</section>
<footer>
Roject &mdash; developer documentation
</footer>
</div>
<script>var NAV_ROOT = '../';</script>
<script src="../_assets_/nav-data.js"></script>
<script src="../_assets_/nav.js"></script>
</body>
</html>