2026-07-06 17:28:59 +00:00
|
|
|
<!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">
|
|
|
|
|
|
2026-07-12 14:14:58 +00:00
|
|
|
<div style="width:100%;height:400px;overflow:hidden;background:#000;margin-bottom:2rem;border-radius:8px;">
|
|
|
|
|
<img src="../_assets_/roject.svg" alt="Rojects"
|
2026-07-11 12:06:55 +00:00
|
|
|
style="width:100%;height:100%;object-fit:cover;object-position:center;">
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-06 17:28:59 +00:00
|
|
|
<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>
|
2026-07-14 12:34:47 +00:00
|
|
|
<h2>What it is</h2>
|
2026-07-06 17:28:59 +00:00
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
<p>
|
2026-07-11 12:06:55 +00:00
|
|
|
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,
|
2026-07-14 12:34:47 +00:00
|
|
|
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.
|
2026-07-06 17:28:59 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-07-14 12:34:47 +00:00
|
|
|
</section>
|
2026-07-06 17:28:59 +00:00
|
|
|
|
2026-07-14 12:34:47 +00:00
|
|
|
<section>
|
|
|
|
|
<h2>What exists now</h2>
|
2026-07-14 11:31:11 +00:00
|
|
|
|
|
|
|
|
<div class="card">
|
2026-07-14 12:34:47 +00:00
|
|
|
<h3>Auth & accounts</h3>
|
2026-07-14 11:31:11 +00:00
|
|
|
<p>
|
2026-07-14 12:34:47 +00:00
|
|
|
User accounts are managed by <strong>rokojori-auth</strong> at
|
|
|
|
|
<code>account.rokojori.com</code> — registration, login, JWT issuance, refresh
|
2026-07-17 11:37:48 +00:00
|
|
|
token rotation, roles, and account deletion.
|
|
|
|
|
</p>
|
|
|
|
|
<p style="margin-top:0.75rem">
|
2026-07-18 06:37:47 +00:00
|
|
|
<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>
|
2026-07-17 11:37:48 +00:00
|
|
|
defaults to <code>AUTH_HOST</code>; set it to <code>http://localhost:3001</code>
|
2026-07-18 06:37:47 +00:00
|
|
|
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.
|
2026-07-14 11:31:11 +00:00
|
|
|
</p>
|
2026-07-11 12:06:55 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-07-14 20:27:03 +00:00
|
|
|
<h3>Projects & storage</h3>
|
2026-07-11 12:06:55 +00:00
|
|
|
<p>
|
2026-07-14 20:27:03 +00:00
|
|
|
Projects with member management (viewer / editor / admin roles).
|
2026-07-14 12:34:47 +00:00
|
|
|
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
|
2026-07-14 20:27:03 +00:00
|
|
|
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>.
|
2026-07-15 18:39:06 +00:00
|
|
|
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>
|
|
|
|
|
|
2026-07-16 05:27:48 +00:00
|
|
|
<div class="card">
|
|
|
|
|
<h3>Ecosystem — tunnel.rokojori.com</h3>
|
|
|
|
|
<p>
|
tunnel: Electron agent, production deployment, streaming, Roject integration, chunk animation
— Electron Tunnel Agent app (tray, login, tunnel list, start/stop/delete/create)
— tunnel.rokojori.com deployed (nginx WS upgrade, systemd, certbot, port 3003)
— Streaming relay protocol: res_start/res_data/res_end replaces single-shot response
— Roject: browse-tunnels button in rojo-settings-panel, /tunnels/browse proxy route
— Roject: LLM chat via tunnel (TUNNEL_SERVER_URL, /v1 path, dual-source JWT)
— rojo-chat-panel: typeText() splits large relay chunks for smooth streaming appearance
— Boards, outline, and history updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 13:46:51 +00:00
|
|
|
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.
|
tunnel: Electron agent, production deployment, streaming, Roject integration, chunk animation
— Electron Tunnel Agent app (tray, login, tunnel list, start/stop/delete/create)
— tunnel.rokojori.com deployed (nginx WS upgrade, systemd, certbot, port 3003)
— Streaming relay protocol: res_start/res_data/res_end replaces single-shot response
— Roject: browse-tunnels button in rojo-settings-panel, /tunnels/browse proxy route
— Roject: LLM chat via tunnel (TUNNEL_SERVER_URL, /v1 path, dual-source JWT)
— rojo-chat-panel: typeText() splits large relay chunks for smooth streaming appearance
— Boards, outline, and history updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 13:46:51 +00:00
|
|
|
</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.
|
2026-07-16 05:27:48 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-15 18:39:06 +00:00
|
|
|
<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.
|
2026-07-11 12:06:55 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
2026-07-14 12:34:47 +00:00
|
|
|
<h3>Editor</h3>
|
2026-07-11 12:06:55 +00:00
|
|
|
<p>
|
2026-07-14 12:34:47 +00:00
|
|
|
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:
|
page-editor-panel: replace html-editor-panel with structured .page editor
Renames html-editor-panel → page-editor-panel and changes the handled
extension from .html/.htm to .page. The new editor introduces a structured
format (page-header / page-root / page-block / page-area / page-footer),
a block registry with Full Width and Two Columns templates, a two-mode
sidebar (Blocks / Areas), rich-text wrapSelection helper, auto-template
injection for empty files, sandbox="allow-same-origin" on the iframe,
and editor-style injection that is stripped before saving.
Adds a default-roject theme (dark BG, Barlow font, blue headings) embedded
as a <style> block in the page <head>, scoped to [data-theme="default-roject"]
on both <body> and <page-root>. Areas toolbar gains H1, H2, H3 buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:07:32 +00:00
|
|
|
<code>.page</code> → <code>page-editor-panel</code> (structured page editor,
|
|
|
|
|
see card below); all other text formats → <code>code-panel</code>
|
2026-07-18 06:37:47 +00:00
|
|
|
(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.
|
2026-07-31 11:17:40 +00:00
|
|
|
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 ></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.
|
2026-07-18 06:37:47 +00:00
|
|
|
</p>
|
2026-07-25 20:50:14 +00:00
|
|
|
<p style="margin-top:0.75rem">
|
|
|
|
|
<strong>Tab container context menu:</strong>
|
|
|
|
|
<em>Add ></em> opens a panel-type submenu. <em>Duplicate</em> clones the active
|
|
|
|
|
tab. <em>Split ></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>
|
2026-07-31 11:17:40 +00:00
|
|
|
<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 ></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>
|
2026-07-31 18:43:08 +00:00
|
|
|
<p style="margin-top:0.75rem">
|
|
|
|
|
<strong>Layout persistence</strong> — the full tab tree is saved per project per
|
|
|
|
|
device to <code>.roject/layout-<deviceId>.json</code> inside the project
|
|
|
|
|
directory. Remote projects write to
|
|
|
|
|
<code>storage/<id>/root/.roject/</code>; local Electron projects write to
|
|
|
|
|
<code><localRoot>/.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>
|
2026-07-18 06:37:47 +00:00
|
|
|
</div>
|
|
|
|
|
|
page-editor-panel: replace html-editor-panel with structured .page editor
Renames html-editor-panel → page-editor-panel and changes the handled
extension from .html/.htm to .page. The new editor introduces a structured
format (page-header / page-root / page-block / page-area / page-footer),
a block registry with Full Width and Two Columns templates, a two-mode
sidebar (Blocks / Areas), rich-text wrapSelection helper, auto-template
injection for empty files, sandbox="allow-same-origin" on the iframe,
and editor-style injection that is stripped before saving.
Adds a default-roject theme (dark BG, Barlow font, blue headings) embedded
as a <style> block in the page <head>, scoped to [data-theme="default-roject"]
on both <body> and <page-root>. Areas toolbar gains H1, H2, H3 buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:07:32 +00:00
|
|
|
<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><body></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"><page-header></page-header>
|
|
|
|
|
|
|
|
|
|
<page-root>
|
|
|
|
|
<page-block>
|
|
|
|
|
<page-area></page-area>
|
|
|
|
|
</page-block>
|
|
|
|
|
</page-root>
|
|
|
|
|
|
|
|
|
|
<page-footer></page-footer></pre>
|
|
|
|
|
<p>
|
|
|
|
|
The <code><head></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><page-header></code>,
|
|
|
|
|
one <code><page-root></code>, and one <code><page-footer></code>
|
|
|
|
|
as direct children of <code><body></code>; no other elements at that level.
|
|
|
|
|
<code><page-root></code> may be empty or contain any number of
|
|
|
|
|
<code><page-block></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><script></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><head></code> after load via
|
|
|
|
|
a <code><style id="pep-editor-injected"></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><page-root></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><page-area></code> spanning
|
|
|
|
|
the full container width.</li>
|
|
|
|
|
<li><strong>Two Columns</strong> — two equal <code><page-area></code>
|
|
|
|
|
elements side by side on landscape; stacked (left above right) on portrait
|
|
|
|
|
via a CSS media query.</li>
|
|
|
|
|
</ul>
|
|
|
|
|
|
2026-07-31 11:17:40 +00:00
|
|
|
<h4 style="margin-top:1rem">Toolbar modes</h4>
|
page-editor-panel: replace html-editor-panel with structured .page editor
Renames html-editor-panel → page-editor-panel and changes the handled
extension from .html/.htm to .page. The new editor introduces a structured
format (page-header / page-root / page-block / page-area / page-footer),
a block registry with Full Width and Two Columns templates, a two-mode
sidebar (Blocks / Areas), rich-text wrapSelection helper, auto-template
injection for empty files, sandbox="allow-same-origin" on the iframe,
and editor-style injection that is stripped before saving.
Adds a default-roject theme (dark BG, Barlow font, blue headings) embedded
as a <style> block in the page <head>, scoped to [data-theme="default-roject"]
on both <body> and <page-root>. Areas toolbar gains H1, H2, H3 buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:07:32 +00:00
|
|
|
<p>
|
2026-07-31 11:17:40 +00:00
|
|
|
Two icon buttons in <code>.pep-toolbar</code> (pushed to the right by a
|
|
|
|
|
<code>.pep-toolbar-sep</code> spacer) switch between modes:
|
page-editor-panel: replace html-editor-panel with structured .page editor
Renames html-editor-panel → page-editor-panel and changes the handled
extension from .html/.htm to .page. The new editor introduces a structured
format (page-header / page-root / page-block / page-area / page-footer),
a block registry with Full Width and Two Columns templates, a two-mode
sidebar (Blocks / Areas), rich-text wrapSelection helper, auto-template
injection for empty files, sandbox="allow-same-origin" on the iframe,
and editor-style injection that is stripped before saving.
Adds a default-roject theme (dark BG, Barlow font, blue headings) embedded
as a <style> block in the page <head>, scoped to [data-theme="default-roject"]
on both <body> and <page-root>. Areas toolbar gains H1, H2, H3 buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-24 19:07:32 +00:00
|
|
|
</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><page-root></code>.</li>
|
|
|
|
|
<li><strong>Areas mode</strong> — a formatting toolbar that acts on the current
|
|
|
|
|
selection inside a <code><page-area></code>. See rich text below.</li>
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
<h4 style="margin-top:1rem">Rich text editing in areas</h4>
|
|
|
|
|
<p>
|
|
|
|
|
Each <code><page-area></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><b></code>, <code><i></code>,
|
|
|
|
|
<code><u></code>; <code><span style="..."></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><b></code> tags) are not
|
|
|
|
|
merged. A cleanup pass is not implemented yet.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
<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):
|
2026-07-11 12:06:55 +00:00
|
|
|
</p>
|
2026-07-18 06:37:47 +00:00
|
|
|
<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>
|
2026-07-11 12:06:55 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-07-14 20:27:03 +00:00
|
|
|
<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>
|
|
|
|
|
|
2026-07-11 12:06:55 +00:00
|
|
|
<div class="card">
|
2026-07-14 12:34:47 +00:00
|
|
|
<h3>AI chat & utilities</h3>
|
2026-07-06 17:28:59 +00:00
|
|
|
<p>
|
2026-07-14 12:34:47 +00:00
|
|
|
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).
|
2026-07-14 20:27:03 +00:00
|
|
|
<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.
|
2026-07-11 12:06:55 +00:00
|
|
|
</p>
|
2026-07-06 17:28:59 +00:00
|
|
|
</div>
|
2026-07-11 12:06:55 +00:00
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
<div class="card">
|
2026-07-14 12:34:47 +00:00
|
|
|
<h3>Electron desktop app</h3>
|
2026-07-14 11:31:11 +00:00
|
|
|
<p>
|
2026-07-14 12:34:47 +00:00
|
|
|
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>.
|
2026-07-14 11:31:11 +00:00
|
|
|
</p>
|
2026-07-30 20:02:14 +00:00
|
|
|
<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>
|
2026-07-14 11:31:11 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-07-13 19:56:25 +00:00
|
|
|
<div class="card">
|
2026-07-14 12:34:47 +00:00
|
|
|
<h3>Deployment & CI</h3>
|
2026-07-13 19:56:25 +00:00
|
|
|
<p>
|
2026-07-14 12:34:47 +00:00
|
|
|
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.
|
2026-07-13 19:56:25 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-07-14 12:34:47 +00:00
|
|
|
</section>
|
2026-07-13 19:56:25 +00:00
|
|
|
|
2026-07-14 12:34:47 +00:00
|
|
|
<section>
|
|
|
|
|
<h2>What's next</h2>
|
2026-07-12 18:59:23 +00:00
|
|
|
|
2026-07-13 11:54:11 +00:00
|
|
|
<div class="card">
|
|
|
|
|
<p>
|
2026-07-14 12:34:47 +00:00
|
|
|
Current tasks, known bugs, and longer-horizon features are tracked on the
|
|
|
|
|
<a href="../boards/index.html">Boards</a>.
|
2026-07-13 11:54:11 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-07-06 17:28:59 +00:00
|
|
|
</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>.
|
2026-07-14 12:34:47 +00:00
|
|
|
Auth is handled by <code>rokojori-auth</code>; Roject verifies the shared
|
2026-07-13 13:37:19 +00:00
|
|
|
<code>accessToken</code> JWT cookie using <code>jsonwebtoken</code> +
|
|
|
|
|
<code>cookie-parser</code>. All entity IDs are UUIDs via
|
2026-07-14 12:34:47 +00:00
|
|
|
<code>crypto.randomUUID()</code>. Start the server with <code>npm start</code>.
|
2026-07-06 17:28:59 +00:00
|
|
|
</p>
|
|
|
|
|
<div class="tags">
|
|
|
|
|
<span class="tag">Node.js</span>
|
|
|
|
|
<span class="tag">Express</span>
|
|
|
|
|
<span class="tag">ts-node</span>
|
2026-07-13 13:37:19 +00:00
|
|
|
<span class="tag">jsonwebtoken</span>
|
|
|
|
|
<span class="tag">cookie-parser</span>
|
2026-07-06 17:28:59 +00:00
|
|
|
<span class="tag">UUID IDs</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-11 12:24:17 +00:00
|
|
|
<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
|
2026-07-14 12:34:47 +00:00
|
|
|
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
|
2026-07-11 12:24:17 +00:00
|
|
|
<a href="../reference/editor-singleton/index.html">Editor Singleton reference</a>.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-07-06 17:28:59 +00:00
|
|
|
<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
|
2026-07-12 12:01:01 +00:00
|
|
|
<code>source/components/<name>/</code>. CSS uses the element tag as root
|
2026-07-06 17:28:59 +00:00
|
|
|
selector with <code>display: block</code>. TypeScript compiles to
|
2026-07-12 12:01:01 +00:00
|
|
|
<code>build/app/components/</code> via <code>tsconfig.client.json</code>
|
2026-07-14 12:34:47 +00:00
|
|
|
(<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>.
|
2026-07-06 17:28:59 +00:00
|
|
|
</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
|
2026-07-12 12:01:01 +00:00
|
|
|
<code>source/library-ts/</code>. It has two parts: <code>browser/</code>
|
2026-07-06 17:28:59 +00:00
|
|
|
(DOM-capable) and <code>node/</code> (Node.js only). The browser part is
|
2026-07-14 12:34:47 +00:00
|
|
|
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>.
|
2026-07-06 17:28:59 +00:00
|
|
|
</p>
|
|
|
|
|
<div class="tags">
|
|
|
|
|
<span class="tag">git submodule</span>
|
2026-07-12 12:01:01 +00:00
|
|
|
<span class="tag">source/library-ts/</span>
|
2026-07-06 17:28:59 +00:00
|
|
|
<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>
|