Commit 6d0baa5388bc8b580880e00cb31b54de7bdbb36a
Commits[COMMIT BEGIN]commit 6d0baa5388bc8b580880e00cb31b54de7bdbb36a Author: 0x4248 <[email protected]> Date: Sun Jan 11 22:36:02 2026 +0000 Orion: init Signed-off-by: 0x4248 <[email protected]> diff --git a/lab/orion/commands/echo.py b/lab/orion/commands/echo.py new file mode 100644 index 0000000..2c3874e --- /dev/null +++ b/lab/orion/commands/echo.py @@ -0,0 +1,19 @@ +from fastapi import Request +from core.registry import registry +from core.commands import Command +from core import page + +def echo(request: Request, text: str = ""): + if not text: + return page.message(request, "ECHO", "No text provided") + return page.message(request, "ECHO", text) + +registry.register(Command( + name="echo", + handler=echo, + summary="Echo back text", + mode="both", + form_fields=[ + {"name": "text", "type": "text"} + ] +)) diff --git a/lab/orion/commands/system/open.py b/lab/orion/commands/system/open.py new file mode 100644 index 0000000..726db9d --- /dev/null +++ b/lab/orion/commands/system/open.py @@ -0,0 +1,31 @@ +from fastapi import Request +from fastapi.responses import RedirectResponse +from core.registry import registry +from core.commands import Command +from core import page + + +def open_page(request: Request, name: str = ""): + if not name: + return page.message( + request, + "OPEN", + "usage: open <page>" + ) + + cmd = registry.get(name) + if not cmd or not cmd.supports_ui(): + return page.message( + request, + "OPEN", + f"No UI page for: {name}" + ) + + return RedirectResponse(f"/command/{name}", status_code=303) + +registry.register(Command( + name="open", + handler=open_page, + summary="Open a UI page", + mode="cli", +)) diff --git a/lab/orion/commands/testing/demo.py b/lab/orion/commands/testing/demo.py new file mode 100644 index 0000000..0373122 --- /dev/null +++ b/lab/orion/commands/testing/demo.py @@ -0,0 +1,104 @@ +from fastapi import Request, UploadFile +from core.registry import registry +from core.commands import Command +from core import page + + +def hello_cli(request: Request, *args): + name = args[0] if args else "world" + return page.message(request, "HELLO (CLI)", f"hello {name}") + +registry.register(Command( + name="hello", + handler=hello_cli, + summary="CLI-only hello", + mode="cli", +)) + +def ui_only(request: Request, text: str = ""): + return page.message( + request, + "UI ONLY", + f"You typed: {text or '(empty)'}" + ) + +registry.register(Command( + name="uionly", + handler=ui_only, + summary="UI-only command", + mode="ui", + form_fields=[ + {"name": "text", "type": "text"}, + ], +)) + +def both(request: Request, value: str = "", extra: str = ""): + return page.message( + request, + "BOTH", + f"value = {value or '(none)'} | extra = {extra or '(none)'}" + ) + +registry.register(Command( + name="both", + handler=both, + summary="CLI + UI command", + mode="both", + form_fields=[ + {"name": "value", "type": "text"}, + {"name": "extra", "type": "text"} + ], +)) + + +def both_plus( + request: Request, + value: str = "", + enable: str | None = None, + payload: UploadFile | None = None, +): + flags = [] + if enable: + flags.append("enable") + + file_info = "(none)" + + file_data = payload.file.read() if payload else None + if payload: + file_info = payload.filename + + return page.message( + request, + "BOTH+", + f"value={value}\nflags={flags}\nfile={file_data}" + ) + + +registry.register(Command( + name="both_plus", + handler=both_plus, + summary="CLI + UI + checkbox + file", + mode="both", + form_fields=[ + {"name": "value", "type": "text"}, + {"name": "enable", "type": "checkbox"}, + {"name": "payload", "type": "file"}, + ], +)) + + +def file_demo(request: Request, upload: UploadFile | None = None): + if not upload: + return page.message( + request, + "FILE DEMO", + error="No file uploaded" + ) + + data = upload.file.read() + return page.message( + request, + "FILE DEMO", + f"Uploaded file: {upload.filename} ({len(data)} bytes)" + ) + diff --git a/lab/orion/core/auth.py b/lab/orion/core/auth.py new file mode 100644 index 0000000..9d8f6f1 --- /dev/null +++ b/lab/orion/core/auth.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Request, Form +from fastapi.responses import RedirectResponse +from core import page as p + +router = APIRouter() + +# ---- users DB (placeholder) ---- + +users = { + "admin": { + "password": "admin", + "roles": ["admin", "user"], + "email": "admin@orion", + "telephone": "101", + } +} + +# ---- middleware ---- + +async def auth_middleware(request: Request, call_next): + if request.url.path.startswith("/login"): + return await call_next(request) + + user = request.cookies.get("user") + if user in users: + return await call_next(request) + + return RedirectResponse(url="/login", status_code=303) + +# ---- routes ---- + [email protected]("/login") +async def login_page(request: Request): + return p.form( + request, + title="LOGIN", + action="/login", + fields=[ + {"name": "username"}, + {"name": "password", "type": "password"}, + ], + msg="Please login with your credentials.", + ) + [email protected]("/login") +async def login_submit( + request: Request, + username: str = Form(...), + password: str = Form(...) +): + user = users.get(username) + if user and user["password"] == password: + response = RedirectResponse(url="/", status_code=303) + response.set_cookie("user", username, httponly=True) + return response + + return p.form( + request, + title="LOGIN", + action="/login", + fields=[ + {"name": "username"}, + {"name": "password", "type": "password"}, + ], + error="Invalid username or password.", + ) + [email protected]("/logout") +async def logout(): + response = RedirectResponse("/login", status_code=303) + response.delete_cookie("user") + return response diff --git a/lab/orion/core/commands.py b/lab/orion/core/commands.py new file mode 100644 index 0000000..6004c39 --- /dev/null +++ b/lab/orion/core/commands.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass, field +from typing import Callable, Dict, List, Optional, Any, Literal + +Mode = Literal["cli", "ui", "both"] + +@dataclass +class Command: + name: str + handler: Callable[..., Any] + summary: str = "" + mode: Mode = "cli" + form_fields: List[Dict[str, Any]] = field(default_factory=list) + + def supports_cli(self) -> bool: + return self.mode in ("cli", "both") + + def supports_ui(self) -> bool: + return self.mode in ("ui", "both") diff --git a/lab/orion/core/layout.py b/lab/orion/core/layout.py new file mode 100644 index 0000000..38f8de1 --- /dev/null +++ b/lab/orion/core/layout.py @@ -0,0 +1,172 @@ +from fastapi import Request + +STYLE = """ +<style> +html, body { + background: #000000; + color: #ffffff; + margin: 0; + padding: 0.5em; +} + +* { + font-size: 16px; + font-family: "Departure Mono", monospace; +} + +body { + max-width: 800px; + margin: auto; +} + +a { + color: #4da6ff; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.header { + margin-bottom: 0.5em; +} + +hr { + border: none; + border-top: 1px solid #444; + margin: 0.5em 0; +} + +pre { + white-space: pre-wrap; +} + +input, textarea { + background: #000000; + color: #ffffff; + border: 1px solid #444; + font-family: monospace; + padding: 0.2em; + margin-bottom: 0.5em; +} + +input[type=submit] { + width: 100%; + color: lime; + font-weight: bold; +} + +input[type=textline] { + width: 100%; +} + +label { + font-weight: bold; + text-transform: uppercase; +} + +.error { + background-color: red; + color: white; + padding-top: 0.5em; + padding-bottom: 0.5em; + animation: pulse_error 1s; +} + +@keyframes pulse_error { + 0% { background-color: #ff8f8f; } + 100% { background-color: red; } +} + +.error strong { + text-decoration: underline; + text-align: center; +} + +textarea { + width: 100%; + height: 6em; +} + +.center { + text-align: center; +} + +hr, input, textarea { + border: 1px solid #fff; +} + +a:focus, a:hover { + background-color: #0044cc; + color: #ffffff; + border-radius: 0px; + border: none; + # dont add default html stuff + outline: none; + +} + +input:focus, textarea:focus { + outline: none; + border: 1px solid #4da6ff; +} + +input[type=submit]:focus , input[type=submit]:hover { + border: 1px solid lime; + animation: pulse 1s infinite; +} +@keyframes pulse { + 0% { border: 1px solid lime; } + 50% { border: 1px solid white; } + 100% { border: 1px solid lime; } +} +</style> +""" + +SCRIPT = """ +<script> +document.addEventListener('keydown', function(event) { + if (event.key === 'F2') { + window.location.href = '/command'; + } + + if (event.target.tagName.toLowerCase() === 'input' || event.target.tagName.toLowerCase() === 'textarea') { + return; + } + if (event.key === 'F4' || event.key === ',' || event.key === 'Escape') { + window.history.back(); + } + if (event.key === 'F1') { + const firstInput = document.querySelector('input, textarea'); + if (firstInput) { + firstInput.focus(); + } + } + + if (event.key === '`') { + const homeLink = document.querySelector('a[href="/"]'); + if (homeLink) { + homeLink.focus(); + } + } + +}); +</script> +""" + + +def layout(request: Request, title: str, buttons: list[tuple[str, str]], header_html: str | None, body_html: str) -> str: + nav = " ".join(f"[<a href='{href}'>{label}</a>]" for label, href in buttons) + header = header_html if header_html is not None else f"<div class='header'><strong>ORION SYSTEM:</strong> {title}<br>{nav}</div>" + return f""" + <html> + <head>{STYLE}</head> + <body> + {header} + <hr/> + {body_html} + </body> + {SCRIPT} + </html> + """ diff --git a/lab/orion/core/page.py b/lab/orion/core/page.py new file mode 100644 index 0000000..4f5223a --- /dev/null +++ b/lab/orion/core/page.py @@ -0,0 +1,258 @@ +from fastapi import Request +from fastapi.responses import HTMLResponse +from core.layout import layout +from typing import Iterable, Sequence + + +# ============================================================================ +# Core render primitive +# ============================================================================ + +def render( + request: Request, + *, + title: str, + body: str, + buttons: Sequence[tuple[str, str]] | None = None, + header: str | None = None, +) -> HTMLResponse: + """ + Lowest-level page renderer. + Everything funnels through this. + """ + return HTMLResponse(layout(request, title, buttons, header, body)) + + +# ============================================================================ +# Navigation +# ============================================================================ + +DEFAULT_NAV: list[tuple[str, str]] = [ + ("HOME <span style='color:grey;'>`</span>", "/"), + ("RUN <span style='color:grey;'>F2</span>", "/command"), + ("FORMS", "/search_forms"), + ("LOGOUT", "/logout"), + ("EXIT <span style='color:grey;'>F4</span>", "/exit"), +] + + +def with_nav( + buttons: Sequence[tuple[str, str]] | None = None, +) -> Sequence[tuple[str, str]]: + return buttons if buttons is not None else DEFAULT_NAV + + +# ============================================================================ +# Simple pages +# ============================================================================ + +def static( + request: Request, + title: str, + html: str, + *, + buttons: Sequence[tuple[str, str]] | None = None, +): + return render( + request, + title=title, + body=html, + buttons=with_nav(buttons), + ) + + +def message( + request: Request, + title: str, + text: str = "", + *, + error: str | None = None, + ok_href: str = "javascript:history.back()", + center: bool = True, + buttons: Sequence[tuple[str, str]] | None = None, +): + err = ( + f"<pre class='error'><strong>ERROR:</strong>\n{error}</pre>" + if error + else "" + ) + + body = f""" + {err} + <div class='{'center' if center else ''}'> + <pre>{text}</pre> + <a href='{ok_href}' autofocus>[ OK ]</a> + </div> + """ + + return render( + request, + title=title, + body=body, + buttons=with_nav(buttons), + ) + + +# ============================================================================ +# Menus / lists +# ============================================================================ + +def menu( + request: Request, + title: str, + entries: Iterable[tuple[str, str]], + *, + buttons: Sequence[tuple[str, str]] | None = None, +): + lines = [] + first = True + for label, href in entries: + if first: + lines.append(f"<a href='{href}' autofocus>[{label}]</a>") + first = False + else: + lines.append(f"<a href='{href}'>[{label}]</a>") + + body = "<pre>" + "\n".join(lines) + "</pre>" + + return render( + request, + title=title, + body=body, + buttons=with_nav(buttons), + ) + + +def sectioned_menu( + request: Request, + title: str, + sections: Iterable[tuple[str, Iterable[tuple[str, str]]]], + *, + buttons: Sequence[tuple[str, str]] | None = None, +): + lines = [] + for section_title, entries in sections: + lines.append(f"<strong>{section_title}</strong>") + for label, href in entries: + lines.append(f"<a href='{href}'>[{label}]</a>") + lines.append("") + + body = "<pre>" + "\n".join(lines) + "</pre>" + + return render( + request, + title=title, + body=body, + buttons=with_nav(buttons), + ) + + +# ============================================================================ +# Forms +# ============================================================================ + +def form( + request: Request, + title: str, + action: str, + fields: list[dict], + error: str | None = None, + msg: str | None = None, +): + rows = [] + + has_file = any(f.get("type") == "file" for f in fields) + enctype = "multipart/form-data" if has_file else "application/x-www-form-urlencoded" + + for f in fields: + name = f["name"] + ftype = f.get("type", "text") + + if ftype == "textarea": + rows.append( + f"<label>{name}</label><br/>" + f"<textarea name='{name}'></textarea>" + ) + + elif ftype == "checkbox": + rows.append( + f"<label>" + f"<input type='checkbox' name='{name}' /> {name}" + f"</label>" + ) + + elif ftype == "file": + accept = f.get("accept") + accept_attr = f" accept='{accept}'" if accept else "" + rows.append( + f"<label>{name}</label><br/>" + f"<input type='file' name='{name}'{accept_attr} />" + ) + + else: + rows.append( + f"<label>{name}</label><br/>" + f"<input name='{name}' type='{ftype}' />" + ) + + err = ( + f"<pre class='error'><strong>ERROR:</strong>\n{error}</pre>" + if error else "" + ) + + body = f""" + {err} + {f"<pre class='msg'>{msg}</pre>" if msg else ""} + <form method="post" action="{action}" enctype="{enctype}"> + {'<br/>'.join(rows)} + <br/><input type="submit" value="SUBMIT" /> + </form> + """ + return render( + request, + title=title, + body=body, + buttons=with_nav(), + ) + + +# ============================================================================ +# Future-proof helpers (cheap, useful) +# ============================================================================ + +def redirect_notice( + request: Request, + title: str, + text: str, + href: str, +): + """ + Message page that explains a redirect target. + Useful for permissions, warnings, deprecations. + """ + body = f""" + <pre>{text}</pre> + <a href='{href}' autofocus>[ CONTINUE ]</a> + """ + return render( + request, + title=title, + body=body, + buttons=with_nav(), + ) + + +def empty( + request: Request, + title: str = "", +): + """ + Placeholder page. + Useful during development. + """ + return render( + request, + title=title, + body="", + buttons=with_nav(), + ) diff --git a/lab/orion/core/registry.py b/lab/orion/core/registry.py new file mode 100644 index 0000000..3d36c81 --- /dev/null +++ b/lab/orion/core/registry.py @@ -0,0 +1,19 @@ +from typing import Dict, Optional, List +from .commands import Command + +class CommandRegistry: + def __init__(self): + self._cmds: Dict[str, Command] = {} + + def register(self, cmd: Command): + if cmd.name in self._cmds: + raise ValueError(f"Duplicate command: {cmd.name}") + self._cmds[cmd.name] = cmd + + def get(self, name: str) -> Optional[Command]: + return self._cmds.get(name) + + def all(self) -> List[Command]: + return list(self._cmds.values()) + +registry = CommandRegistry() diff --git a/lab/orion/main.py b/lab/orion/main.py new file mode 100644 index 0000000..3ed3e08 --- /dev/null +++ b/lab/orion/main.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI, Request +from fastapi.responses import RedirectResponse + +from pages import command +from core import auth +from core import page as p + + +import commands.system.open +# import commands.testing.demo +import commands.echo + +app = FastAPI() + + + +app.middleware("http")(auth.auth_middleware) + +app.include_router(auth.router) +app.include_router(command.router) + + [email protected]_handler(404) +async def not_found(request: Request, exc): + return p.static( + request, + "404 NOT FOUND", + "<pre class='error'>404 NOT FOUND</pre>" + "<a href='javascript:history.back()'>[ BACK ]</a>", + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000)[COMMIT END](C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.