From 910927993ed5c6872b59a1165387b8a5b4b712e0 Mon Sep 17 00:00:00 2001 From: buenosairesam Date: Mon, 10 Aug 2026 09:19:19 -0300 Subject: [PATCH] updates 33.1 139 --- .gitignore | 5 + CLAUDE.md | 29 ++- cfg/sample/data/__init__.py | 2 +- cfg/standalone/data/__init__.py | 2 +- cfg/standalone/data/rooms.json | 2 +- docs/graphs/README.md | 67 ++++++ docs/graphs/artery_hierarchy.dot | 28 +-- docs/graphs/artery_hierarchy.lucid.svg | 101 +++++++++ docs/graphs/artery_hierarchy.svg | 136 ++++++------ docs/graphs/cfg_gen_flow.dot | 24 +- docs/graphs/cfg_gen_flow.lucid.svg | 114 ++++++++++ docs/graphs/cfg_gen_flow.svg | 68 +++--- docs/graphs/render.sh | 69 ++++++ docs/graphs/room_layers.dot | 22 +- docs/graphs/room_layers.lucid.svg | 133 +++++++++++ docs/graphs/room_layers.svg | 16 +- docs/graphs/system_overview.dot | 58 ++--- docs/graphs/system_overview.lucid.svg | 209 ++++++++++++++++++ docs/graphs/system_overview.svg | 50 ++--- docs/graphs/themes/dark.gvpr | 92 ++++++++ docs/graphs/themes/lucid.gvpr | 114 ++++++++++ docs/graphs/wrapping.dot | 34 ++- docs/graphs/wrapping.lucid.svg | 119 ++++++++++ docs/graphs/wrapping.svg | 54 ++--- schema.json | 2 +- soleprint/artery/index.html | 21 +- soleprint/artery/plexuses/bundle/plexus.json | 43 ++++ soleprint/atlas/index.html | 10 + soleprint/atlas/main.py | 46 ++-- soleprint/common/theme/bake.py | 177 +++++++++++++++ soleprint/common/theme/theme.js | 8 +- soleprint/common/theme/themes/lucid.css | 150 +++++++++++++ soleprint/common/theme/themes/mcrn.css | 6 +- soleprint/common/theme/themes/soleprint.css | 6 +- soleprint/common/theme/tokens.css | 10 +- soleprint/index.html | 14 ++ soleprint/run.py | 14 +- soleprint/station/index.html | 14 ++ .../station/monitors/databrowse/README.md | 26 +-- .../station/monitors/databrowse/index.html | 4 +- .../tools/datagen/templates/index.html | 15 ++ .../tools/graphgen/templates/index.html | 15 ++ soleprint/station/tools/sbwrapper/index.html | 10 +- .../tools/shuntgen/templates/index.html | 31 +++ soleprint/station/tools/tester/.env | 6 - soleprint/station/tools/tester/.env.example | 19 ++ .../tools/tester/SESSION_6_IMPLEMENTATION.md | 4 +- soleprint/station/tools/tester/get-api-key.sh | 17 +- .../tools/tester/tests/_dev/test_health.py | 1 - 49 files changed, 1876 insertions(+), 341 deletions(-) create mode 100644 docs/graphs/README.md create mode 100644 docs/graphs/artery_hierarchy.lucid.svg create mode 100644 docs/graphs/cfg_gen_flow.lucid.svg create mode 100755 docs/graphs/render.sh create mode 100644 docs/graphs/room_layers.lucid.svg create mode 100644 docs/graphs/system_overview.lucid.svg create mode 100644 docs/graphs/themes/dark.gvpr create mode 100644 docs/graphs/themes/lucid.gvpr create mode 100644 docs/graphs/wrapping.lucid.svg create mode 100644 soleprint/artery/plexuses/bundle/plexus.json create mode 100644 soleprint/common/theme/bake.py create mode 100644 soleprint/common/theme/themes/lucid.css delete mode 100644 soleprint/station/tools/tester/.env create mode 100644 soleprint/station/tools/tester/.env.example diff --git a/.gitignore b/.gitignore index 986c43b..dde7527 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,11 @@ __pycache__/ .venv/ venv/ +# Secrets. There was no rule here, which is how a real API key ended up tracked +# in station/tools/tester/.env. Templates still ship. +.env +!.env.example + # Node node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md index b4f3c72..36932b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -221,8 +221,27 @@ framework, and never something a test reaches into. ## Known Broken (found, not yet fixed) -- `tester/tests/_dev/test_health.py` imports `..endpoints`, which does not exist in - core — the module is unimportable. -- Client leaks remain in `station/monitors/databrowse/` (README + `index.html`) and - `atlas/main.py` (`PAWPRINT_URL`, role labels). Core should carry no client - vocabulary; it belongs in `cfg//`. +- `tester/tests/example/test_health.py` imports `pytest`, which is neither a + dependency nor allowed here — `tests/README.md` says stdlib `unittest` and + `httpx`, no pytest. It fails to import, so `python -m tester discover` reports + it as `_FailedTest`. It also duplicates `test_template.py`, which is the + sanctioned example. Removing it is probably the fix; core ships no tests. +- Old vocabulary survives in prose — `album`, `larder`, `ward`, `nest` and + `pawprint` still appear in READMEs, `docs/`, and this file's history. Live code + is clean; the docs lag. + +## Fixed + +- **A real API key was tracked** in `station/tools/tester/.env`. Untracked, `.env` + added to `.gitignore`, `.env.example` added. **The key is still in git history + and must be rotated** — untracking does not unpublish it. +- Client vocabulary is out of live code. `atlas/main.py` was written entirely as + `Album`/`larder`/`PAWPRINT_URL`; `databrowse` docs described a `larder/` the + code stopped using; `get-api-key.sh` defaulted to the client's database name. +- Two dead back-links, found while renaming: `atlas/main.py` passed + `pawprint_url` and `artery/index.html` read `pawprint_url`, while `run.py` + passes `soleprint_url`. Neither "← Soleprint" link had ever rendered. +- `atlas/main.py` fetched `/api/data/album`; `main.py` serves `/api/data/atlas`. + `get_data()` had been failing and returning empty lists. +- `tester/tests/_dev/test_health.py` imported a non-existent `..endpoints`. The + import was unused; removing it makes the module discoverable (2 tests). diff --git a/cfg/sample/data/__init__.py b/cfg/sample/data/__init__.py index 9836af5..d4b1b2c 100644 --- a/cfg/sample/data/__init__.py +++ b/cfg/sample/data/__init__.py @@ -1,5 +1,5 @@ """ -Pawprint Data Layer +Soleprint Data Layer JSON file storage (future: MongoDB) """ diff --git a/cfg/standalone/data/__init__.py b/cfg/standalone/data/__init__.py index 9836af5..d4b1b2c 100644 --- a/cfg/standalone/data/__init__.py +++ b/cfg/standalone/data/__init__.py @@ -1,5 +1,5 @@ """ -Pawprint Data Layer +Soleprint Data Layer JSON file storage (future: MongoDB) """ diff --git a/cfg/standalone/data/rooms.json b/cfg/standalone/data/rooms.json index 9310a6e..8d881e6 100644 --- a/cfg/standalone/data/rooms.json +++ b/cfg/standalone/data/rooms.json @@ -1,5 +1,5 @@ { "items": [ - {"name": "pawprint-local", "slug": "pawprint-local", "title": "Pawprint Local", "status": "dev", "config_path": "deploy/pawprint-local"} + {"name": "standalone-local", "slug": "standalone-local", "title": "Standalone Local", "status": "dev", "config_path": "deploy/standalone-local"} ] } diff --git a/docs/graphs/README.md b/docs/graphs/README.md new file mode 100644 index 0000000..ff08c6a --- /dev/null +++ b/docs/graphs/README.md @@ -0,0 +1,67 @@ +# Graphs + +The `.dot` files carry **structure and meaning**. The palette lives in +`themes/*.gvpr` and is applied at render time. + +```bash +./render.sh # every graph, every theme +./render.sh lucid # one theme +./render.sh dark system_overview +``` + +Needs graphviz (`sudo apt install graphviz`). The committed SVGs work without +it; this is only needed to re-render. + +## Why themes, and why gvpr + +The sources used to hardcode the dark palette inline, which meant one look and +no way to get another. `gvpr` rewrites the *parsed* graph, so it overrides +whatever a `.dot` set — one source renders in any theme without being edited. + +Command-line `-G`/`-N`/`-E` flags would not do: those are defaults, and an +attribute written in the file beats them. + +Colour is **baked into each SVG** rather than driven by CSS, because both docs +sites embed graphs with ``. That makes the SVG a separate document +the page's stylesheet cannot reach. + +## Output + +| Theme | Writes | For | +| --- | --- | --- | +| `dark` | `.svg` | the docs site — this is the default, and what `docs/data/en/*.md` links to | +| `lucid` | `.lucid.svg` | regulated documents, print, and sitting beside a real lucid.app export | + +## Classes + +Nodes, edges, clusters and the graph itself may carry a `class`. Anything +untagged gets the theme's neutral treatment. + +| Class | Means | dark | lucid | +| --- | --- | --- | --- | +| `accent` | the emphasised thing | amber outline | blue outline, pale blue fill | +| `accent-text` | emphasised *label*, not a box | amber text | blue text | +| `ok` | live, working | green text | green outline, pale green fill | +| `artery` / `atlas` / `station` | belongs to that system | that system's colour | its pale equivalent | +| `muted` | present but not the point | grey text | grey fill | + +`class` survives into the SVG (``), so an **inlined** SVG +can also be styled by page CSS. That is not how the docs embed them, but it is +there if a page wants it. + +## What themes never touch + +- **`shape`** — a cylinder is a datastore, not a decoration. +- **`style=invis`** — layout scaffolding. Filling it would draw it. +- **`style=dashed`** — a weaker relationship; the theme preserves it and adds to it. +- **`label`, `rankdir`, `rank`, `fontsize`** — content and layout. + +## Adding a theme + +Drop a `themes/.gvpr` in beside the others; `render.sh` picks it up with no +edit. Match the palette to `soleprint/common/theme/themes/.css` so a +diagram and the page around it are the same visual language. + +Name fonts that exist on the target. `lucid.gvpr` uses Arial deliberately: it is +on every Windows box and fontconfig aliases it to Liberation Sans on Linux, so +the SVG measures the same on both and text does not reflow out of its box. diff --git a/docs/graphs/artery_hierarchy.dot b/docs/graphs/artery_hierarchy.dot index 10b9307..9c2d64d 100644 --- a/docs/graphs/artery_hierarchy.dot +++ b/docs/graphs/artery_hierarchy.dot @@ -1,43 +1,37 @@ digraph artery_hierarchy { - bgcolor="#0a0a0a" rankdir=LR fontname="Helvetica" - node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5" shape=box] - edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#b91c1c"] + node [fontname="Helvetica" fontsize=11 style=filled shape=box] + edge [class="artery" fontname="Helvetica" fontsize=9] label="Artery — Component Hierarchy" labelloc=t fontsize=14 - fontcolor="#fca5a5" - vein [label="Vein\nstateless API connector" fillcolor="#1a1a1a"] - pulse [label="Pulse\nVein + Room + Depot" fillcolor="#1a1a1a"] - plexus [label="Plexus\nfull app: backend\n+ frontend + DB" fillcolor="#1a1a1a"] - shunt [label="Shunt\nfake connector\nfor testing" fillcolor="#1a1a1a" color="#d4a574"] + vein [label="Vein\nstateless API connector"] + pulse [label="Pulse\nVein + Room + Depot"] + plexus [label="Plexus\nfull app: backend\n+ frontend + DB"] + shunt [class="accent" label="Shunt\nfake connector\nfor testing"] vein -> pulse [label="compose"] pulse -> plexus [label="extend"] - shunt -> vein [label="replaces" style=dashed color="#d4a574" fontcolor="#d4a574"] + shunt -> vein [class="accent" label="replaces" style=dashed] // Examples subgraph cluster_examples { label="Live Veins" style=dashed - color="#333" - fontcolor="#666" - jira [label="Jira" fillcolor="#1a1a1a" fontcolor="#15803d" fontsize=9] - google [label="Google" fillcolor="#1a1a1a" fontcolor="#d4a574" fontsize=9] - ia [label="IA" fillcolor="#1a1a1a" fontcolor="#15803d" fontsize=9] + jira [class="ok" label="Jira" fontsize=9] + google [class="accent-text" label="Google" fontsize=9] + ia [class="ok" label="IA" fontsize=9] } subgraph cluster_shunts { label="Shunts" style=dashed - color="#333" - fontcolor="#666" - mp [label="MercadoPago" fillcolor="#1a1a1a" fontcolor="#d4a574" fontsize=9] + mp [class="accent-text" label="MercadoPago" fontsize=9] } jira -> vein [style=invis] diff --git a/docs/graphs/artery_hierarchy.lucid.svg b/docs/graphs/artery_hierarchy.lucid.svg new file mode 100644 index 0000000..d02a353 --- /dev/null +++ b/docs/graphs/artery_hierarchy.lucid.svg @@ -0,0 +1,101 @@ + + + + + + +artery_hierarchy + +Artery — Component Hierarchy + +cluster_examples + +Live Veins + + +cluster_shunts + +Shunts + + + +jira + +Jira + + + +vein + +Vein +stateless API connector + + + + +google + +Google + + + +ia + +IA + + + +mp + +MercadoPago + + + +shunt + +Shunt +fake connector +for testing + + + + +pulse + +Pulse +Vein + Room + Depot + + + +vein->pulse + + +compose + + + +plexus + +Plexus +full app: backend ++ frontend + DB + + + +pulse->plexus + + +extend + + + +shunt->vein + + +replaces + + + diff --git a/docs/graphs/artery_hierarchy.svg b/docs/graphs/artery_hierarchy.svg index 044ed15..73c84c6 100644 --- a/docs/graphs/artery_hierarchy.svg +++ b/docs/graphs/artery_hierarchy.svg @@ -4,98 +4,98 @@ - - + + artery_hierarchy - -Artery — Component Hierarchy + +Artery — Component Hierarchy cluster_examples - -Live Veins + +Live Veins cluster_shunts - -Shunts + +Shunts + + + +jira + +Jira - + vein - -Vein -stateless API connector + +Vein +stateless API connector + + + + +google + +Google + + + +ia + +IA + + + +mp + +MercadoPago + + + +shunt + +Shunt +fake connector +for testing + - + pulse - -Pulse -Vein + Room + Depot + +Pulse +Vein + Room + Depot - + vein->pulse - - -compose + + +compose - + plexus - -Plexus -full app: backend -+ frontend + DB + +Plexus +full app: backend ++ frontend + DB - + pulse->plexus - - -extend - - - -shunt - -Shunt -fake connector -for testing + + +extend - + shunt->vein - - -replaces - - - -jira - -Jira - - - - -google - -Google - - - -ia - -IA + + +replaces - - -mp - -MercadoPago - - diff --git a/docs/graphs/cfg_gen_flow.dot b/docs/graphs/cfg_gen_flow.dot index 3bbb97a..16f1e61 100644 --- a/docs/graphs/cfg_gen_flow.dot +++ b/docs/graphs/cfg_gen_flow.dot @@ -1,43 +1,37 @@ digraph cfg_gen_flow { - bgcolor="#0a0a0a" rankdir=LR fontname="Helvetica" - node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5" shape=box] - edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#d4a574"] + node [fontname="Helvetica" fontsize=11 style=filled shape=box] + edge [class="accent" fontname="Helvetica" fontsize=9] label="Build Flow — cfg/ to gen/" labelloc=t fontsize=14 - fontcolor="#d4a574" // Source subgraph cluster_source { label="Source (committed)" style=dashed - color="#333" - fontcolor="#666" - core [label="soleprint/\ncore framework" fillcolor="#1a1a1a"] - cfg [label="cfg//\nroom config" fillcolor="#1a1a1a"] + core [label="soleprint/\ncore framework"] + cfg [label="cfg//\nroom config"] } // Build - build [label="build.py\n--cfg " fillcolor="#1a1a1a" color="#d4a574" shape=component] + build [class="accent" label="build.py\n--cfg " shape=component] // Output subgraph cluster_output { label="Output (generated, gitignored)" style=dashed - color="#333" - fontcolor="#666" - gen_spr [label="gen//soleprint/\ncore + room merged" fillcolor="#1a1a1a"] - gen_app [label="gen///\ncloned repos" fillcolor="#1a1a1a"] - gen_link [label="gen//link/\nDB bridge" fillcolor="#1a1a1a"] + gen_spr [label="gen//soleprint/\ncore + room merged"] + gen_app [label="gen///\ncloned repos"] + gen_link [label="gen//link/\nDB bridge"] } // Run - docker [label="docker compose up" fillcolor="#1a1a1a" shape=component] + docker [label="docker compose up" shape=component] // Flow core -> build diff --git a/docs/graphs/cfg_gen_flow.lucid.svg b/docs/graphs/cfg_gen_flow.lucid.svg new file mode 100644 index 0000000..74d7810 --- /dev/null +++ b/docs/graphs/cfg_gen_flow.lucid.svg @@ -0,0 +1,114 @@ + + + + + + +cfg_gen_flow + +Build Flow — cfg/ to gen/ + +cluster_source + +Source (committed) + + +cluster_output + +Output (generated, gitignored) + + + +core + +soleprint/ +core framework + + + +build + + + +build.py +--cfg <room> + + + +core->build + + + + + +cfg + +cfg/<room>/ +room config + + + +cfg->build + + + + + +gen_spr + +gen/<room>/soleprint/ +core + room merged + + + +docker + + + +docker compose up + + + +gen_spr->docker + + + + + +gen_app + +gen/<room>/<app>/ +cloned repos + + + +gen_link + +gen/<room>/link/ +DB bridge + + + +build->gen_spr + + + + + +build->gen_app + + +if managed + + + +build->gen_link + + +if managed + + + diff --git a/docs/graphs/cfg_gen_flow.svg b/docs/graphs/cfg_gen_flow.svg index 18fbfcc..be0d0b8 100644 --- a/docs/graphs/cfg_gen_flow.svg +++ b/docs/graphs/cfg_gen_flow.svg @@ -28,7 +28,7 @@ core framework - + build @@ -37,7 +37,7 @@ --cfg <room> - + core->build @@ -50,65 +50,65 @@ room config - + cfg->build - + gen_spr gen/<room>/soleprint/ core + room merged - - -build->gen_spr - - + + +docker + + + +docker compose up + + + +gen_spr->docker + + - + gen_app gen/<room>/<app>/ cloned repos - - -build->gen_app - - -if managed - - + gen_link gen/<room>/link/ DB bridge + + +build->gen_spr + + + + + +build->gen_app + + +if managed + - + build->gen_link -if managed - - - -docker - - - -docker compose up - - - -gen_spr->docker - - +if managed diff --git a/docs/graphs/render.sh b/docs/graphs/render.sh new file mode 100755 index 0000000..055755e --- /dev/null +++ b/docs/graphs/render.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Render every .dot through every theme. +# +# ./render.sh # all graphs, all themes +# ./render.sh lucid # one theme +# ./render.sh dark system_overview +# +# The sources carry structure and meaning; themes/*.gvpr carry palette. gvpr +# rewrites the parsed graph, so it wins over anything a .dot sets inline — which +# is what lets one source render in several looks without being edited. +# +# Output naming: the default theme writes .svg, because that is what +# docs/data/en/*.md already links to and those links should keep working. Every +# other theme writes ..svg. +# +# Why baked rather than CSS: both docs sites embed graphs with , +# which makes the SVG a separate document that the page's stylesheet cannot +# reach. Colour has to be in the file. +set -euo pipefail +cd "$(dirname "$0")" + +DEFAULT_THEME=dark + +if ! command -v dot >/dev/null 2>&1 || ! command -v gvpr >/dev/null 2>&1; then + echo "graphviz not found — install with: sudo apt install graphviz" >&2 + echo "(the committed .svg files already work; this is only needed to re-render)" >&2 + exit 1 +fi + +theme_arg="${1:-}" +graph_arg="${2:-}" + +themes=() +if [ -n "$theme_arg" ]; then + if [ ! -f "themes/${theme_arg}.gvpr" ]; then + echo "no such theme: themes/${theme_arg}.gvpr" >&2 + echo "available: $(ls themes/*.gvpr 2>/dev/null | xargs -n1 basename | sed 's/\.gvpr$//' | tr '\n' ' ')" >&2 + exit 1 + fi + themes=("$theme_arg") +else + for t in themes/*.gvpr; do themes+=("$(basename "$t" .gvpr)"); done +fi + +shopt -s nullglob +sources=(*.dot) +if [ -n "$graph_arg" ]; then + sources=("${graph_arg%.dot}.dot") + [ -f "${sources[0]}" ] || { echo "no such graph: ${sources[0]}" >&2; exit 1; } +fi +[ ${#sources[@]} -gt 0 ] || { echo "no .dot files here"; exit 0; } + +for theme in "${themes[@]}"; do + for src in "${sources[@]}"; do + base="${src%.dot}" + if [ "$theme" = "$DEFAULT_THEME" ]; then + out="${base}.svg" + else + out="${base}.${theme}.svg" + fi + # One pipeline, so a gvpr failure fails the whole render rather than + # silently writing a half-themed file. + gvpr -c -f "themes/${theme}.gvpr" "$src" | dot -Tsvg -o "$out" + printf " %-14s %s\n" "$theme" "$out" + done +done + +echo +echo "done — ${#sources[@]} graph(s) x ${#themes[@]} theme(s)" diff --git a/docs/graphs/room_layers.dot b/docs/graphs/room_layers.dot index 8cf40de..7741671 100644 --- a/docs/graphs/room_layers.dot +++ b/docs/graphs/room_layers.dot @@ -1,22 +1,20 @@ digraph room_layers { - bgcolor="#0a0a0a" rankdir=TB fontname="Helvetica" - node [fontname="Helvetica" fontsize=10 style=filled color="#333" fontcolor="#e5e5e5" shape=record] - edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#666"] + node [fontname="Helvetica" fontsize=10 style=filled shape=record] + edge [fontname="Helvetica" fontsize=9] label="Room Layers — init wizard" labelloc=t fontsize=14 - fontcolor="#d4a574" - l0 [label="{Layer 0 | Config + Data | config.json · data/*.json}" fillcolor="#1a1a1a" color="#d4a574"] - l1 [label="{Layer 1 | Docker | soleprint/docker-compose.yml · .env}" fillcolor="#1a1a1a"] - l2 [label="{Layer 2 | Managed App | docker-compose.yml · Dockerfiles · .env}" fillcolor="#1a1a1a"] - l3 [label="{Layer 3 | Link | link/main.py · adapters/ · Dockerfile}" fillcolor="#1a1a1a"] - l4 [label="{Layer 4 | Scripts | ctrl/start.sh · stop.sh · status.sh · logs.sh}" fillcolor="#1a1a1a"] - l5 [label="{Layer 5 | Systems | tester/environments.json · tests/}" fillcolor="#1a1a1a"] - l6 [label="{Layer 6 | Nginx | nginx/local.conf · docker-compose.nginx.yml}" fillcolor="#1a1a1a"] + l0 [class="accent" label="{Layer 0 | Config + Data | config.json · data/*.json}"] + l1 [label="{Layer 1 | Docker | soleprint/docker-compose.yml · .env}"] + l2 [label="{Layer 2 | Managed App | docker-compose.yml · Dockerfiles · .env}"] + l3 [label="{Layer 3 | Link | link/main.py · adapters/ · Dockerfile}"] + l4 [label="{Layer 4 | Scripts | ctrl/start.sh · stop.sh · status.sh · logs.sh}"] + l5 [label="{Layer 5 | Systems | tester/environments.json · tests/}"] + l6 [label="{Layer 6 | Nginx | nginx/local.conf · docker-compose.nginx.yml}"] l0 -> l1 [label="required"] l1 -> l2 [label="if managed"] @@ -26,6 +24,6 @@ digraph room_layers { l5 -> l6 [label="if frontend"] // Annotations - note_req [label="every room" fillcolor="#0a0a0a" fontcolor="#d4a574" color="#0a0a0a" shape=plaintext fontsize=9] + note_req [class="accent" label="every room" shape=plaintext fontsize=9] note_req -> l0 [style=invis] } diff --git a/docs/graphs/room_layers.lucid.svg b/docs/graphs/room_layers.lucid.svg new file mode 100644 index 0000000..ed12798 --- /dev/null +++ b/docs/graphs/room_layers.lucid.svg @@ -0,0 +1,133 @@ + + + + + + +room_layers + +Room Layers — init wizard + + +l0 + +Layer 0 + +Config + Data + +config.json · data/*.json + + + +l1 + +Layer 1 + +Docker + +soleprint/docker-compose.yml · .env + + + +l0->l1 + + +required + + + +l2 + +Layer 2 + +Managed App + +docker-compose.yml · Dockerfiles · .env + + + +l1->l2 + + +if managed + + + +l4 + +Layer 4 + +Scripts + +ctrl/start.sh · stop.sh · status.sh · logs.sh + + + +l1->l4 + + +optional + + + +l3 + +Layer 3 + +Link + +link/main.py · adapters/ · Dockerfile + + + +l2->l3 + + +optional + + + +l5 + +Layer 5 + +Systems + +tester/environments.json · tests/ + + + +l4->l5 + + +optional + + + +l6 + +Layer 6 + +Nginx + +nginx/local.conf · docker-compose.nginx.yml + + + +l5->l6 + + +if frontend + + + +note_req + +every room + + + + diff --git a/docs/graphs/room_layers.svg b/docs/graphs/room_layers.svg index 7a39d9b..7cf5537 100644 --- a/docs/graphs/room_layers.svg +++ b/docs/graphs/room_layers.svg @@ -11,7 +11,7 @@ Room Layers — init wizard - + l0 Layer 0 @@ -55,7 +55,7 @@ if managed - + l4 Layer 4 @@ -65,14 +65,14 @@ ctrl/start.sh · stop.sh · status.sh · logs.sh - + l1->l4 optional - + l3 Layer 3 @@ -82,7 +82,7 @@ link/main.py · adapters/ · Dockerfile - + l2->l3 @@ -123,10 +123,10 @@ if frontend - + note_req - -every room + +every room diff --git a/docs/graphs/system_overview.dot b/docs/graphs/system_overview.dot index 9d0dee6..6154f58 100644 --- a/docs/graphs/system_overview.dot +++ b/docs/graphs/system_overview.dot @@ -1,88 +1,78 @@ digraph system_overview { - bgcolor="#0a0a0a" rankdir=TB compound=true fontname="Helvetica" - node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5"] - edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#666"] + node [fontname="Helvetica" fontsize=11 style=filled] + edge [fontname="Helvetica" fontsize=9] label="Soleprint — System Overview" labelloc=t fontsize=14 - fontcolor="#d4a574" // Core subgraph cluster_core { label="Soleprint Hub" style=dashed - color="#d4a574" - fontcolor="#d4a574" + class="accent" - hub [label="soleprint\ncore coordinator\nport 12000" fillcolor="#1a1a1a" shape=box] + hub [label="soleprint\ncore coordinator\nport 12000" shape=box] } // Artery subgraph cluster_artery { label="Artery — Todo lo vital" style=dashed - color="#b91c1c" - fontcolor="#fca5a5" + class="artery" - veins [label="Veins\nstateless connectors" fillcolor="#1a1a1a"] - shunts [label="Shunts\nmock connectors" fillcolor="#1a1a1a"] - pulses [label="Pulses\ncomposed flows" fillcolor="#1a1a1a"] + veins [label="Veins\nstateless connectors"] + shunts [label="Shunts\nmock connectors"] + pulses [label="Pulses\ncomposed flows"] } // Atlas subgraph cluster_atlas { label="Atlas — Documentacion accionable" style=dashed - color="#15803d" - fontcolor="#86efac" + class="atlas" - books [label="Books\ndocumentation" fillcolor="#1a1a1a"] - templates [label="Templates\npatterns" fillcolor="#1a1a1a"] + books [label="Books\ndocumentation"] + templates [label="Templates\npatterns"] } // Station subgraph cluster_station { label="Station — Centro de control" style=dashed - color="#1d4ed8" - fontcolor="#93c5fd" + class="station" - tools [label="Tools\ntester · datagen · modelgen" fillcolor="#1a1a1a"] - monitors [label="Monitors\ndatabrowse" fillcolor="#1a1a1a"] + tools [label="Tools\ntester · datagen · modelgen"] + monitors [label="Monitors\ndatabrowse"] } // External subgraph cluster_external { label="External APIs" style=dashed - color="#333" - fontcolor="#666" - jira [label="Jira" fillcolor="#1a1a1a" fontcolor="#a3a3a3"] - google [label="Google" fillcolor="#1a1a1a" fontcolor="#a3a3a3"] - slack [label="Slack" fillcolor="#1a1a1a" fontcolor="#a3a3a3"] + jira [label="Jira"] + google [label="Google"] + slack [label="Slack"] } // Managed app subgraph cluster_managed { label="Managed App" style=dashed - color="#333" - fontcolor="#666" - app_fe [label="Frontend" fillcolor="#1a1a1a" fontcolor="#a3a3a3"] - app_be [label="Backend" fillcolor="#1a1a1a" fontcolor="#a3a3a3"] - app_db [label="Database" fillcolor="#1a1a1a" fontcolor="#a3a3a3" shape=cylinder] + app_fe [label="Frontend"] + app_be [label="Backend"] + app_db [label="Database" shape=cylinder] } // Connections - hub -> veins [label="routes" color="#b91c1c"] - hub -> books [label="routes" color="#15803d"] - hub -> tools [label="routes" color="#1d4ed8"] + hub -> veins [class="artery" label="routes"] + hub -> books [class="atlas" label="routes"] + hub -> tools [class="station" label="routes"] veins -> jira [label="API"] veins -> google [label="OAuth"] @@ -93,5 +83,5 @@ digraph system_overview { monitors -> app_db [label="browse" style=dashed] // Sidebar injection - hub -> app_fe [label="sidebar\ninjection" color="#d4a574" style=dashed] + hub -> app_fe [class="accent" label="sidebar\ninjection" style=dashed] } diff --git a/docs/graphs/system_overview.lucid.svg b/docs/graphs/system_overview.lucid.svg new file mode 100644 index 0000000..7880aa2 --- /dev/null +++ b/docs/graphs/system_overview.lucid.svg @@ -0,0 +1,209 @@ + + + + + + +system_overview + +Soleprint — System Overview + +cluster_core + +Soleprint Hub + + +cluster_artery + +Artery — Todo lo vital + + +cluster_atlas + +Atlas — Documentacion accionable + + +cluster_station + +Station — Centro de control + + +cluster_external + +External APIs + + +cluster_managed + +Managed App + + + +hub + +soleprint +core coordinator +port 12000 + + + +veins + +Veins +stateless connectors + + + +hub->veins + + +routes + + + +books + +Books +documentation + + + +hub->books + + +routes + + + +tools + +Tools +tester · datagen · modelgen + + + +hub->tools + + +routes + + + +app_fe + +Frontend + + + +hub->app_fe + + +sidebar +injection + + + +pulses + +Pulses +composed flows + + + +veins->pulses + + +compose + + + +jira + +Jira + + + +veins->jira + + +API + + + +google + +Google + + + +veins->google + + +OAuth + + + +slack + +Slack + + + +veins->slack + + +API + + + +shunts + +Shunts +mock connectors + + + +templates + +Templates +patterns + + + +app_be + +Backend + + + +tools->app_be + + +test + + + +monitors + +Monitors +databrowse + + + +app_db + + +Database + + + +monitors->app_db + + +browse + + + diff --git a/docs/graphs/system_overview.svg b/docs/graphs/system_overview.svg index 38c34e9..82c9a57 100644 --- a/docs/graphs/system_overview.svg +++ b/docs/graphs/system_overview.svg @@ -10,22 +10,22 @@ system_overview Soleprint — System Overview - + cluster_core Soleprint Hub - + cluster_artery Artery — Todo lo vital - + cluster_atlas Atlas — Documentacion accionable - + cluster_station Station — Centro de control @@ -56,11 +56,11 @@ stateless connectors - + hub->veins -routes +routes @@ -70,11 +70,11 @@ documentation - + hub->books -routes +routes @@ -84,25 +84,25 @@ tester · datagen · modelgen - + hub->tools -routes +routes app_fe -Frontend +Frontend - + hub->app_fe -sidebar -injection +sidebar +injection @@ -112,7 +112,7 @@ composed flows - + veins->pulses @@ -122,10 +122,10 @@ jira -Jira +Jira - + veins->jira @@ -135,10 +135,10 @@ google -Google +Google - + veins->google @@ -148,10 +148,10 @@ slack -Slack +Slack - + veins->slack @@ -175,10 +175,10 @@ app_be -Backend +Backend - + tools->app_be @@ -196,10 +196,10 @@ app_db -Database +Database - + monitors->app_db diff --git a/docs/graphs/themes/dark.gvpr b/docs/graphs/themes/dark.gvpr new file mode 100644 index 0000000..8a0859e --- /dev/null +++ b/docs/graphs/themes/dark.gvpr @@ -0,0 +1,92 @@ +// Dark — the palette the docs have always used, restated as a theme. +// +// This exists so the .dot sources can stop carrying colour. It reproduces what +// the committed SVGs look like today, which also makes it the regression +// oracle: strip the inline colours from a source, render it through this, and +// the result should be the diagram you had before. If it is not, the strip was +// wrong. +// +// Palette matches common/theme/themes/soleprint.css, so a diagram and the page +// around it are the same visual language. + +BEGIN { + string BG = "#0a0a0a"; + string FILL = "#1a1a1a"; + string LINE = "#333333"; + string INK = "#e5e5e5"; + string MUTED = "#a3a3a3"; + string DIM = "#666666"; + string FONT = "Helvetica"; + graph_t sg; + + // The soleprint amber, and the three system colours, each with the lighter + // text variant the docs use for labels on dark. + string ACC_S = "#d4a574"; string ACC_T = "#d4a574"; + string ART_S = "#b91c1c"; string ART_T = "#fca5a5"; + string ATL_S = "#15803d"; string ATL_T = "#86efac"; + string STA_S = "#1d4ed8"; string STA_T = "#93c5fd"; +} + +BEG_G { + $G.bgcolor = BG; + setDflt($G, "N", "fontname", FONT); + setDflt($G, "E", "fontname", FONT); + // Declare `class` so reading it on an untagged object is defined + // rather than a warning — most nodes carry no class by design. + setDflt($G, "G", "class", ""); + setDflt($G, "N", "class", ""); + setDflt($G, "E", "class", ""); + $G.fontname = FONT; + // The graph title takes the accent of whatever system the graph is about; + // graphs declare that with a class on the digraph itself. + if ($G.class == "artery") { $G.fontcolor = ART_T; } + else if ($G.class == "atlas") { $G.fontcolor = ATL_T; } + else if ($G.class == "station") { $G.fontcolor = STA_T; } + else { $G.fontcolor = ACC_T; } + + + for (sg = fstsubg($G); sg; sg = nxtsubg(sg)) { + if (index(sg.name, "cluster") == 0) { + sg.fontname = FONT; + sg.color = LINE; + sg.fontcolor = DIM; + if (sg.class == "artery") { sg.color = ART_S; sg.fontcolor = ART_T; } + else if (sg.class == "atlas") { sg.color = ATL_S; sg.fontcolor = ATL_T; } + else if (sg.class == "station") { sg.color = STA_S; sg.fontcolor = STA_T; } + else if (sg.class == "accent") { sg.color = ACC_S; sg.fontcolor = ACC_T; } + else if (sg.class == "ok") { sg.color = ATL_S; sg.fontcolor = ATL_T; } + if (index(sg.style, "dashed") < 0) { sg.style = "dashed"; } + } + } +} + +N { + if (index($.style, "invis") >= 0) { continue; } + + $.fontname = FONT; + $.fontcolor = INK; + $.color = LINE; + $.fillcolor = FILL; + + if ($.class == "accent") { $.color = ACC_S; } + else if ($.class == "artery") { $.color = ART_S; } + else if ($.class == "atlas") { $.color = ATL_S; } + else if ($.class == "station") { $.color = STA_S; } + else if ($.class == "accent-text") { $.fontcolor = ACC_T; } + else if ($.class == "ok") { $.fontcolor = ATL_T; } + else if ($.class == "muted") { $.fontcolor = MUTED; } + + if (index($.style, "dashed") >= 0) { $.style = "filled,dashed"; } + else { $.style = "filled"; } +} + +E { + $.fontname = FONT; + $.fontcolor = MUTED; + $.color = DIM; + + if ($.class == "accent") { $.color = ACC_S; $.fontcolor = ACC_T; } + else if ($.class == "artery") { $.color = ART_S; $.fontcolor = ART_T; } + else if ($.class == "atlas") { $.color = ATL_S; $.fontcolor = ATL_T; } + else if ($.class == "station") { $.color = STA_S; $.fontcolor = STA_T; } +} diff --git a/docs/graphs/themes/lucid.gvpr b/docs/graphs/themes/lucid.gvpr new file mode 100644 index 0000000..807d9c6 --- /dev/null +++ b/docs/graphs/themes/lucid.gvpr @@ -0,0 +1,114 @@ +// Lucid — graphviz output shaped after a lucid.app export. +// +// Run through gvpr, not as .dot attributes, because gvpr rewrites the parsed +// graph: it wins over whatever the source set inline, so one .dot renders in +// any theme without being edited. See render.sh. +// +// The look, and why each part is what it is: +// white canvas this ends up in a document and gets printed +// rounded box, hairline Lucid's default shape is a rounded rect with a grey +// 1px stroke, not a coloured fill +// pale-blue accent #d6e4ff on #3a7dff is Lucid's own selected-shape pair +// Arial the one face guaranteed on Windows, and aliased to +// Liberation Sans by fontconfig on Linux — so the SVG +// measures the same on both and the text does not +// reflow out of its box +// +// Structure is preserved, only palette is replaced. Two styles carry meaning +// rather than decoration and are explicitly kept: `invis` (a layout spacer — +// overwriting it makes hidden scaffolding visible) and `dashed` (a weaker +// relationship). Shapes are never touched: a cylinder is a datastore. + +BEGIN { + string BG = "#ffffff"; + string INK = "#1f2933"; + string MUTED = "#616e7c"; + string LINE = "#9aa5b1"; + string SOFT = "#cbd2d9"; + string FONT = "Arial"; + graph_t sg; + + string FILL = "#ffffff"; + string FILL_ALT = "#f5f7fa"; + + // class -> (stroke, fill). Lucid's palette for emphasised shapes. + string ACC_S = "#3a7dff"; string ACC_F = "#d6e4ff"; + string ART_S = "#c0392b"; string ART_F = "#fdeaea"; + string ATL_S = "#1a7f45"; string ATL_F = "#e6f5ec"; + string STA_S = "#2b5fd9"; string STA_F = "#e8effd"; +} + +BEG_G { + $G.bgcolor = BG; + $G.fontname = FONT; + $G.fontcolor = INK; + + // Defaults for anything the per-node block does not reach. + setDflt($G, "N", "fontname", FONT); + setDflt($G, "E", "fontname", FONT); + // Declare `class` so reading it on an untagged object is defined + // rather than a warning — most nodes carry no class by design. + setDflt($G, "G", "class", ""); + setDflt($G, "N", "class", ""); + setDflt($G, "E", "class", ""); + + // Clusters: a pale container, the way Lucid draws a grouping box. + for (sg = fstsubg($G); sg; sg = nxtsubg(sg)) { + if (index(sg.name, "cluster") == 0) { + sg.fontname = FONT; + sg.color = SOFT; + sg.fontcolor = MUTED; + sg.bgcolor = FILL_ALT; + // A tagged container keeps its identity, the way a Lucid swimlane + // is tinted by what it holds. + if (sg.class == "artery") { sg.color = ART_S; sg.bgcolor = ART_F; } + else if (sg.class == "atlas") { sg.color = ATL_S; sg.bgcolor = ATL_F; } + else if (sg.class == "station") { sg.color = STA_S; sg.bgcolor = STA_F; } + else if (sg.class == "accent") { sg.color = ACC_S; sg.bgcolor = ACC_F; } + // Keep dashed where the source chose it; it reads as "logical + // grouping" rather than "deployed boundary". + if (index(sg.style, "dashed") < 0) { sg.style = "rounded"; } + else { sg.style = "dashed,rounded"; } + } + } +} + +N { + // An invisible node is layout scaffolding. Filling it would draw it. + if (index($.style, "invis") >= 0) { continue; } + + $.fontname = FONT; + $.fontcolor = INK; + $.color = LINE; + $.fillcolor = FILL; + $.penwidth = 1; + + if ($.class == "accent") { $.color = ACC_S; $.fillcolor = ACC_F; } + else if ($.class == "artery") { $.color = ART_S; $.fillcolor = ART_F; } + else if ($.class == "atlas") { $.color = ATL_S; $.fillcolor = ATL_F; } + else if ($.class == "station") { $.color = STA_S; $.fillcolor = STA_F; } + else if ($.class == "accent-text") { $.fontcolor = ACC_S; } + else if ($.class == "ok") { $.color = ATL_S; $.fillcolor = ATL_F; } + else if ($.class == "muted") { $.fillcolor = FILL_ALT; $.fontcolor = MUTED; } + + // `record` ignores rounding, and plaintext has no box to round. + if ($.shape == "record" || $.shape == "Mrecord" || $.shape == "plaintext") { + $.style = "filled"; + } else if (index($.style, "dashed") >= 0) { + $.style = "filled,rounded,dashed"; + } else { + $.style = "filled,rounded"; + } +} + +E { + $.fontname = FONT; + $.fontcolor = MUTED; + $.color = LINE; + $.arrowsize = 0.7; + + if ($.class == "accent") { $.color = ACC_S; $.fontcolor = ACC_S; } + else if ($.class == "artery") { $.color = ART_S; $.fontcolor = ART_S; } + else if ($.class == "atlas") { $.color = ATL_S; $.fontcolor = ATL_S; } + else if ($.class == "station") { $.color = STA_S; $.fontcolor = STA_S; } +} diff --git a/docs/graphs/wrapping.dot b/docs/graphs/wrapping.dot index f405f72..132235a 100644 --- a/docs/graphs/wrapping.dot +++ b/docs/graphs/wrapping.dot @@ -1,45 +1,39 @@ digraph wrapping { - bgcolor="#0a0a0a" rankdir=LR fontname="Helvetica" - node [fontname="Helvetica" fontsize=11 style=filled color="#333" fontcolor="#e5e5e5" shape=box] - edge [fontname="Helvetica" fontsize=9 fontcolor="#a3a3a3" color="#666"] + node [fontname="Helvetica" fontsize=11 style=filled shape=box] + edge [fontname="Helvetica" fontsize=9] label="Sidebar Injection — How Wrapping Works" labelloc=t fontsize=14 - fontcolor="#d4a574" - browser [label="Browser" fillcolor="#1a1a1a" shape=oval] + browser [label="Browser" shape=oval] subgraph cluster_nginx { label="Nginx (reverse proxy)" style=dashed - color="#d4a574" - fontcolor="#d4a574" + class="accent" - proxy [label="proxy_pass\n+\nsub_filter\ninjects sidebar" fillcolor="#1a1a1a" shape=component] + proxy [label="proxy_pass\n+\nsub_filter\ninjects sidebar" shape=component] } subgraph cluster_app { label="Managed App" style=dashed - color="#333" - fontcolor="#666" - frontend [label="Frontend\n(React/Next/Vue)" fillcolor="#1a1a1a"] - backend [label="Backend API" fillcolor="#1a1a1a"] + frontend [label="Frontend\n(React/Next/Vue)"] + backend [label="Backend API"] } subgraph cluster_spr { label="Soleprint" style=dashed - color="#d4a574" - fontcolor="#d4a574" + class="accent" - sidebar_css [label="sidebar.css" fillcolor="#1a1a1a"] - sidebar_js [label="sidebar.js" fillcolor="#1a1a1a"] - hub [label="Hub API\n/api/sidebar/config" fillcolor="#1a1a1a"] + sidebar_css [label="sidebar.css"] + sidebar_js [label="sidebar.js"] + hub [label="Hub API\n/api/sidebar/config"] } browser -> proxy [label="myroom.spr.local.ar"] @@ -47,8 +41,8 @@ digraph wrapping { proxy -> hub [label="/spr/ → soleprint"] // The injection - proxy -> sidebar_css [label="injects into " color="#d4a574" style=dashed] - proxy -> sidebar_js [color="#d4a574" style=dashed] + proxy -> sidebar_css [class="accent" label="injects into " style=dashed] + proxy -> sidebar_js [class="accent" style=dashed] - sidebar_js -> hub [label="loads config" color="#d4a574"] + sidebar_js -> hub [class="accent" label="loads config"] } diff --git a/docs/graphs/wrapping.lucid.svg b/docs/graphs/wrapping.lucid.svg new file mode 100644 index 0000000..9365564 --- /dev/null +++ b/docs/graphs/wrapping.lucid.svg @@ -0,0 +1,119 @@ + + + + + + +wrapping + +Sidebar Injection — How Wrapping Works + +cluster_nginx + +Nginx (reverse proxy) + + +cluster_app + +Managed App + + +cluster_spr + +Soleprint + + + +proxy + + + +proxy_pass ++ +sub_filter +injects sidebar + + + +frontend + +Frontend +(React/Next/Vue) + + + +proxy->frontend + + +/ → app + + + +sidebar_css + +sidebar.css + + + +proxy->sidebar_css + + +injects into </head> + + + +sidebar_js + +sidebar.js + + + +proxy->sidebar_js + + + + + +hub + +Hub API +/api/sidebar/config + + + +proxy->hub + + +/spr/ → soleprint + + + +backend + +Backend API + + + +sidebar_js->hub + + +loads config + + + +browser + +Browser + + + +browser->proxy + + +myroom.spr.local.ar + + + diff --git a/docs/graphs/wrapping.svg b/docs/graphs/wrapping.svg index 95b172f..6c73749 100644 --- a/docs/graphs/wrapping.svg +++ b/docs/graphs/wrapping.svg @@ -10,7 +10,7 @@ wrapping Sidebar Injection — How Wrapping Works - + cluster_nginx Nginx (reverse proxy) @@ -20,19 +20,13 @@ Managed App - + cluster_spr Soleprint - - -browser - -Browser - - + proxy @@ -42,15 +36,8 @@ sub_filter injects sidebar - - -browser->proxy - - -myroom.spr.local.ar - - + frontend Frontend @@ -64,56 +51,69 @@ / → app - + sidebar_css sidebar.css - + proxy->sidebar_css -injects into </head> +injects into </head> - + sidebar_js sidebar.js - + proxy->sidebar_js - + hub Hub API /api/sidebar/config - + proxy->hub /spr/ → soleprint - + backend Backend API - + sidebar_js->hub -loads config +loads config + + + +browser + +Browser + + + +browser->proxy + + +myroom.spr.local.ar diff --git a/schema.json b/schema.json index d7bcb20..578a27b 100644 --- a/schema.json +++ b/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Pawprint Models", + "title": "Soleprint Models", "description": "Platform-agnostic model definitions. Portable to TypeScript, Pydantic, Django, Prisma.", "definitions": { "Status": { diff --git a/soleprint/artery/index.html b/soleprint/artery/index.html index 093310e..ac52373 100644 --- a/soleprint/artery/index.html +++ b/soleprint/artery/index.html @@ -374,6 +374,21 @@ margin-bottom: 0.5rem; } + + + + + + ", f"{indent}{END}"] + return "\n".join(lines) + + +def bake(path: Path, values: dict[str, str]) -> tuple[bool, str]: + """Return (changed, note) for one page.""" + html = path.read_text() + + link = re.search(r'([ \t]*)', html) + if not link: + return False, "no /theme.css link — skipped" + + indent = link.group(1) + names = used(html) + if not names: + return False, "uses no theme variables — skipped" + + fresh = block(names, values, indent) + + existing = re.search(re.escape(BEGIN) + r".*?" + re.escape(END), html, re.S) + if existing: + updated = html[: existing.start()] + fresh.lstrip() + html[existing.end() :] + else: + # Before the link, never after: document order is what makes the served + # stylesheet win over the baked one. + updated = html[: link.start()] + fresh + "\n" + html[link.start() :] + + if updated == html: + return False, f"up to date ({len(names)} variables)" + path.write_text(updated) + return True, f"baked {len(names)} variables" + + +def main() -> int: + check = "--check" in sys.argv + values = palette() + missing = [n for n, v in values.items() if not v] + if missing: + print(f"warning: unresolved tokens: {', '.join(sorted(missing))}", file=sys.stderr) + + stale = [] + for rel in PAGES: + path = SPR_ROOT / rel + if not path.exists(): + print(f" {rel}: not found") + continue + if check: + before = path.read_text() + changed, note = bake(path, values) + if changed: + path.write_text(before) + stale.append(rel) + print(f" {rel}: STALE") + else: + print(f" {rel}: {note}") + else: + _, note = bake(path, values) + print(f" {rel}: {note}") + + if check and stale: + print(f"\n{len(stale)} page(s) stale — run: python3 common/theme/bake.py", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/soleprint/common/theme/theme.js b/soleprint/common/theme/theme.js index d24c350..3383279 100644 --- a/soleprint/common/theme/theme.js +++ b/soleprint/common/theme/theme.js @@ -19,7 +19,11 @@ (function () { "use strict"; - var THEMES = ["soleprint", "mcrn"]; + // Order is the toggle order. soleprint stays first because it is the + // default; lucid is last because it is the one you switch to on purpose, + // to show someone something. + var THEMES = ["soleprint", "mcrn", "lucid"]; + var LABELS = { soleprint: "SPR", mcrn: "MCRN", lucid: "LUCID" }; var KEY = "spr-theme"; var root = document.documentElement; @@ -82,7 +86,7 @@ var button = document.createElement("button"); button.type = "button"; button.dataset.theme = theme; - button.textContent = theme === "mcrn" ? "MCRN" : "SPR"; + button.textContent = LABELS[theme] || theme.toUpperCase(); button.title = "Switch to the " + theme + " theme"; button.addEventListener("click", function () { apply(theme, true); diff --git a/soleprint/common/theme/themes/lucid.css b/soleprint/common/theme/themes/lucid.css new file mode 100644 index 0000000..49102f4 --- /dev/null +++ b/soleprint/common/theme/themes/lucid.css @@ -0,0 +1,150 @@ +/* Lucid — the regulated-document look, shaped after lucid.app exports. + * + * Why it exists: the deliverable gets shown on Windows, printed, and pasted next + * to real Lucidchart diagrams. Dark developer chrome cannot go in that room. The + * target is that a generated page and a genuine Lucid export sit side by side + * without announcing which is which. + * + * This is the first LIGHT theme here, and that is the part that needed care — + * tokens.css and both sibling themes were written assuming near-black. Two + * things do not survive the inversion and are overridden below rather than + * inherited: + * + * - the glow. A coloured halo means "lit" against black; against white it just + * looks like a rendering fault. Replaced with a hairline drop shadow. + * - --dim as body-adjacent text. At #555568 on white it fails contrast, so the + * ramp is rebuilt from the light end rather than reused. + * + * Fonts are stacks, never a webfont: this has to render with no egress. Arial is + * last because it is the one face guaranteed on Windows and aliased on Linux — + * the same reason the graphviz themes name it (see docs/graphs/themes/). + */ + +[data-theme="lucid"] { + color-scheme: light; + + --bg: #ffffff; + --bg-2: #f5f7fa; + --surface: #f5f7fa; + --surface-raised: #e4e7eb; + --border: #cbd2d9; + --border-strong: #9aa5b1; + + /* Measured against both #ffffff and the #f5f7fa panel, because --dim is used + * for 11px notes and --status-warn for 10px labels — sizes where AA wants + * 4.5:1, not the 3:1 that large text gets away with. The obvious lighter + * greys (#7b8794, #73808d) come in at 3.4–3.8 on the panel and were dropped + * for that reason. */ + --text: #1f2933; /* 14.76 on white — near-black; pure #000 reads harsh in print */ + --muted: #616e7c; /* 5.21 / 4.86 */ + --dim: #66717d; /* 4.97 / 4.63 */ + + --accent: #3a7dff; + --accent-dim: #2f6ae0; + --accent-text: #1c5bd9; /* darkened: the fill blue is too light for small text */ + --glow: rgba(58, 125, 255, 0.18); + + --status-ok: #0b875b; /* 4.53 / 4.23 */ + --status-info: #1c5bd9; /* 5.92 / 5.52 */ + --status-warn: #a35f00; /* 5.01 / 4.67 — #b06a00 was 3.99 on the panel */ + --status-error: #cf2e2e; /* 5.14 / 4.79 */ + --status-idle: #9aa5b1; /* dots and rules only, never text */ + + --radius-sm: 4px; + --radius: 6px; + --radius-lg: 8px; + --radius-xl: 8px; + + --font-ui: "Segoe UI", Inter, system-ui, -apple-system, Arial, sans-serif; + --font-mono: "Cascadia Mono", Consolas, "JetBrains Mono", monospace; + --font-heading: "Segoe UI", Inter, system-ui, Arial, sans-serif; + --heading-transform: none; + --heading-spacing: 0; + --heading-weight: 600; + --label-spacing: 0.02em; + + --speed-fast: 0.12s; + --speed: 0.18s; + + /* Elevation, not luminosity. */ + --hover-shadow: 0 1px 3px rgba(16, 24, 40, 0.1), 0 1px 2px rgba(16, 24, 40, 0.06); + --hover-lift: none; + --focus-shadow: 0 0 0 2px rgba(58, 125, 255, 0.35); +} + +/* Panels are white cards on a pale canvas — the inverse of the dark themes, + * where the panel is lighter than the page. */ +[data-theme="lucid"] .panel, +[data-theme="lucid"] .card, +[data-theme="lucid"] .model-card { + background: #ffffff; +} + +[data-theme="lucid"] .card:hover, +[data-theme="lucid"] .panel:hover, +[data-theme="lucid"] .system-card:hover, +[data-theme="lucid"] .tool-card:hover, +[data-theme="lucid"] .model-card:hover { + border-color: var(--system-accent, var(--accent)); + box-shadow: var(--hover-shadow); + transform: none; +} + +/* A solid fill with white knocked out, the way a Lucid toolbar reads. No + * gradient: gradients are the first thing that looks wrong in print. */ +[data-theme="lucid"] button[aria-pressed="true"], +[data-theme="lucid"] .active, +[data-theme="lucid"] .selected { + background: var(--accent); + border-color: var(--accent); + color: #ffffff; +} + +[data-theme="lucid"] .label, +[data-theme="lucid"] .panel-title { + font-size: var(--font-size-sm); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); +} + +/* Pale-fill chips, the shape Lucid uses for tags on a shape. */ +[data-theme="lucid"] .badge { + border-radius: var(--radius-sm); + padding: 1px 6px; + font-family: var(--font-mono); + font-size: 10px; + background: #eaf0ff; + border: 1px solid #c3d4ff; + color: var(--accent-text); +} + +[data-theme="lucid"] code, +[data-theme="lucid"] pre { + background: #f5f7fa; + border-color: var(--border); +} + +/* Diagrams are the point of this theme, and they are rendered as , so the + * page cannot colour them — it can only stop fighting them. A white-canvas SVG + * needs a frame to read as a figure rather than as a hole in the page. */ +[data-theme="lucid"] img[src$=".svg"] { + background: #ffffff; + border: 1px solid var(--border); + border-radius: var(--radius); +} + +/* Printing is a first-class output here: this theme exists to end up in a + * document. Drop the chrome that has no meaning on paper. */ +@media print { + [data-theme="lucid"] #spr-theme-toggle, + [data-theme="lucid"] #spr-sidebar { + display: none !important; + } + [data-theme="lucid"] .panel, + [data-theme="lucid"] .card { + box-shadow: none; + break-inside: avoid; + } +} diff --git a/soleprint/common/theme/themes/mcrn.css b/soleprint/common/theme/themes/mcrn.css index 6861504..ead29bb 100644 --- a/soleprint/common/theme/themes/mcrn.css +++ b/soleprint/common/theme/themes/mcrn.css @@ -42,9 +42,9 @@ --radius-lg: 0; --radius-xl: 0; - --font-ui: "JetBrains Mono", "Fira Code", "SF Mono", monospace; - --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace; - --font-heading: "JetBrains Mono", "Fira Code", "SF Mono", monospace; + --font-ui: "JetBrains Mono", "Cascadia Mono", Consolas, "SF Mono", monospace; + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, "SF Mono", monospace; + --font-heading: "JetBrains Mono", "Cascadia Mono", Consolas, "SF Mono", monospace; --heading-transform: uppercase; --heading-spacing: 0.1em; --heading-weight: 400; diff --git a/soleprint/common/theme/themes/soleprint.css b/soleprint/common/theme/themes/soleprint.css index afbe6b7..50f265c 100644 --- a/soleprint/common/theme/themes/soleprint.css +++ b/soleprint/common/theme/themes/soleprint.css @@ -41,9 +41,9 @@ --radius-lg: 8px; --radius-xl: 12px; - --font-ui: "Inter", system-ui, -apple-system, sans-serif; - --font-mono: "JetBrains Mono", "Fira Code", monospace; - --font-heading: "Inter", system-ui, sans-serif; + --font-ui: Inter, "Segoe UI", system-ui, -apple-system, Arial, sans-serif; + --font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace; + --font-heading: Inter, "Segoe UI", system-ui, Arial, sans-serif; --heading-transform: none; --heading-spacing: 0.02em; --heading-weight: 600; diff --git a/soleprint/common/theme/tokens.css b/soleprint/common/theme/tokens.css index d0289d9..6feae80 100644 --- a/soleprint/common/theme/tokens.css +++ b/soleprint/common/theme/tokens.css @@ -20,7 +20,11 @@ * handler beside /sidebar.css. */ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&display=swap"); +/* No webfont import. This has to render on a locked-down Windows box with no + * egress and from a double-clicked file, and a blocked stylesheet there is a + * blank page or a stall, not a fallback. The stacks below resolve to something + * deliberate on every target: Segoe UI and Consolas ship with Windows, Inter and + * JetBrains Mono are picked up where they happen to be installed. */ :root { /* ── surfaces ─────────────────────────────────────────────────────── */ @@ -63,8 +67,8 @@ --hairline: 1px; /* ── type ─────────────────────────────────────────────────────────── */ - --font-ui: "Inter", system-ui, -apple-system, sans-serif; - --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace; + --font-ui: "Segoe UI", Inter, system-ui, -apple-system, Arial, sans-serif; + --font-mono: "Cascadia Mono", "JetBrains Mono", Consolas, "SF Mono", monospace; --font-heading: var(--font-ui); --font-size-sm: 11px; --font-size-base: 13px; diff --git a/soleprint/index.html b/soleprint/index.html index a06bd59..2f197bd 100644 --- a/soleprint/index.html +++ b/soleprint/index.html @@ -249,6 +249,20 @@ {% endif %} + + + diff --git a/soleprint/run.py b/soleprint/run.py index 4414422..2b309ee 100644 --- a/soleprint/run.py +++ b/soleprint/run.py @@ -601,11 +601,21 @@ def station_route(path: str): # page can switch themes without a second request and without FOUC. +def available_themes() -> list[str]: + """Theme names, from the files themselves — adding one is adding a file.""" + theme_dir = SPR_ROOT / "common" / "theme" / "themes" + if not theme_dir.exists(): + return ["soleprint"] + names = sorted(p.stem for p in theme_dir.glob("*.css")) + # Default first, so a consumer taking names[0] gets the sensible one. + return sorted(names, key=lambda n: (n != "soleprint", n)) + + def get_default_theme() -> str: """The theme a page is served in, before the browser has an opinion.""" framework = load_config().get("framework", {}) theme = framework.get("theme", "soleprint") - return theme if theme in ("soleprint", "mcrn") else "soleprint" + return theme if theme in available_themes() else "soleprint" @app.get("/theme.css") @@ -652,7 +662,7 @@ def theme_js(): @app.get("/api/theme") def theme_config(): """The server-side default, for pages that render their own tag.""" - return {"theme": get_default_theme(), "themes": ["soleprint", "mcrn"]} + return {"theme": get_default_theme(), "themes": available_themes()} @app.get("/sidebar.css") diff --git a/soleprint/station/index.html b/soleprint/station/index.html index 8af590b..58c797a 100644 --- a/soleprint/station/index.html +++ b/soleprint/station/index.html @@ -157,6 +157,20 @@ opacity: 0.5; } + + + + + +