373 lines
19 KiB
HTML
373 lines
19 KiB
HTML
<!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 & 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, roles, and account deletion.
|
|
</p>
|
|
<p style="margin-top:0.75rem">
|
|
<strong>Token refresh:</strong>
|
|
rokojori-auth's page-level middleware (before <code>express.static</code>)
|
|
verifies the <code>accessToken</code> cookie but calls <code>next()</code> on
|
|
any error — no redirect on expiry. Transparent refresh for API calls is handled
|
|
by Roject's <code>jwtMiddleware</code>: when a <code>TokenExpiredError</code>
|
|
hits an <code>/api/</code> route and a <code>refreshToken</code> cookie is present,
|
|
it calls <code>POST AUTH_INTERNAL_HOST/api/auth/refresh</code> server-side, sets
|
|
the new cookies on the response, decodes the new JWT into <code>req.user</code>,
|
|
and continues transparently. If refresh fails, the request falls through to
|
|
<code>requireAuth</code> which returns 401. <code>AUTH_INTERNAL_HOST</code>
|
|
defaults to <code>AUTH_HOST</code>; set it to <code>http://localhost:3001</code>
|
|
in production to bypass nginx. The <code>editor-shell</code> checks
|
|
<code>GET /api/auth/me</code> on startup and redirects to <code>/</code> on 401.
|
|
</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. This means an
|
|
explicit <code>Authorization: Bearer ...</code> header always wins — critical for
|
|
Electron (which injects tokens via <code>onBeforeSendHeaders</code>), API clients,
|
|
and any context where a stale browser cookie might otherwise 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).
|
|
The env var <code>JWT_CLOCK_TOLERANCE</code> (default 0 / unset) is read as an
|
|
integer and passed as <code>clockTolerance</code> when non-zero. Set to
|
|
<code>7200</code> in <code>.env</code> for local development to absorb clock skew
|
|
between the Windows dev machine and the production auth server. Never set this in
|
|
production — if you need it there, fix the clock instead.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Projects & storage</h3>
|
|
<p>
|
|
Projects with member management (viewer / editor / admin roles).
|
|
Each project gets a real directory on disk at <code>storage/<uuid>/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 & 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
|
|
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/<family>/<weight>.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><tab-container></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:
|
|
HTML → <code>html-editor-panel</code> (iframe, contenteditable, MutationObserver,
|
|
undo/redo, Ctrl+S save); 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.
|
|
</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 & utilities</h3>
|
|
<p>
|
|
A <code>rojo-chat-panel</code> provides a streaming AI chat interface backed by
|
|
LangChain + OpenAI-compatible models. A reusable <code><confirm-dialog></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>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h3>Deployment & 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 & 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/<name>/</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>
|
|
<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>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<footer>
|
|
Roject — developer documentation
|
|
</footer>
|
|
|
|
</div>
|
|
<script>var NAV_ROOT = '../';</script>
|
|
<script src="../_assets_/nav-data.js"></script>
|
|
<script src="../_assets_/nav.js"></script>
|
|
</body>
|
|
</html>
|