From f871804e5b46fc6bddceea584820a4281c381eac Mon Sep 17 00:00:00 2001 From: Rokojori Date: Sun, 12 Jul 2026 13:05:53 +0200 Subject: [PATCH] history: nginx reverse proxy on Server A, directory restructure plan Co-Authored-By: Claude Sonnet 4.6 --- src/components/code-panel/code-panel.ts | 145 +++++++++ src/components/editor-shell/editor-shell.ts | 1 + .../file-tree-panel/file-tree-panel.ts | 22 +- .../html-editor-panel/html-editor-panel.ts | 1 + src/components/tab-container/tab-container.ts | 5 +- src/editor/Editor.ts | 17 +- src/editor/FileEditorRegistry.ts | 84 ++++++ src/library-ts | 2 +- src/rojos/roject.svg | 277 ++++++++++++++++++ workspace/_assets_/nav-data.js | 1 + .../07-July/09-Thursday/rojo-chat-panel.svg | 4 +- .../2026/07-July/10-Friday/CodePanel.txt | 24 +- .../2026/07-July/11-Saturday/index.html | 118 ++++++++ workspace/history/index.html | 5 + 14 files changed, 695 insertions(+), 11 deletions(-) create mode 100644 src/components/code-panel/code-panel.ts create mode 100644 src/editor/FileEditorRegistry.ts create mode 100644 src/rojos/roject.svg create mode 100644 workspace/history/2026/07-July/11-Saturday/index.html diff --git a/src/components/code-panel/code-panel.ts b/src/components/code-panel/code-panel.ts new file mode 100644 index 0000000..b9c40e5 --- /dev/null +++ b/src/components/code-panel/code-panel.ts @@ -0,0 +1,145 @@ +import { Editor } from '../../editor/Editor.js'; +import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js'; + +declare const CodeMirror: any; + +class CodePanel extends HTMLElement +{ + currentPath: string | null = null; + _pinned: boolean = false; + _cm: any = null; + _initialized = false; + _ignoreChange = false; + + connectedCallback(): void + { + if ( this._initialized ) return; + this._initialized = true; + + this.className = 'code-panel'; + this.innerHTML = ` +
+ + + + +
+
Open a file from the file tree
+ + `; + + const editorDiv = this.querySelector( '.cp-editor' ) as HTMLElement; + + this._cm = CodeMirror( editorDiv, + { + value: '', + lineNumbers: true, + theme: 'cp-dark', + indentWithTabs: false, + tabSize: 2, + indentUnit: 2, + lineWrapping: false, + readOnly: false, + } ); + + this._cm.on( 'change', () => this._onContentChanged() ); + + this.querySelector( '.cp-pin' )!.addEventListener( 'click', () => this._togglePin() ); + this.querySelector( '.cp-save' )!.addEventListener( 'click', () => this._save() ); + this.querySelector( '.cp-undo' )!.addEventListener( 'click', () => this._cm.undo() ); + this.querySelector( '.cp-redo' )!.addEventListener( 'click', () => this._cm.redo() ); + + Editor.get().onDocumentOpened.addListener( ( e ) => + { + if ( 'code-panel' !== e.editorTag ) return; + if ( ! this._pinned ) this._loadDocument( e.path, e.content ); + } ); + + document.addEventListener( 'keydown', ( e: KeyboardEvent ) => + { + if ( ! this.currentPath ) return; + if ( e.ctrlKey && e.key === 's' ) { e.preventDefault(); this._save(); } + } ); + } + + _loadDocument( path: string, content: string ): void + { + this.currentPath = path; + this._updateTabLabel( path ); + + this.querySelector( '.cp-empty' )!.setAttribute( 'style', 'display:none' ); + ( this.querySelector( '.cp-editor' ) as HTMLElement ).style.display = ''; + + this._ignoreChange = true; + this._cm.setValue( content ); + this._cm.clearHistory(); + this._cm.setOption( 'mode', this._resolveMode( path ) ); + this._ignoreChange = false; + + this._updateButtons( false ); + this._cm.refresh(); + } + + _resolveMode( filePath: string ): string + { + const name = filePath.slice( filePath.lastIndexOf( '/' ) + 1 ); + + if ( name.endsWith( '.js' ) || name.endsWith( '.json' ) ) return 'javascript'; + if ( name.endsWith( '.ts' ) ) return 'javascript'; + if ( name.endsWith( '.css' ) ) return 'css'; + if ( name.endsWith( '.html' ) || name.endsWith( '.htm' ) ) return 'htmlmixed'; + if ( name.endsWith( '.xml' ) || name.endsWith( '.svg' ) ) return 'xml'; + if ( name.endsWith( '.md' ) ) return 'markdown'; + + return 'null'; + } + + _onContentChanged(): void + { + if ( this._ignoreChange || ! this.currentPath ) return; + + const content = this._cm.getValue(); + Editor.get().markDirty( this.currentPath, content ); + this._updateButtons( true ); + } + + _togglePin(): void + { + this._pinned = ! this._pinned; + this.querySelector( '.cp-pin' )!.classList.toggle( 'pinned', this._pinned ); + } + + async _save(): Promise + { + if ( ! this.currentPath ) return; + await Editor.get().save( this.currentPath ); + this._updateButtons( false ); + } + + _updateTabLabel( path: string ): void + { + const name = path ? path.slice( path.lastIndexOf( '/' ) + 1 ) : ''; + this.dispatchEvent( new CustomEvent( 'panel:label-change', { bubbles: true, detail: { label: '📝 ' + name } } ) ); + } + + _updateButtons( dirty: boolean ): void + { + ( this.querySelector( '.cp-save' ) as HTMLButtonElement ).disabled = ! dirty; + ( this.querySelector( '.cp-undo' ) as HTMLButtonElement ).disabled = this._cm.historySize().undo < 1; + ( this.querySelector( '.cp-redo' ) as HTMLButtonElement ).disabled = this._cm.historySize().redo < 1; + } + + addContextMenuEntries( dir: ContextMenuDirectory ): void + { + if ( this.currentPath ) + { + dir.add( new ContextMenuReadOnlyEntry( dir, `Editing: ${this.currentPath}` ) ); + } + else + { + dir.add( new ContextMenuReadOnlyEntry( dir, 'No file open' ) ); + } + } +} + +customElements.define( 'code-panel', CodePanel ); diff --git a/src/components/editor-shell/editor-shell.ts b/src/components/editor-shell/editor-shell.ts index 6d08ed2..9049be9 100644 --- a/src/components/editor-shell/editor-shell.ts +++ b/src/components/editor-shell/editor-shell.ts @@ -92,6 +92,7 @@ class EditorShell extends HTMLElement { customElements.whenDefined( 'tab-container' ), customElements.whenDefined( 'file-tree-panel' ), customElements.whenDefined( 'html-editor-panel' ), + customElements.whenDefined( 'code-panel' ), this._loadLayout(), ] ); diff --git a/src/components/file-tree-panel/file-tree-panel.ts b/src/components/file-tree-panel/file-tree-panel.ts index ba6c521..eda4f4e 100644 --- a/src/components/file-tree-panel/file-tree-panel.ts +++ b/src/components/file-tree-panel/file-tree-panel.ts @@ -31,6 +31,7 @@ class FileTreePanel extends HTMLElement { this.querySelector( '[data-action="add-dir"]' )!.addEventListener( 'click', () => this.addDirectory() ); Editor.get().onFilesChanged.addListener( () => this.refresh() ); + Editor.get().onFileTypeUnknown.addListener( ( e ) => this._showTypeError( e.path ) ); await this.refresh(); } @@ -369,10 +370,27 @@ class FileTreePanel extends HTMLElement { ${this.renderNodes(n.children ?? [], depth + 1)} `; } - const isHtml = n.name.endsWith('.html'); - return `
  • ${n.name}
  • `; + return `
  • ${n.name}
  • `; }).join('') + ''; } + + _showTypeError( filePath: string ): void + { + const lastDot = filePath.lastIndexOf( '.' ); + const ext = lastDot === -1 ? '' : filePath.slice( lastDot ); + const msg = ext ? `Can't open extension "${ext}"` : `Can't open file without extension`; + + const tree = this.querySelector( '.ftp-tree' )!; + const existing = tree.querySelector( '.ftp-type-error' ); + if ( existing ) existing.remove(); + + const div = document.createElement( 'div' ); + div.className = 'ftp-type-error'; + div.textContent = msg; + tree.prepend( div ); + + setTimeout( () => div.remove(), 3000 ); + } } customElements.define('file-tree-panel', FileTreePanel); diff --git a/src/components/html-editor-panel/html-editor-panel.ts b/src/components/html-editor-panel/html-editor-panel.ts index 592103e..6e3c7ed 100644 --- a/src/components/html-editor-panel/html-editor-panel.ts +++ b/src/components/html-editor-panel/html-editor-panel.ts @@ -40,6 +40,7 @@ class HtmlEditorPanel extends HTMLElement Editor.get().onDocumentOpened.addListener( ( e ) => { + if ( 'html-editor-panel' !== e.editorTag ) return; if ( ! this._pinned ) this._loadDocument( e.path, e.content ); } ); diff --git a/src/components/tab-container/tab-container.ts b/src/components/tab-container/tab-container.ts index a0c8f9c..74b727a 100644 --- a/src/components/tab-container/tab-container.ts +++ b/src/components/tab-container/tab-container.ts @@ -56,13 +56,13 @@ class TabContainer extends HTMLElement { Editor.get().onDocumentDirty.addListener( ( e ) => { - const tab = this.tabs.find( t => t.panelType === 'html-editor' && ( t.element as any ).currentPath === e.path ); + const tab = this.tabs.find( t => ( t.element as any ).currentPath === e.path ); if ( tab ) { tab.dirty = true; this.renderBar(); } } ); Editor.get().onDocumentSaved.addListener( ( e ) => { - const tab = this.tabs.find( t => t.panelType === 'html-editor' && ( t.element as any ).currentPath === e.path ); + const tab = this.tabs.find( t => ( t.element as any ).currentPath === e.path ); if ( tab ) { tab.dirty = false; this.renderBar(); } } ); } @@ -75,6 +75,7 @@ class TabContainer extends HTMLElement { const addDir = new ContextMenuDirectory(root, 'Add'); const panelTypes = [ { label: 'HTML Editor', panelType: 'html-editor', tag: 'html-editor-panel' }, + { label: 'Code Editor', panelType: 'code-panel', tag: 'code-panel' }, { label: 'File Tree', panelType: 'file-tree', tag: 'file-tree-panel' }, { label: 'Rojo Chat', panelType: 'rojo-chat', tag: 'rojo-chat-panel' }, ]; diff --git a/src/editor/Editor.ts b/src/editor/Editor.ts index eb9585a..92d682c 100644 --- a/src/editor/Editor.ts +++ b/src/editor/Editor.ts @@ -1,10 +1,12 @@ import { EventSlot } from '../library-ts/browser/events/EventSlot.js'; +import { FileEditorRegistry } from './FileEditorRegistry.js'; export interface DocumentOpenedEvent { path: string; content: string; + editorTag: string; } export interface DocumentPathEvent @@ -30,15 +32,26 @@ export class Editor projectName: string = ''; openDocs: Map = new Map(); activeDoc: string | null = null; - + fileEditorRegistry: FileEditorRegistry = new FileEditorRegistry(); readonly onDocumentOpened: EventSlot = new EventSlot(); readonly onDocumentDirty: EventSlot = new EventSlot(); readonly onDocumentSaved: EventSlot = new EventSlot(); readonly onFilesChanged: EventSlot = new EventSlot(); + readonly onFileTypeUnknown: EventSlot = new EventSlot(); async openDocument( filePath: string ): Promise { + await this.fileEditorRegistry.load( this.projectId ); + + const editorTag = this.fileEditorRegistry.resolve( filePath ); + + if ( editorTag === null ) + { + this.onFileTypeUnknown.dispatch( { path: filePath } ); + return; + } + if ( ! this.openDocs.has( filePath ) ) { const res = await fetch( `/api/files/${this.projectId}/${filePath}` ); @@ -48,7 +61,7 @@ export class Editor this.activeDoc = filePath; const entry = this.openDocs.get( filePath )!; - this.onDocumentOpened.dispatch( { path: filePath, content: entry.content } ); + this.onDocumentOpened.dispatch( { path: filePath, content: entry.content, editorTag } ); } markDirty( filePath: string, content: string ): void diff --git a/src/editor/FileEditorRegistry.ts b/src/editor/FileEditorRegistry.ts new file mode 100644 index 0000000..adde368 --- /dev/null +++ b/src/editor/FileEditorRegistry.ts @@ -0,0 +1,84 @@ +interface RegistryEntry +{ + suffix: string; + editor: string; +} + +export class FileEditorRegistry +{ + static readonly DefaultEntries: RegistryEntry[] = + [ + { suffix: 'html', editor: 'HTMLEditorPanel' }, + { suffix: 'htm', editor: 'HTMLEditorPanel' }, + { suffix: 'js', editor: 'CodePanel' }, + { suffix: 'ts', editor: 'CodePanel' }, + { suffix: 'css', editor: 'CodePanel' }, + { suffix: 'json', editor: 'CodePanel' }, + { suffix: 'md', editor: 'CodePanel' }, + { suffix: 'txt', editor: 'CodePanel' }, + { suffix: 'svg', editor: 'CodePanel' }, + { suffix: 'xml', editor: 'CodePanel' }, + { suffix: 'yaml', editor: 'CodePanel' }, + { suffix: 'yml', editor: 'CodePanel' }, + { suffix: 'sh', editor: 'CodePanel' }, + { suffix: 'py', editor: 'CodePanel' }, + ]; + + static readonly EditorTagNames: Record = + { + 'HTMLEditorPanel': 'html-editor-panel', + 'CodePanel': 'code-panel', + }; + + projectEntries: RegistryEntry[] | null = null; + _loaded = false; + + async load( projectId: string ): Promise + { + if ( this._loaded ) return; + this._loaded = true; + + try + { + const res = await fetch( `/api/files/${projectId}/workspace/editor/file-editors.json` ); + if ( ! res.ok ) return; + const data = await res.json(); + if ( Array.isArray( data ) ) this.projectEntries = data; + } + catch {} + } + + resolve( filePath: string ): string | null + { + const filename = filePath.slice( filePath.lastIndexOf( '/' ) + 1 ); + + if ( this.projectEntries !== null ) + { + const editor = this._matchSuffix( filename, this.projectEntries ); + if ( editor !== null ) return this._resolveTag( editor ); + } + + const editor = this._matchSuffix( filename, FileEditorRegistry.DefaultEntries ); + if ( editor !== null ) return this._resolveTag( editor ); + + return null; + } + + _resolveTag( editorName: string ): string + { + return FileEditorRegistry.EditorTagNames[ editorName ] ?? editorName; + } + + _matchSuffix( filename: string, entries: RegistryEntry[] ): string | null + { + for ( const entry of entries ) + { + if ( filename.endsWith( '.' + entry.suffix ) ) + { + return entry.editor; + } + } + + return null; + } +} diff --git a/src/library-ts b/src/library-ts index 89534bb..f24cc9d 160000 --- a/src/library-ts +++ b/src/library-ts @@ -1 +1 @@ -Subproject commit 89534bb72b8d2119ca5e12da05549edbd9133945 +Subproject commit f24cc9d8b80ccbdd2a20fa66dc7cf854204d566f diff --git a/src/rojos/roject.svg b/src/rojos/roject.svg new file mode 100644 index 0000000..9d45528 --- /dev/null +++ b/src/rojos/roject.svg @@ -0,0 +1,277 @@ + + + +ROJECTROJECTROJECTROJECTROJECTCOMPUTER! ZOOM IN!COMPUTER! ZOOM IN!COMPUTER! ZOOM IN! diff --git a/workspace/_assets_/nav-data.js b/workspace/_assets_/nav-data.js index 7d82f7c..f5f7cb1 100644 --- a/workspace/_assets_/nav-data.js +++ b/workspace/_assets_/nav-data.js @@ -35,6 +35,7 @@ var NAV_DATA = { title: 'History', path: 'history/index.html', children: [ + { title: 'Saturday, 11 July 2026', path: 'history/2026/07-July/11-Saturday/index.html' }, { title: 'Friday, 10 July 2026', path: 'history/2026/07-July/10-Friday/index.html' }, { title: 'Thursday, 9 July 2026', path: 'history/2026/07-July/09-Thursday/index.html' }, { title: 'Monday, 6 July 2026', path: 'history/2026/07-July/06-Monday/index.html' }, diff --git a/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg b/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg index 0c9277d..daae5b6 100644 --- a/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg +++ b/workspace/history/2026/07-July/09-Thursday/rojo-chat-panel.svg @@ -28,8 +28,8 @@ inkscape:document-units="px" showgrid="false" inkscape:zoom="0.41781005" - inkscape:cx="-276.44141" - inkscape:cy="1626.3371" + inkscape:cx="51.458791" + inkscape:cy="1659.8452" inkscape:window-width="1920" inkscape:window-height="1017" inkscape:window-x="-8" diff --git a/workspace/history/2026/07-July/10-Friday/CodePanel.txt b/workspace/history/2026/07-July/10-Friday/CodePanel.txt index 7d734da..5f74ac1 100644 --- a/workspace/history/2026/07-July/10-Friday/CodePanel.txt +++ b/workspace/history/2026/07-July/10-Friday/CodePanel.txt @@ -1,2 +1,22 @@ -[ Code Panel ] -A new panel window will \ No newline at end of file +[ CodePanel: Feature ] +A new panel window will be added, that can read code files. +Currently there's only one file editor for HTML files. + +This should be now extended to any editor panes that can work with mainly text files: +HTMLEditorPanel and CodePanel + +When a file is opened it should be opened in the CodePanel except it is an html file, where the HTMLEditorPanel would take over. + + +[ CodePanel: UI] +Like any panel it will have a toolbar similar like the html editor panel: Pin, Undo, Redo, Save +It will than use a coding window that has line numbers and highlighted code. + +[ CodePanel: Technical implementation] +It should use the code mirror editor as editing tool. Later, language servers should be able to be added. They could +however sit on a different machine, so it should be able to confige remote language servers. + + + + + diff --git a/workspace/history/2026/07-July/11-Saturday/index.html b/workspace/history/2026/07-July/11-Saturday/index.html new file mode 100644 index 0000000..d6d6f96 --- /dev/null +++ b/workspace/history/2026/07-July/11-Saturday/index.html @@ -0,0 +1,118 @@ + + + + + + Session Summary — 11 July 2026 + + + + +
    + +
    +

    Saturday, 11 July 2026

    +

    Roject — Session Summary

    +

    + nginx reverse proxy on Server A — Gitea moved to a local port, + SSL termination handed to nginx, development.rokojori.com restored. + Project directory restructure planned for next session. +

    +
    + +
    +

    What we built

    + +
    +

    nginx reverse proxy on Server A

    +

    + Installed nginx on Server A (development.rokojori.com). Gitea was + previously running directly on port 443 with its own TLS. It was moved + to port 4444 on localhost, running plain HTTP. nginx now owns port 443 + for the domain and proxies traffic to Gitea internally. +

    +

    + The nginx server block for development.rokojori.com includes + the standard reverse-proxy headers (Host, X-Real-IP, + X-Forwarded-For, X-Forwarded-Proto) and uses + the existing Let's Encrypt certificate. +

    +

    + Gitea's app.ini was updated: PROTOCOL = http, + HTTP_PORT = 4444, ROOT_URL = https://development.rokojori.com/. + Port 4444 was closed in the IONOS firewall so Gitea is only reachable + through nginx. +

    +
    + nginx + reverse proxy + Server A + Gitea :4444 + Let's Encrypt +
    +
    + +
    + +
    +

    Key Decisions

    + +
    + nginx handles TLS, Gitea runs plain HTTP locally +

    + Gitea's built-in TLS was disabled so that nginx becomes the sole + TLS termination point. This is the standard pattern for a reverse + proxy setup: one certificate, one HTTPS endpoint, all internal + communication over plain HTTP on localhost. It also makes it + straightforward to add a second domain (roject.rokojori.com) + to the same nginx instance later. +

    +
    + +
    + Port 4444 closed at the firewall level +

    + Once Gitea dropped its own TLS, port 4444 became an unencrypted + HTTP port. Closing it in the IONOS firewall ensures Gitea is + unreachable directly from the internet and all traffic must pass + through nginx. +

    +
    + +
    + Directory restructure deferred to next session +

    + A full restructure of the project layout was planned: source/ + for all frontend + backend + locale source, build/app/ for + compiled output, build/data/db/ for JSON user data, and + build/data/storage/ for project files. The git submodule + (src/library-ts/) was committed and pushed clean before the + session ended, ready for the git mv that the restructure + requires. +

    +
    + +
    + +
    +

    Structural Changes

    +
    +

    + Server A infrastructure only — no source files changed this session.
    + nginx config: /etc/nginx/sites-available/gitea-server (new)
    + Gitea config: app.ini — PROTOCOL, HTTP_PORT, ROOT_URL updated
    + IONOS firewall: port 4444 closed +

    +
    +
    + +
    + Roject — session log — 11 July 2026 +
    + +
    + + + + + diff --git a/workspace/history/index.html b/workspace/history/index.html index f81b131..1fe01f5 100644 --- a/workspace/history/index.html +++ b/workspace/history/index.html @@ -19,6 +19,11 @@

    2026 — July

    +
    +

    Saturday, 11 July 2026

    +

    nginx reverse proxy on Server A — Gitea moved to a local HTTP port, TLS termination handed to nginx, development.rokojori.com restored. Project directory restructure planned.

    +
    +

    Friday, 10 July 2026

    CodePanel — CodeMirror 5 code editor panel; FileEditorRegistry routes files to editors by suffix with a project-level JSON override; all file types now clickable in the tree.