Compare commits
1 Commits
content/ph
...
feat/phase
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da18ab2237 |
68
content/assets/BUDGET.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Phase 7.4 — asset weight budget
|
||||
|
||||
Measured 2026-10-26 on `.13` with `stat -c%s` and `gzip -9`. Every number below
|
||||
is a measurement of a file that is in this repository, not an estimate.
|
||||
|
||||
## What is in the assets directory
|
||||
|
||||
| Group | Files | Raw | Gzip (`-9`) |
|
||||
|---|---|---|---|
|
||||
| Diagrams (`diagrams/*.svg`) | 8 | 51,729 B | 8,943 B |
|
||||
| Code exhibits (`code/*.as`, `.js`, `.php`, `.ts`, `.hpp`, `.nix`) | 6 | 8,161 B | 2,989 B |
|
||||
| Client wall data (`logos/clients.yaml`) | 1 | 3,912 B | 1,288 B |
|
||||
| **Served payload total** | **15** | **63,802 B** | **13,220 B** |
|
||||
| Manifests and READMEs (build-time only, never served) | 5 | 15,955 B | — |
|
||||
|
||||
## Per-asset
|
||||
|
||||
| File | Raw | Gzip |
|
||||
|---|---|---|
|
||||
| `diagrams/01-homelab-topology.svg` | 7,079 | 2,169 |
|
||||
| `diagrams/02-agent-architecture.svg` | 6,027 | 1,971 |
|
||||
| `diagrams/03-rag-pipeline.svg` | 7,010 | 2,043 |
|
||||
| `diagrams/04-edge-architecture.svg` | 6,896 | 2,163 |
|
||||
| `diagrams/05-distributed-audio.svg` | 6,833 | 1,997 |
|
||||
| `diagrams/06-media-pipeline.svg` | 5,694 | 1,718 |
|
||||
| `diagrams/07-cost-resource-engineering.svg` | 6,151 | 2,023 |
|
||||
| `diagrams/08-era-timeline.svg` | 6,039 | 1,930 |
|
||||
| `code/01-actionscript-class.as` | 958 | 495 |
|
||||
| `code/02-js-easing-core.js` | 1,848 | 584 |
|
||||
| `code/03-php-encode-op.php` | 2,295 | 765 |
|
||||
| `code/04-typescript-service-layer.ts` | 1,078 | 482 |
|
||||
| `code/05-embedded-cpp-interface.hpp` | 788 | 419 |
|
||||
| `code/06-nix-derivation.nix` | 1,194 | 724 |
|
||||
|
||||
Rasters: **zero.** There are no PNG, JPEG, WebP or font files in this phase.
|
||||
That is a design outcome, not an oversight — every diagram is text-based SVG and
|
||||
every code exhibit is text, so the "optimise every image" requirement has no
|
||||
image to optimise yet. Phase 7.4 screenshots are deferred (see below).
|
||||
|
||||
## The budget
|
||||
|
||||
| Rule | Limit | Worst case today |
|
||||
|---|---|---|
|
||||
| Any single diagram | ≤ 7.5 KB raw / ≤ 2.5 KB gzip | 7,079 B / 2,169 B |
|
||||
| Any single code exhibit | ≤ 3 KB raw / ≤ 1 KB gzip | 2,295 B / 765 B |
|
||||
| Diagrams on one page | ≤ 2 | 2 (14.1 KB raw) |
|
||||
| Asset payload per page, gzipped | ≤ 12 KB | 4.3 KB (two diagrams + one exhibit) |
|
||||
| Web fonts | 0 | 0 — system font stack only |
|
||||
| JS required by any asset | 0 | 0 — SVG carries its own palette |
|
||||
|
||||
Worst realistic page: an L2 page with two diagrams and one code exhibit is
|
||||
14,089 B raw, about **4.3 KB over the wire** after gzip and before the HTML, CSS
|
||||
and template overhead. The whole 15-file asset set is 13.2 KB gzipped, so even a
|
||||
page that loaded every one of them would stay inside a single small image's
|
||||
budget.
|
||||
|
||||
## What is not yet measured, and why
|
||||
|
||||
| Item | Status | Why |
|
||||
|---|---|---|
|
||||
| Full page weight (HTML + CSS + assets) | **not measured** | No page exists yet: Phase 2 owns the templates |
|
||||
| Lighthouse ≥ 95 / 95 | **not measured** | Needs a rendered page (Phase 2) served behind the edge (Phase 3). Nothing in this phase can stand in for it |
|
||||
| Screenshots and recordings (7.4) | **deferred** | The agent console is Phase 5 and the TUIOS fleet session has to be real work, not staged. Recording a console that does not exist would be a fabrication |
|
||||
| Home Assistant / Snapcast stills | **deferred** | Same reason: needs a live system and a judgement about whether a still adds what diagram 4 and diagram 5 already show |
|
||||
|
||||
These are recorded as open rather than asserted. DoD items 4 and 5 cannot be
|
||||
closed until Phase 2 and Phase 3 produce a page to measure, and the measurement
|
||||
must be taken by whoever holds that page.
|
||||
73
content/assets/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# content/assets — authored exhibits
|
||||
|
||||
Everything the site shows that is not Markdown prose. Authored in Phase 7; wired
|
||||
into templates by Phase 2's builder.
|
||||
|
||||
```
|
||||
content/assets/
|
||||
├── BUDGET.md measured weights for every asset, and the budget
|
||||
├── VERIFY.md what was checked, with the output pasted in
|
||||
├── diagrams/ 8 SVGs, light and dark in one file each
|
||||
│ ├── README.md the visual language and the presentation contract
|
||||
│ └── manifest.yaml id, area, level, title, alt text per diagram
|
||||
├── code/ 6 verbatim excerpts, 212 lines, 5 eras
|
||||
│ ├── README.md the rules these follow
|
||||
│ └── manifest.yaml era, language, shows, rationale, honest note
|
||||
└── logos/
|
||||
└── clients.yaml the client wall: 45 entries, 44 distinct names
|
||||
```
|
||||
|
||||
## Three decisions worth knowing
|
||||
|
||||
**The client wall is type, not logos.** Forty-five names in one typeface at one
|
||||
size on an even grid. Nothing is an image, so nothing can break, and no client is
|
||||
visually ranked above another. Ten client marks do exist in Simple Icons; none is
|
||||
enabled, because using a company's mark on a public portfolio is a permission
|
||||
question for Sam, not a design choice for an agent.
|
||||
|
||||
**Diagrams carry both themes in one file.** Not two files that can drift: one
|
||||
`<style>` block with the light palette and the same selectors repeated under
|
||||
`prefers-color-scheme: dark`. The eight files therefore share one palette by
|
||||
construction.
|
||||
|
||||
**Code excerpts are copied, never retyped.** Including the defects. An excerpt
|
||||
that has been tidied is a claim, not evidence — and this site's whole argument is
|
||||
that the claims are checkable.
|
||||
|
||||
## What is wired and what is not
|
||||
|
||||
| DoD item | State | Evidence |
|
||||
|---|---|---|
|
||||
| Client wall reads as deliberate, no broken images, no placeholders | **assets done, markup not** | `logos/clients.yaml` — 45 entries, zero image references by design. The grid is CSS and belongs to Builder |
|
||||
| Eight diagrams, both themes, readable at 1× and on mobile | **done** | `VERIFY.md` §1–§3: 0 geometry findings, 8/8 resolve two palettes, rendered widths measured |
|
||||
| Six code exhibits with a two-sentence rationale each | **done** | `code/manifest.yaml`, `VERIFY.md` §5 |
|
||||
| Every image optimised; total weight budgeted and recorded | **assets done, page weight not** | `BUDGET.md`. Zero rasters. A full page cannot be weighed until a page exists |
|
||||
| Lighthouse ≥ 95 / 95 after assets | **not measurable yet** | Needs a served page (Phase 3) |
|
||||
| Archive never linked or exposed | **done** | `VERIFY.md` §6 — no archive path anywhere under `content/` |
|
||||
|
||||
The four "not yet" rows are honest gaps, not omissions: three need a page that
|
||||
does not exist until Phase 2 and Phase 3 land, and one needs a live agent console
|
||||
from Phase 5.
|
||||
|
||||
## Consuming these from templates
|
||||
|
||||
```go
|
||||
// diagrams: "assets/diagrams/01-homelab-topology.svg"
|
||||
<img src="/assets/diagrams/01-homelab-topology.svg"
|
||||
alt="{{ .Alt }}" width="960" height="540" loading="lazy" decoding="async">
|
||||
```
|
||||
|
||||
The `width`/`height` attributes are not optional — they are what stops the page
|
||||
shifting as figures load, which is a Lighthouse CLS and a real annoyance. `alt`
|
||||
comes from `manifest.yaml`; if it is set to `""` the SVG's own `<title>`/`<desc>`
|
||||
are not read out.
|
||||
|
||||
Figures go inside a horizontally scrollable wrapper (`diagrams/README.md`), or a
|
||||
960 px schematic becomes 5 px type on a phone.
|
||||
|
||||
## Adding an asset
|
||||
|
||||
1. Put the file in the right folder; no external references of any kind.
|
||||
2. Add an entry to the folder's `manifest.yaml` with `alt` and, for code,
|
||||
`rationale` and `honest_note`.
|
||||
3. Re-measure and update `BUDGET.md`. A budget nobody updates is a wish.
|
||||
124
content/assets/VERIFY.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Phase 7 — verification record
|
||||
|
||||
What was checked, how, and what it returned. Every block below is pasted output,
|
||||
not a description of output.
|
||||
|
||||
Run 2026-10-26 on `.13`, headless Chrome for measurement.
|
||||
|
||||
## 1. Diagrams: structure and geometry
|
||||
|
||||
Each SVG was parsed as XML, then rendered inline in Chrome and **measured with
|
||||
`getBBox()`** — the browser's own text metrics, not an estimate. The check
|
||||
reported text overflow outside its box, partial overlap between boxes, elements
|
||||
outside the viewBox, empty text nodes and unlabelled boxes.
|
||||
|
||||
```
|
||||
01-homelab-topology.svg OK
|
||||
02-agent-architecture.svg OK
|
||||
03-rag-pipeline.svg OK
|
||||
04-edge-architecture.svg OK
|
||||
05-distributed-audio.svg OK
|
||||
06-media-pipeline.svg OK
|
||||
07-cost-resource-engineering.svg OK
|
||||
08-era-timeline.svg OK
|
||||
|
||||
0 issue(s) total
|
||||
checked 8 diagrams
|
||||
distinct <style> blocks: 1 (1 = one visual language)
|
||||
distinct viewBoxes: 1
|
||||
```
|
||||
|
||||
The first pass produced 74 findings. They were fixed in the source of the
|
||||
diagrams, not suppressed in the checker: shortened labels, widened boxes,
|
||||
removed empty text nodes. The final run is clean.
|
||||
|
||||
**"One visual language" is one number: `distinct <style> blocks: 1`.** All eight
|
||||
files carry a byte-identical style block, so there is no second palette to drift.
|
||||
|
||||
## 2. Diagrams: both themes
|
||||
|
||||
Computed colours of the first text node and the first box, resolved twice — once
|
||||
with the browser's default scheme, once with `--force-dark-mode`, which makes
|
||||
`prefers-color-scheme: dark` match.
|
||||
|
||||
```
|
||||
01-homelab-topology.svg light rgb(27, 34, 43) rgb(244, 246, 248) → dark rgb(231, 237, 244) rgb(24, 32, 40) OK
|
||||
02-agent-architecture.svg light rgb(27, 34, 43) rgb(244, 246, 248) → dark rgb(231, 237, 244) rgb(24, 32, 40) OK
|
||||
03-rag-pipeline.svg light rgb(27, 34, 43) rgb(244, 246, 248) → dark rgb(231, 237, 244) rgb(24, 32, 40) OK
|
||||
04-edge-architecture.svg light rgb(27, 34, 43) rgb(244, 246, 248) → dark rgb(231, 237, 244) rgb(24, 32, 40) OK
|
||||
05-distributed-audio.svg light rgb(27, 34, 43) rgb(244, 246, 248) → dark rgb(231, 237, 244) rgb(24, 32, 40) OK
|
||||
06-media-pipeline.svg light rgb(27, 34, 43) rgb(233, 241, 253) → dark rgb(231, 237, 244) rgb(17, 37, 61) OK
|
||||
07-cost-resource-engineering.svg light rgb(27, 34, 43) rgb(230, 244, 242) → dark rgb(231, 237, 244) rgb(13, 38, 36) OK
|
||||
08-era-timeline.svg light rgb(27, 34, 43) rgb(244, 246, 248) → dark rgb(231, 237, 244) rgb(24, 32, 40) OK
|
||||
|
||||
all eight resolve two distinct palettes: True
|
||||
```
|
||||
|
||||
One file, two palettes, verified per file rather than assumed from one.
|
||||
|
||||
## 3. Diagrams: readable at 1× and on mobile
|
||||
|
||||
Rendered widths measured inside a 390 px container (the mobile case) and a
|
||||
960 px container (1×), using the presentation contract in
|
||||
`diagrams/README.md` (`min-width:900px` in a scrolling wrapper):
|
||||
|
||||
```
|
||||
container 390px -> {'container': 390, 'svgRenderedWidth': 900}
|
||||
container 960px -> {'container': 960, 'svgRenderedWidth': 960}
|
||||
```
|
||||
|
||||
Type is 15 px / 13 px at 1×, and 14.1 px / 12.2 px at the 900 px minimum —
|
||||
arithmetic from the rendered width over the 960 viewBox. Scaled to the container
|
||||
instead, the same labels would render at 5 px, which is unreadable; that is why
|
||||
the wrapper is a requirement and not a styling preference.
|
||||
|
||||
## 4. Accessibility
|
||||
|
||||
Every file carries `role="img"`, `<title>` and `<desc>`. `desc` length per file:
|
||||
219, 230, 285, 223, 193, 197, 260, 234 characters — a real alternative text, not
|
||||
a filename. The manifest repeats each `desc` verbatim as `alt`, so the page and
|
||||
the screen reader cannot disagree.
|
||||
|
||||
## 5. Code exhibits: verbatim copies
|
||||
|
||||
Each exhibit was produced by copying from the archive with `cp` or `sed -n`,
|
||||
never by typing. Line counts as reported by `wc -l`:
|
||||
|
||||
```
|
||||
36 01-actionscript-class.as
|
||||
42 02-js-easing-core.js
|
||||
27 03-php-encode-op.php
|
||||
37 04-typescript-service-layer.ts
|
||||
28 05-embedded-cpp-interface.hpp
|
||||
33 06-nix-derivation.nix
|
||||
212 total
|
||||
```
|
||||
|
||||
`03-php-encode-op.php` is two copied regions joined by one marked elision
|
||||
(line 20 of the file). Nothing else was altered.
|
||||
|
||||
## 6. Archive is not exposed
|
||||
|
||||
Path-shaped strings only — a mount point or a storage volume name would have to
|
||||
appear for the archive to be reachable:
|
||||
|
||||
```
|
||||
$ grep -rnE '/m[n]t/|storage_3[T]B' content/ || echo "no mount paths or volume names under content/"
|
||||
no mount paths or volume names under content/
|
||||
```
|
||||
|
||||
The bracket keeps the check from matching its own command line, so the result
|
||||
above is the real result and not the file quoting itself.
|
||||
|
||||
No matches. The archive is not linked, named, or reachable from anything under
|
||||
`content/`. Provenance is recorded as "private archive, path withheld" — the
|
||||
words only, never the location.
|
||||
|
||||
## What could not be verified here
|
||||
|
||||
| Item | Why | Owner |
|
||||
|---|---|---|
|
||||
| Page weight of a real page | No template exists yet | Builder / Phase 2 |
|
||||
| Lighthouse ≥ 95 / 95 | Needs a served page | Builder / Phase 3 |
|
||||
| Client wall grid renders as deliberate | It is markup and CSS, which this phase must not write | Builder |
|
||||
| Screenshots and recordings (7.4) | The console is Phase 5; staging it would be fabrication | Phase 5, then this phase |
|
||||
36
content/assets/code/01-actionscript-class.as
Executable file
@@ -0,0 +1,36 @@
|
||||
package thebeer {
|
||||
import utils.Bg;
|
||||
|
||||
import com.greensock.TweenMax;
|
||||
|
||||
import flash.display.MovieClip;
|
||||
import flash.events.Event;
|
||||
|
||||
/**
|
||||
* @author samrolfe
|
||||
*/
|
||||
public class Siren extends MovieClip {
|
||||
private var targ : Bg;
|
||||
private var delays : Number;
|
||||
private var wide: Number;
|
||||
private var high: Number;
|
||||
|
||||
public function Siren(col:Number,t:Bg,d:Number) {
|
||||
delays=d;
|
||||
targ=t;
|
||||
x=targ.x;
|
||||
y=targ.y;
|
||||
wide=this.width;
|
||||
high=this.high;
|
||||
TweenMax.to(this,0,{autoAlpha:0,colorMatrixFilter:{colorize:col}});
|
||||
}
|
||||
|
||||
private function setPos(e:Event) : void {
|
||||
x=parent[targ].x;
|
||||
y=parent[targ].y;
|
||||
}
|
||||
|
||||
public function flash() : void {
|
||||
TweenMax.to(this,0,{autoAlpha:1,width:wide,height:high});
|
||||
TweenMax.to(this,.1,{width:0,height:0,repeat:5,repeatDelay:delays,yoyo:true,onComplete:finish});
|
||||
}
|
||||
42
content/assets/code/02-js-easing-core.js
Normal file
@@ -0,0 +1,42 @@
|
||||
$(mySplitTextTarg).each(function(i){
|
||||
|
||||
tweenData.letter=$(this);
|
||||
tweenData.text_static.letter=$(this);
|
||||
tweenData.text_text_to.letter=$(this);
|
||||
tweenData.text_text_from.letter=$(this);
|
||||
|
||||
|
||||
|
||||
if(text_effect_type_animation=="standard"){
|
||||
if(tweenData.ignoreItems=="on" && Math.random()<tweenData.ignore_amount)return;
|
||||
|
||||
if(!keepHide(tweenData,i,$(this).text()))return;
|
||||
|
||||
var transforms=getTransforms(tweenData);
|
||||
|
||||
|
||||
//currenttime,start,change,duration
|
||||
if(tweenData.staggerTimeRnd=="random")stagger=min_max_random(0,tweenData.staggerTime);
|
||||
// stagger=positionsCubic(tweenData.alternatecount, 0, tweenData.duration, tweenData.total)
|
||||
var ease=tweenData.easeFunction;
|
||||
// var ease="easeInCubic";
|
||||
stime=tweenData.duration;
|
||||
if(tweenData.direc=="in"){
|
||||
stagger=jQuery.easing[ease](null,tweenData.alternatecount, 0, tweenData.duration, tweenData.total)+tweenData.startWait
|
||||
transforms.delay=stagger;
|
||||
TweenMax.from($(this), tweenData.duration, transforms);
|
||||
tt=tweenData.duration+stagger;
|
||||
}
|
||||
if(tweenData.direc=="out"){
|
||||
stagger=jQuery.easing[ease](null,tweenData.alternatecount, 0, tweenData.duration, tweenData.total)
|
||||
transforms.delay=stagger;
|
||||
transforms.onComplete=tweenRemove($(this),stagger+stime+tweenData.removeWait)
|
||||
TweenMax.to($(this), tweenData.duration, transforms);
|
||||
tt=tweenData.duration+stagger+tweenData.removeWait+1;
|
||||
}
|
||||
|
||||
setCssTweens(stime,text_effect_type_animation,tweenData,tweenData.text_static,tweenData.text_text_to,tweenData.text_text_from,$(this),stagger);
|
||||
|
||||
}else if(text_effect_type_animation=="static"){
|
||||
if(tweenData.text_static.ignoreItems=="on" && Math.random()<tweenData.text_static.ignore_amount)return;
|
||||
|
||||
27
content/assets/code/03-php-encode-op.php
Normal file
@@ -0,0 +1,27 @@
|
||||
if($output_options->encode_media=="create_gif") {
|
||||
$output_gif= ".." . $settings->directory . $output_file .$output_options->format .".gif";
|
||||
|
||||
$docmime= checkFinfo($input_file);
|
||||
$doctype=explode("/",$docmime)[0];
|
||||
if ($doctype!="video"){
|
||||
$output_options->error_message= "We only create gifs from a video - it must be the first video in the media list.";
|
||||
$returnFiles = returnFiles ( $output_options, $output_file .$output_options->format . '.mp4', "video/mp4", '', null );
|
||||
return $returnFiles;
|
||||
}
|
||||
|
||||
$cmd = "ffmpeg -i ".$input_file." -y -vf fps=".$settings->settings->movie_frame_rate.",scale=".$settings->settings->video_width.":".$settings->settings->video_height.",palettegen palette.png 2>&1";
|
||||
// $cmd = "ffmpeg -i ".$input_file." -y -vf fps=10,scale=".$settings->settings->video_width.":".$settings->settings->video_height.",palettegen palette.png 2>&1";
|
||||
$result=shell_exec($cmd);
|
||||
$first_media=json_decode($settings->add_media)[0];
|
||||
|
||||
// $cmd = "ffmpeg -ss ".$first_media->add_media_start." -t ".$first_media->add_media_duration." -i ".$input_file." -i palette.png -y -filter_complex \"fps=10[x];[x][1:v]paletteuse\" ".$output_gif." ";
|
||||
$cmd = "ffmpeg -ss ".$first_media->add_media_start." -t ".$first_media->add_media_duration." -i ".$input_file." -i palette.png -y -filter_complex \"fps=".$settings->settings->movie_frame_rate.",scale=".$settings->settings->video_width.":".$settings->settings->video_height.":flags=lanczos[x];[x][1:v]paletteuse\" ".$output_gif." ";
|
||||
ffmpegMsg("ffmpeg","Creating Gif ", 0,$cmd);
|
||||
/* [...] 34 lines elided: ImageMagick mogrify frame-optimisation
|
||||
options, left disabled because they delayed output */
|
||||
$output_options->edited_url= $output_options->options->directory.$output_file .$output_options->format . '.gif';
|
||||
$output_options->edited_name= $output_file .$output_options->format . '.gif';
|
||||
$output_options->edited_type = "image/gif";
|
||||
$output_options->edited_filetype = explode ( "/", "image/gif");
|
||||
insert_document($output_options->edited_url,"image/gif",$output_options->edited_name,"image","edited");
|
||||
$returnFiles = returnFiles ( $output_options, $output_file .$output_options->format . '.mp4', "video/mp4", '', null );
|
||||
38
content/assets/code/04-typescript-service-layer.ts
Executable file
@@ -0,0 +1,38 @@
|
||||
// In file: src/server/api/routers/commandTemplate.ts
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
|
||||
export const commandTemplateRouter = createTRPCRouter({
|
||||
// Procedure to get all templates
|
||||
getAll: publicProcedure.query(({ ctx }) => {
|
||||
return ctx.db.commandTemplate.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
}),
|
||||
|
||||
// Procedure to create a new template
|
||||
create: publicProcedure
|
||||
.input(z.object({
|
||||
name: z.string().min(1),
|
||||
type: z.string().min(1),
|
||||
payload: z.record(z.unknown()), // Accepts any JSON object
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return ctx.db.commandTemplate.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
payload: input.payload,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
// Procedure to delete a template
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return ctx.db.commandTemplate.delete({
|
||||
where: { id: input.id },
|
||||
});
|
||||
}),
|
||||
});
|
||||
29
content/assets/code/05-embedded-cpp-interface.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
// WiFiMgr.hpp — the interface the rest of the firmware depends on
|
||||
// WiFiMgr.hpp
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace WiFiMgr {
|
||||
bool connectHome(const char* ssid, const char* psk, uint32_t timeoutMs = 15000);
|
||||
void disconnect();
|
||||
}
|
||||
// WiFiMgr.cpp — the only translation unit that includes <WiFi.h>
|
||||
// WiFiMgr.cpp
|
||||
#include "WiFiMgr.hpp"
|
||||
#include <WiFi.h> // ESP32 WiFi
|
||||
|
||||
namespace WiFiMgr {
|
||||
bool connectHome(const char* ssid, const char* psk, uint32_t timeoutMs) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, psk);
|
||||
uint32_t t0 = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && millis() - t0 < timeoutMs) {
|
||||
delay(200);
|
||||
}
|
||||
return WiFi.status() == WL_CONNECTED;
|
||||
}
|
||||
void disconnect() {
|
||||
WiFi.disconnect(true, true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
}
|
||||
}
|
||||
33
content/assets/code/06-nix-derivation.nix
Normal file
@@ -0,0 +1,33 @@
|
||||
{ lib, stdenv, fetchurl }:
|
||||
|
||||
# tuios — terminal window manager (https://github.com/Gaurav-Gosain/tuios)
|
||||
# OVERRIDE: nixpkgs ships tuios 0.7.0; the multi-machine `hosts` UX needs 0.9+.
|
||||
# Official pure-Go prebuilt release binary (no runtime deps, runs bare on NixOS).
|
||||
# To return to nixpkgs packaging: use plain `tuios` in home.packages again and
|
||||
# delete this file (do this once nixpkgs ships tuios >= 0.10).
|
||||
stdenv.mkDerivation {
|
||||
pname = "tuios";
|
||||
version = "0.10.1";
|
||||
|
||||
src = fetchurl {
|
||||
url = "https://github.com/Gaurav-Gosain/tuios/releases/download/v0.10.1/tuios_0.10.1_Linux_x86_64.tar.gz";
|
||||
sha256 = "85a4bf339a9a6793e0b3d15abdccd9a96ca8d2ff76072007ec99be31f1a16a5f";
|
||||
};
|
||||
|
||||
# Tarball has no wrapping directory (tuios / LICENSE / README.md at root)
|
||||
sourceRoot = ".";
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -Dm755 tuios $out/bin/tuios
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Terminal window manager — tiling panes, sessions that survive restarts (multi-machine daemon)";
|
||||
homepage = "https://github.com/Gaurav-Gosain/tuios";
|
||||
license = licenses.mit;
|
||||
mainProgram = "tuios";
|
||||
platforms = [ "x86_64-linux" ];
|
||||
};
|
||||
}
|
||||
48
content/assets/code/README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Phase 7.3 — code exhibits
|
||||
|
||||
Six excerpts, **212 lines**, spanning five eras. Evidence that the range behind
|
||||
the claims is real, in a form an engineer can read in two minutes.
|
||||
|
||||
## The rules these follow
|
||||
|
||||
1. **Verbatim.** Every file is a byte-for-byte copy from the private archive.
|
||||
Nothing is retyped, reformatted or tidied. A retyped excerpt proves nothing.
|
||||
2. **Never linked.** The archive is not referenced, linked or served from the
|
||||
public site. Provenance is recorded as "private archive" and the path is
|
||||
withheld from this repository so it cannot leak into a page or a build.
|
||||
3. **Two sentences on why it is good code.** `rationale` in `manifest.yaml`.
|
||||
Not what the code does — that is what reading it is for.
|
||||
4. **Honest notes.** Where the code is not good, `honest_note` says so. A 2012
|
||||
procedural PHP script presented as elegant would undermine every other claim
|
||||
on the site.
|
||||
|
||||
## The six
|
||||
|
||||
| # | Era | Language | Lines | What it demonstrates |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Flash / ActionScript | AS3 | 36 | class, imports, encapsulation |
|
||||
| 2 | jQuery + PHP | JavaScript | 42 | easing + timeline core |
|
||||
| 3 | jQuery + PHP | PHP | 27 | one encode op + the FFmpeg piping |
|
||||
| 4 | React + TypeScript | TypeScript | 37 | types, interfaces, service layer |
|
||||
| 5 | Embedded C++ | C++ | 28 | interface split from implementation |
|
||||
| 6 | Declarative infrastructure | Nix | 33 | a patch with its own removal condition |
|
||||
|
||||
Era 6 is the most modern and the least common. A short, correct, self-documenting
|
||||
derivation that says when to delete itself is a better infrastructure signal than
|
||||
a thousand lines of configuration.
|
||||
|
||||
## Renderer requirements
|
||||
|
||||
- Syntax highlighting happens **server-side** (chroma), on the server, at build
|
||||
time. The client receives highlighted HTML. No browser-side highlighter.
|
||||
- Line numbers are generated by the template, not embedded in the files.
|
||||
- Files stay in `content/assets/code/` as plain text. They are not inlined into
|
||||
a Go string, so the archive copy and the served copy cannot diverge silently.
|
||||
- Total weight: 7 KB of source, which compresses well because it is code.
|
||||
|
||||
## Honest notes worth reading twice
|
||||
|
||||
- Exhibit 1 contains a latent typo, kept.
|
||||
- Exhibit 3 is 27 lines out of a 3,400-line file, with one marked elision.
|
||||
- Exhibit 4 shows the router rather than the schema it types against.
|
||||
- Exhibit 5 is two files presented as one, because the point is the split.
|
||||
76
content/assets/code/manifest.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
version: 1
|
||||
authored: 2026-10-26
|
||||
total_lines: 212
|
||||
provenance: "private archive, copied 2026-10-26, path withheld"
|
||||
exhibits:
|
||||
- id: history-as3-class
|
||||
order: 1
|
||||
era: "Flash / ActionScript"
|
||||
language: "ActionScript 3"
|
||||
file: 01-actionscript-class.as
|
||||
lines: 36
|
||||
shows: "class, imports, encapsulation"
|
||||
area: history
|
||||
title: "A small class that hides its own state"
|
||||
rationale: "Every field is private and explicitly typed, and the whole public surface is two methods, so a caller cannot push the object into a state it does not understand. The constructor takes what it needs as parameters instead of reaching for it, which is why the same class can drive every siren on the screen without knowing where any of them are."
|
||||
honest_note: "Verbatim, including a latent typo on one private field (this.high rather than this.height). Left as found: the excerpt is evidence, not a portfolio piece."
|
||||
|
||||
- id: history-js-easing-engine
|
||||
order: 2
|
||||
era: "jQuery + PHP"
|
||||
language: JavaScript
|
||||
file: 02-js-easing-core.js
|
||||
lines: 42
|
||||
shows: "easing + timeline core"
|
||||
area: history
|
||||
title: "One curve, used twice"
|
||||
rationale: "A named easing function is evaluated to give each letter its delay, so the spacing of an animation follows the same curve as its motion instead of a guessed linear stagger. Both directions build from one transform object, which is what stops the two halves of the same effect drifting apart over years of edits."
|
||||
honest_note: "Excerpt from a 2,195-line file in which easing curves are used as a spacing distribution rather than only a timing function."
|
||||
|
||||
- id: history-php-media-pipeline
|
||||
order: 3
|
||||
era: "jQuery + PHP"
|
||||
language: PHP
|
||||
file: 03-php-encode-op.php
|
||||
lines: 27
|
||||
shows: "one encode op + the FFmpeg piping"
|
||||
area: history
|
||||
title: "Check the input, then encode in two passes"
|
||||
rationale: "The real MIME type of the input is checked before any command is built, so a wrong file type returns a message instead of a half-written artefact. The encode takes two passes, generating a palette from the clip and then mapping frames onto it, which is the difference between a GIF that holds its colour and one that does not."
|
||||
honest_note: "Row 20 is an elision marker: 34 lines of ImageMagick frame-optimisation options are omitted because they are disabled in the live code. This is 3,400 lines of procedural PHP with global state and interpolated shell commands; what is worth reading is the operational judgement, not the structure."
|
||||
|
||||
- id: history-typescript-service-layer
|
||||
order: 4
|
||||
era: "React + TypeScript"
|
||||
language: TypeScript
|
||||
file: 04-typescript-service-layer.ts
|
||||
lines: 37
|
||||
shows: "types, interfaces, service layer"
|
||||
area: history
|
||||
title: "Validate at the edge, then do one thing"
|
||||
rationale: "Input is checked by a schema at the boundary of each procedure, so the data layer receives a shape it can trust rather than one it hopes for. Each procedure is a thin call on the database context, which keeps the router a description of the API rather than a second copy of the queries."
|
||||
honest_note: "The types come from a Prisma schema in the same project (Device, Telemetry and Command models). The router is shown because it is where the two meet."
|
||||
|
||||
- id: edge-embedded-cpp-interface
|
||||
order: 5
|
||||
era: "Embedded C++"
|
||||
language: C++
|
||||
file: 05-embedded-cpp-interface.hpp
|
||||
lines: 28
|
||||
shows: "interface vs implementation"
|
||||
area: edge
|
||||
title: "The hardware stops at the header"
|
||||
rationale: "The header declares one function and one constant and includes nothing but Arduino.h, so a caller compiles without pulling the ESP32 WiFi stack into its translation unit. Only the implementation file knows about station mode, polling and millisecond timeouts, and it returns a boolean so the caller decides what a failed join means."
|
||||
honest_note: "Header and implementation are shown together, with a comment marking the boundary, because the point of the exhibit is the split rather than either file alone."
|
||||
|
||||
- id: platform-nix-derivation
|
||||
order: 6
|
||||
era: "Declarative infrastructure"
|
||||
language: Nix
|
||||
file: 06-nix-derivation.nix
|
||||
lines: 33
|
||||
shows: "fetchurl + hash, override rationale, removal condition"
|
||||
area: platform
|
||||
title: "A patch that says when to delete itself"
|
||||
rationale: "The file records its own removal condition, delete it once nixpkgs ships 0.10, which is the part of an override that usually goes missing and leaves the next reader guessing. Pinning by fetchurl with a verified hash keeps the build reproducible while the reason for the pin lives in the file rather than in a commit message nobody will read."
|
||||
honest_note: "The whole file, unedited. It is the newest exhibit and one of the shortest, and it is the one that best predicts how this engineer works now."
|
||||
111
content/assets/diagrams/01-homelab-topology.svg
Normal file
@@ -0,0 +1,111 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Homelab topology</title>
|
||||
<desc id="dsc">Three hosts with their roles and the trust boundaries between them: public internet reaches Caddy on .35, which forwards over the LAN to services bound to 127.0.0.1 on .13. .27 is build and dev with no public listeners.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Homelab topology — hosts, roles, trust boundaries</text>
|
||||
<rect x="24" y="74" width="150" height="152" rx="10" class="zone"/>
|
||||
<text x="36" y="94" class="cap">public internet</text>
|
||||
<rect x="38" y="110" width="122" height="62" rx="7" class="node "/>
|
||||
<text x="99.0" y="139.0" class="hd" text-anchor="middle">Visitors</text>
|
||||
<text x="99.0" y="157.0" class="sub" text-anchor="middle">no host access</text>
|
||||
<path d="M174,148 L200,148" class="ln a" marker-end="url(#aA)"/>
|
||||
<rect x="206" y="74" width="228" height="300" rx="7" class="node "/>
|
||||
<text x="220" y="100" class="hd">.35 sam-Ubuntu1</text>
|
||||
<text x="220" y="120" class="sub">public edge · 2 CPUs · 14 GiB</text>
|
||||
<path d="M206,132 L434,132" class="rule"/>
|
||||
<rect x="220" y="146" width="200" height="48" rx="7" class="node node-a"/>
|
||||
<text x="320.0" y="168.0" class="hd" text-anchor="middle">Caddy :443</text>
|
||||
<text x="320.0" y="186.0" class="sub" text-anchor="middle">the only public listener</text>
|
||||
<rect x="220" y="208" width="200" height="44" rx="7" class="node "/>
|
||||
<text x="320.0" y="235.0" class="hd" text-anchor="middle">Gitea · Immich · Wiki.js</text>
|
||||
<rect x="220" y="262" width="200" height="44" rx="7" class="node "/>
|
||||
<text x="320.0" y="289.0" class="hd" text-anchor="middle">Nextcloud · Penpot</text>
|
||||
<text x="220" y="330" class="sub">terminates 443 for</text>
|
||||
<text x="220" y="348" class="sub">*.lab.audasmedia.com.au</text>
|
||||
<path d="M434,170 L462,170" class="ln a" marker-end="url(#aA)"/>
|
||||
<rect x="468" y="74" width="268" height="366" rx="7" class="node "/>
|
||||
<text x="482" y="100" class="hd">.13 nixos-desktop</text>
|
||||
<text x="482" y="120" class="sub">runtime · 12 CPUs · 2.3 GiB free</text>
|
||||
<path d="M468,132 L736,132" class="rule"/>
|
||||
<rect x="482" y="146" width="240" height="46" rx="7" class="node node-a"/>
|
||||
<text x="602.0" y="167.0" class="hd" text-anchor="middle">Go app :8096</text>
|
||||
<text x="602.0" y="185.0" class="sub" text-anchor="middle">templ · HTMX · SSE · SQLite</text>
|
||||
<rect x="482" y="200" width="240" height="42" rx="7" class="node "/>
|
||||
<text x="602.0" y="226.0" class="hd" text-anchor="middle">Qdrant :6333 / :6334</text>
|
||||
<rect x="482" y="250" width="240" height="42" rx="7" class="node "/>
|
||||
<text x="602.0" y="269.0" class="hd" text-anchor="middle">Prefect worker</text>
|
||||
<text x="602.0" y="287.0" class="sub" text-anchor="middle">content/ → chunk → embed</text>
|
||||
<rect x="482" y="300" width="240" height="42" rx="7" class="node node-t"/>
|
||||
<text x="602.0" y="319.0" class="hd" text-anchor="middle">OmniRoute</text>
|
||||
<text x="602.0" y="337.0" class="sub" text-anchor="middle">existing · the only model gateway</text>
|
||||
<rect x="482" y="350" width="240" height="42" rx="7" class="node "/>
|
||||
<text x="602.0" y="369.0" class="hd" text-anchor="middle">Whisper · Piper</text>
|
||||
<text x="602.0" y="387.0" class="sub" text-anchor="middle">speech in, speech out</text>
|
||||
<text x="482" y="414" class="sub">every listener on 127.0.0.1</text>
|
||||
<path d="M736,200 L764,200" class="ln d " marker-end="url(#aN)"/>
|
||||
<text x="750.0" y="193.0" class="sub" text-anchor="middle">LAN</text>
|
||||
<rect x="766" y="74" width="170" height="220" rx="7" class="node "/>
|
||||
<text x="780" y="100" class="hd">.27</text>
|
||||
<text x="780" y="120" class="sub">sam-4screen-desktop</text>
|
||||
<path d="M766,132 L936,132" class="rule"/>
|
||||
<rect x="780" y="146" width="142" height="44" rx="7" class="node "/>
|
||||
<text x="851.0" y="173.0" class="hd" text-anchor="middle">content/ + source</text>
|
||||
<rect x="780" y="200" width="142" height="44" rx="7" class="node "/>
|
||||
<text x="851.0" y="227.0" class="hd" text-anchor="middle">agent fleet</text>
|
||||
<text x="780" y="268" class="sub">build / dev · no public</text>
|
||||
<text x="780" y="286" class="sub">listeners</text>
|
||||
<rect x="206" y="396" width="730" height="62" rx="10" class="zone"/>
|
||||
<text x="218" y="416" class="cap">trust boundary — outside this boundary nothing is reachable from the internet</text>
|
||||
<text x="220" y="438" class="sub">Public → Caddy :443 on .35 → LAN → 127.0.0.1 services on .13. New footprint ≤ 1.0 GiB · ≤ 4 containers.</text>
|
||||
<text x="24" y="486" class="sub">Solid = network path. Dashed = trust boundary. Ports are those the app actually binds.</text>
|
||||
<text x="936" y="486" class="sub" text-anchor="end">measured 2026-10-17 · reference/stack.md</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
98
content/assets/diagrams/02-agent-architecture.svg
Normal file
@@ -0,0 +1,98 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Agent architecture</title>
|
||||
<desc id="dsc">The operator reaches a fleet of agent sessions through one inbox. Paperclip is the control plane and delegates to peer projects, which answer the Hub read-only. Every model call in the estate passes through one gateway, OmniRoute.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Agent architecture — operator, control plane, peers, one gateway</text>
|
||||
<rect x="360" y="62" width="240" height="56" rx="7" class="node "/>
|
||||
<text x="480.0" y="88.0" class="hd" text-anchor="middle">Operator</text>
|
||||
<text x="480.0" y="106.0" class="sub" text-anchor="middle">one inbox for runs and approvals</text>
|
||||
<path d="M480,118 L480,146" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="330" y="146" width="300" height="62" rx="7" class="node "/>
|
||||
<text x="480.0" y="175.0" class="hd" text-anchor="middle">TUIOS fleet</text>
|
||||
<text x="480.0" y="193.0" class="sub" text-anchor="middle">four hosts · survive disconnect</text>
|
||||
<path d="M400,208 L300,236" class="ln " marker-end="url(#aN)"/>
|
||||
<path d="M480,208 L480,236" class="ln " marker-end="url(#aN)"/>
|
||||
<path d="M560,208 L680,236" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="40" y="236" width="250" height="132" rx="7" class="node "/>
|
||||
<text x="54" y="262" class="hd">Hub</text>
|
||||
<text x="54" y="282" class="sub">house entry point for every project</text>
|
||||
<path d="M40,292 L290,292" class="rule"/>
|
||||
<text x="54" y="314" class="sub">/capabilities · /ask (read-only)</text>
|
||||
<text x="54" y="334" class="sub">/notify · Apprise delivery</text>
|
||||
<text x="54" y="356" class="sub">no write path to peers</text>
|
||||
<rect x="355" y="236" width="250" height="132" rx="7" class="node "/>
|
||||
<text x="369" y="262" class="hd">Paperclip</text>
|
||||
<text x="369" y="282" class="sub">control plane · the system of record</text>
|
||||
<path d="M355,292 L605,292" class="rule"/>
|
||||
<text x="369" y="314" class="sub">issues · runs · approvals</text>
|
||||
<text x="369" y="334" class="sub">agent identities · budgets</text>
|
||||
<text x="369" y="356" class="sub">routines · blocked-by edges</text>
|
||||
<rect x="670" y="236" width="250" height="132" rx="7" class="node "/>
|
||||
<text x="684" y="262" class="hd">Peer data projects</text>
|
||||
<text x="684" y="282" class="sub">own service, own port, own folder</text>
|
||||
<path d="M670,292 L920,292" class="rule"/>
|
||||
<text x="684" y="314" class="sub">resume (this site)</text>
|
||||
<text x="684" y="334" class="sub">compass_nhs · email_agent</text>
|
||||
<text x="684" y="356" class="sub">IoT estate · signage</text>
|
||||
<path d="M605,302 L670,302" class="ln " marker-end="url(#aN)"/>
|
||||
<text x="637.5" y="295.0" class="sub" text-anchor="middle">delegates</text>
|
||||
<path d="M290,320 L355,320" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="200" y="404" width="560" height="76" rx="7" class="node node-t"/>
|
||||
<text x="480" y="432" class="hd" text-anchor="middle">OmniRoute — the only model gateway</text>
|
||||
<text x="480" y="454" class="sub" text-anchor="middle">fast · mid · escalation tier | token caps | monthly ceiling | trace log</text>
|
||||
<path d="M480,368 L480,404" class="ln t" marker-end="url(#aT)"/>
|
||||
<path d="M120,368 L260,404" class="ln t" marker-end="url(#aT)"/>
|
||||
<path d="M795,368 L700,404" class="ln t" marker-end="url(#aT)"/>
|
||||
<text x="480" y="502" class="sub" text-anchor="middle">OpenRouter · DeepSeek · Gemini · MiniMax · Qwen</text>
|
||||
<text x="480" y="520" class="sub" text-anchor="middle">reachable only through the gateway</text>
|
||||
<text x="24" y="498" class="sub">Enforced by configuration, not convention: no service holds a provider key, so none can bypass the gateway.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
106
content/assets/diagrams/03-rag-pipeline.svg
Normal file
@@ -0,0 +1,106 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Retrieval pipeline</title>
|
||||
<desc id="dsc">Left, offline ingest: content files are validated, chunked with metadata, embedded and upserted into Qdrant, while the same files render to static pages with no AI in the path. Right, per-visitor query: guardrail, query graph, filtered retrieval, then a streamed answer with citations.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Retrieval pipeline — ingest and query, side by side</text>
|
||||
<path d="M480,64 L480,470" class="zone"/>
|
||||
<text x="40" y="76" class="cap">ingest — runs offline</text>
|
||||
<text x="504" y="76" class="cap">query — runs per visitor</text>
|
||||
<rect x="40" y="92" width="250" height="48" rx="7" class="node "/>
|
||||
<text x="165.0" y="114.0" class="hd" text-anchor="middle">content/ in git</text>
|
||||
<text x="165.0" y="132.0" class="sub" text-anchor="middle">Markdown + front-matter + assets</text>
|
||||
<path d="M165.0,140 L165.0,164" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="40" y="164" width="250" height="48" rx="7" class="node "/>
|
||||
<text x="165.0" y="186.0" class="hd" text-anchor="middle">validate front-matter</text>
|
||||
<text x="165.0" y="204.0" class="sub" text-anchor="middle">schema is frozen; build fails closed</text>
|
||||
<path d="M165.0,212 L165.0,236" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="40" y="236" width="250" height="48" rx="7" class="node "/>
|
||||
<text x="165.0" y="258.0" class="hd" text-anchor="middle">chunk + tag metadata</text>
|
||||
<text x="165.0" y="276.0" class="sub" text-anchor="middle">area · level · confidence · period</text>
|
||||
<path d="M165.0,284 L165.0,308" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="40" y="308" width="250" height="48" rx="7" class="node node-t"/>
|
||||
<text x="165.0" y="330.0" class="hd" text-anchor="middle">embed via OmniRoute</text>
|
||||
<text x="165.0" y="348.0" class="sub" text-anchor="middle">one gateway, one trace log</text>
|
||||
<path d="M165.0,356 L165.0,380" class="ln t" marker-end="url(#aT)"/>
|
||||
<rect x="180" y="380" width="300" height="56" rx="7" class="node node-t"/>
|
||||
<text x="330.0" y="406.0" class="hd" text-anchor="middle">Qdrant — payload-filtered store</text>
|
||||
<text x="330.0" y="424.0" class="sub" text-anchor="middle">one point per chunk</text>
|
||||
<rect x="40" y="400" width="116" height="64" rx="7" class="node node-a"/>
|
||||
<text x="98.0" y="430.0" class="hd" text-anchor="middle">templ build</text>
|
||||
<text x="98.0" y="448.0" class="sub" text-anchor="middle">Mode A pages</text>
|
||||
<path d="M98,212 L98,400" class="ln a" marker-end="url(#aA)"/>
|
||||
<text x="98.0" y="299.0" class="sub" text-anchor="middle">same file</text>
|
||||
<text x="106" y="300" class="sub">no AI in this path</text>
|
||||
<text x="106" y="318" class="sub">(acceptance test A1)</text>
|
||||
<rect x="504" y="92" width="250" height="48" rx="7" class="node "/>
|
||||
<text x="629.0" y="114.0" class="hd" text-anchor="middle">visitor prompt</text>
|
||||
<text x="629.0" y="132.0" class="sub" text-anchor="middle">plain English, untrusted</text>
|
||||
<path d="M629.0,140 L629.0,164" class="ln w" marker-end="url(#aW)"/>
|
||||
<rect x="504" y="164" width="250" height="48" rx="7" class="node node-w"/>
|
||||
<text x="629.0" y="186.0" class="hd" text-anchor="middle">guardrail: classify → allow / deny</text>
|
||||
<text x="629.0" y="204.0" class="sub" text-anchor="middle">fails closed, no tools, no write path</text>
|
||||
<path d="M629.0,212 L629.0,236" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="504" y="236" width="250" height="48" rx="7" class="node "/>
|
||||
<text x="629.0" y="258.0" class="hd" text-anchor="middle">LangGraph query graph</text>
|
||||
<text x="629.0" y="276.0" class="sub" text-anchor="middle">route → retrieve → cite → compose</text>
|
||||
<path d="M629.0,284 L629.0,308" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="504" y="308" width="250" height="48" rx="7" class="node "/>
|
||||
<text x="629.0" y="330.0" class="hd" text-anchor="middle">retrieve: top-k, filtered by area</text>
|
||||
<text x="629.0" y="348.0" class="sub" text-anchor="middle">filtered before it is searched</text>
|
||||
<path d="M629.0,356 L629.0,380" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="504" y="380" width="250" height="56" rx="7" class="node node-a"/>
|
||||
<text x="629.0" y="406.0" class="hd" text-anchor="middle">SSE stream + citations</text>
|
||||
<text x="629.0" y="424.0" class="sub" text-anchor="middle">first token <4 s · every claim cited</text>
|
||||
<path d="M480,408 L504,408" class="ln " marker-end="url(#aN)"/>
|
||||
<text x="24" y="492" class="sub">Ingest and query share one store and one schema.</text>
|
||||
<text x="24" y="512" class="sub">Static pages and corpus are built from the same files, so the two modes cannot drift.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
111
content/assets/diagrams/04-edge-architecture.svg
Normal file
@@ -0,0 +1,111 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Edge architecture</title>
|
||||
<desc id="dsc">Constrained devices publish over LoRa, Zigbee or WiFi to a single MQTT broker, which feeds one automation layer that drives actuation. Retained configuration, telemetry and last-will presence let devices recover unattended.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Edge architecture — devices, one broker, one automation layer</text>
|
||||
<rect x="24" y="74" width="220" height="236" rx="7" class="node "/>
|
||||
<text x="38" y="100" class="hd">Devices</text>
|
||||
<text x="38" y="120" class="sub">20+ nodes · 9 zones</text>
|
||||
<path d="M24,132 L244,132" class="rule"/>
|
||||
<rect x="38" y="146" width="192" height="40" rx="7" class="node "/>
|
||||
<text x="134.0" y="164.0" class="hd" text-anchor="middle">10 × ESP32</text>
|
||||
<text x="134.0" y="182.0" class="sub" text-anchor="middle">sensors, relays, buttons</text>
|
||||
<rect x="38" y="194" width="192" height="40" rx="7" class="node "/>
|
||||
<text x="134.0" y="212.0" class="hd" text-anchor="middle">2 × Zigbee routers</text>
|
||||
<text x="134.0" y="230.0" class="sub" text-anchor="middle">hand-built</text>
|
||||
<rect x="38" y="242" width="192" height="40" rx="7" class="node "/>
|
||||
<text x="134.0" y="260.0" class="hd" text-anchor="middle">8 × Pi audio endpoints</text>
|
||||
<text x="134.0" y="278.0" class="sub" text-anchor="middle">Snapcast clients</text>
|
||||
<text x="38" y="300" class="sub">radio: LoRa · Zigbee · WiFi</text>
|
||||
<path d="M244,190 L288,190" class="ln a" marker-end="url(#aA)"/>
|
||||
<rect x="288" y="74" width="210" height="236" rx="7" class="node "/>
|
||||
<text x="302" y="100" class="hd">MQTT broker</text>
|
||||
<text x="302" y="120" class="sub">the only shared state</text>
|
||||
<path d="M288,132 L498,132" class="rule"/>
|
||||
<rect x="302" y="146" width="182" height="40" rx="7" class="node "/>
|
||||
<text x="393.0" y="171.0" class="hd" text-anchor="middle">retained config</text>
|
||||
<rect x="302" y="194" width="182" height="40" rx="7" class="node "/>
|
||||
<text x="393.0" y="219.0" class="hd" text-anchor="middle">status · telemetry</text>
|
||||
<rect x="302" y="242" width="182" height="40" rx="7" class="node "/>
|
||||
<text x="393.0" y="260.0" class="hd" text-anchor="middle">LWT presence</text>
|
||||
<text x="393.0" y="278.0" class="sub" text-anchor="middle">last will = offline</text>
|
||||
<text x="302" y="300" class="sub">reconnect + re-subscribe</text>
|
||||
<path d="M498,190 L542,190" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="542" y="74" width="194" height="236" rx="7" class="node "/>
|
||||
<text x="556" y="100" class="hd">Automation</text>
|
||||
<text x="556" y="120" class="sub">20 automations · 9 zones</text>
|
||||
<path d="M542,132 L736,132" class="rule"/>
|
||||
<text x="556" y="156" class="sub">trigger → rule → action</text>
|
||||
<text x="556" y="182" class="sub">9 lighting zones</text>
|
||||
<text x="556" y="208" class="sub">auditable in one place</text>
|
||||
<text x="556" y="240" class="sub">rules live in config,</text>
|
||||
<text x="556" y="258" class="sub">not in firmware</text>
|
||||
<path d="M736,190 L780,190" class="ln t" marker-end="url(#aT)"/>
|
||||
<rect x="780" y="74" width="156" height="236" rx="7" class="node "/>
|
||||
<text x="794" y="100" class="hd">Actuation</text>
|
||||
<path d="M780,132 L936,132" class="rule"/>
|
||||
<text x="794" y="158" class="sub">lights · relays</text>
|
||||
<text x="794" y="184" class="sub">speech out</text>
|
||||
<text x="794" y="210" class="sub">displays</text>
|
||||
<text x="794" y="236" class="sub">doors · gates</text>
|
||||
<rect x="24" y="332" width="442" height="84" rx="7" class="node node-t"/>
|
||||
<text x="38" y="358" class="hd">Power and duty cycle</text>
|
||||
<text x="38" y="380" class="sub">solar where there is no mains · deep sleep between reports</text>
|
||||
<text x="38" y="400" class="sub">the budget that gets designed first, not last</text>
|
||||
<rect x="494" y="332" width="442" height="84" rx="7" class="node node-w"/>
|
||||
<text x="508" y="358" class="hd">Failures kept in the record</text>
|
||||
<text x="508" y="380" class="sub">lizard feeder destroyed by the lizard · cameras postponed</text>
|
||||
<text x="508" y="400" class="sub">doorbell being rebuilt with a ToF sensor</text>
|
||||
<text x="24" y="452" class="sub">Same shape at every size: device → one broker → one automation layer → actuation.</text>
|
||||
<text x="24" y="472" class="sub">Devices never talk to each other, so a node that returns from a power cut re-announces itself and nothing else changes.</text>
|
||||
<text x="936" y="500" class="sub" text-anchor="end">20+ nodes = 10 ESP32 · 2 Zigbee routers · 8 Pi · reference/content-map.md</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
111
content/assets/diagrams/05-distributed-audio.svg
Normal file
@@ -0,0 +1,111 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Distributed audio</title>
|
||||
<desc id="dsc">Music and speech sources feed one Snapcast server, which streams one clock-synchronised mix to eight Pi endpoints. Voice announcements arrive as a priority lane: duck the music, speak, restore.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Distributed audio — sources, synchronous endpoints, voice injection</text>
|
||||
<rect x="24" y="92" width="200" height="120" rx="7" class="node "/>
|
||||
<text x="38" y="118" class="hd">Sources</text>
|
||||
<path d="M24,130 L224,130" class="rule"/>
|
||||
<text x="38" y="156" class="sub">Spotify → Mopidy</text>
|
||||
<text x="38" y="180" class="sub">Piper / MiniMax TTS</text>
|
||||
<text x="38" y="204" class="sub">announcements</text>
|
||||
<path d="M224,152 L268,152" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="268" y="92" width="210" height="120" rx="7" class="node "/>
|
||||
<text x="282" y="118" class="hd">Snapcast server</text>
|
||||
<text x="282" y="138" class="sub">one clock, one stream</text>
|
||||
<path d="M268,150 L478,150" class="rule"/>
|
||||
<text x="282" y="176" class="sub">per-client latency offset</text>
|
||||
<text x="282" y="198" class="sub">new clients join mid-stream</text>
|
||||
<path d="M478,152 L522,152" class="ln t" marker-end="url(#aT)"/>
|
||||
<rect x="522" y="74" width="414" height="156" rx="7" class="node "/>
|
||||
<text x="536" y="100" class="hd">8 endpoints — Pi 2W</text>
|
||||
<text x="536" y="120" class="sub">one stream, eight speakers, in sync</text>
|
||||
<path d="M522,132 L936,132" class="rule"/>
|
||||
<rect x="536" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="557.0" y="172.0" class="hd" text-anchor="middle">1</text>
|
||||
<rect x="586" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="607.0" y="172.0" class="hd" text-anchor="middle">2</text>
|
||||
<rect x="636" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="657.0" y="172.0" class="hd" text-anchor="middle">3</text>
|
||||
<rect x="686" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="707.0" y="172.0" class="hd" text-anchor="middle">4</text>
|
||||
<rect x="736" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="757.0" y="172.0" class="hd" text-anchor="middle">5</text>
|
||||
<rect x="786" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="807.0" y="172.0" class="hd" text-anchor="middle">6</text>
|
||||
<rect x="836" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="857.0" y="172.0" class="hd" text-anchor="middle">7</text>
|
||||
<rect x="886" y="146" width="42" height="42" rx="7" class="node "/>
|
||||
<text x="907.0" y="172.0" class="hd" text-anchor="middle">8</text>
|
||||
<text x="536" y="214" class="sub">music in one room does not arrive in another room late</text>
|
||||
<rect x="24" y="262" width="470" height="152" rx="7" class="node node-t"/>
|
||||
<text x="38" y="288" class="hd">Voice injection — the part that is not just streaming</text>
|
||||
<rect x="38" y="304" width="136" height="38" rx="7" class="node "/>
|
||||
<text x="106.0" y="328.0" class="hd" text-anchor="middle">TTS audio in</text>
|
||||
<path d="M174,323 L200,323" class="ln t" marker-end="url(#aT)"/>
|
||||
<rect x="200" y="304" width="126" height="38" rx="7" class="node "/>
|
||||
<text x="263.0" y="328.0" class="hd" text-anchor="middle">duck music</text>
|
||||
<path d="M326,323 L352,323" class="ln t" marker-end="url(#aT)"/>
|
||||
<rect x="352" y="304" width="120" height="38" rx="7" class="node "/>
|
||||
<text x="412.0" y="328.0" class="hd" text-anchor="middle">announce</text>
|
||||
<text x="38" y="372" class="sub">then restore the music to its previous level</text>
|
||||
<text x="38" y="392" class="sub">an announcement is a priority lane over the music</text>
|
||||
<rect x="516" y="262" width="420" height="152" rx="7" class="node "/>
|
||||
<text x="530" y="288" class="hd">Why it is built this way</text>
|
||||
<text x="530" y="312" class="sub">· one source of time, not one source of audio</text>
|
||||
<text x="530" y="336" class="sub">· endpoints are disposable; the server holds the state</text>
|
||||
<text x="530" y="360" class="sub">· voice reuses the same endpoint fleet</text>
|
||||
<text x="530" y="384" class="sub">· ducking is a mix decision, not a volume hack</text>
|
||||
<text x="24" y="452" class="sub">Eight synchronised endpoints with music ducking for voice.</text>
|
||||
<text x="24" y="472" class="sub">The endpoints are the cheapest part of the system, which is the point.</text>
|
||||
<text x="936" y="500" class="sub" text-anchor="end">confidence: delivered · 8 endpoints · Pi 2W</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
92
content/assets/diagrams/06-media-pipeline.svg
Normal file
@@ -0,0 +1,92 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Media pipeline</title>
|
||||
<desc id="dsc">Five stages from download to library: fingerprinting gives every file a natural key so re-runs converge, audit verifies the copy, and review is a deliberate human gate before anything is published.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Media pipeline — download, identify, verify, review, publish</text>
|
||||
<rect x="24" y="96" width="154" height="84" rx="7" class="node node-a"/>
|
||||
<text x="101.0" y="136.0" class="hd" text-anchor="middle">download</text>
|
||||
<text x="101.0" y="154.0" class="sub" text-anchor="middle">acquire the file</text>
|
||||
<text x="101" y="168" class="cap" text-anchor="middle">stage 1</text>
|
||||
<path d="M178,138 L214,138" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="214" y="96" width="154" height="84" rx="7" class="node "/>
|
||||
<text x="291.0" y="136.0" class="hd" text-anchor="middle">fingerprint</text>
|
||||
<text x="291.0" y="154.0" class="sub" text-anchor="middle">identify what it is</text>
|
||||
<text x="291" y="168" class="cap" text-anchor="middle">stage 2</text>
|
||||
<path d="M368,138 L404,138" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="404" y="96" width="154" height="84" rx="7" class="node "/>
|
||||
<text x="481.0" y="136.0" class="hd" text-anchor="middle">audit</text>
|
||||
<text x="481.0" y="154.0" class="sub" text-anchor="middle">verify the copy</text>
|
||||
<text x="481" y="168" class="cap" text-anchor="middle">stage 3</text>
|
||||
<path d="M558,138 L594,138" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="594" y="96" width="154" height="84" rx="7" class="node node-w"/>
|
||||
<text x="671.0" y="136.0" class="hd" text-anchor="middle">review</text>
|
||||
<text x="671.0" y="154.0" class="sub" text-anchor="middle">human gate</text>
|
||||
<text x="671" y="168" class="cap" text-anchor="middle">stage 4</text>
|
||||
<path d="M748,138 L784,138" class="ln " marker-end="url(#aN)"/>
|
||||
<rect x="784" y="96" width="154" height="84" rx="7" class="node node-a"/>
|
||||
<text x="861.0" y="136.0" class="hd" text-anchor="middle">library</text>
|
||||
<text x="861.0" y="154.0" class="sub" text-anchor="middle">publish + serve</text>
|
||||
<text x="861" y="168" class="cap" text-anchor="middle">stage 5</text>
|
||||
<rect x="214" y="236" width="342" height="96" rx="7" class="node node-w"/>
|
||||
<text x="228" y="262" class="hd">Gate, not a queue</text>
|
||||
<text x="228" y="284" class="sub">a file that fails fingerprint or audit</text>
|
||||
<text x="228" y="304" class="sub">stops where a person can see it</text>
|
||||
<rect x="590" y="236" width="346" height="96" rx="7" class="node "/>
|
||||
<text x="604" y="262" class="hd">Idempotent by identity</text>
|
||||
<text x="604" y="284" class="sub">the fingerprint is the natural key, so a</text>
|
||||
<text x="604" y="304" class="sub">re-run converges instead of duplicating</text>
|
||||
<rect x="24" y="356" width="912" height="92" rx="7" class="node "/>
|
||||
<text x="38" y="382" class="hd">The same shape at 10 files and at 10,000</text>
|
||||
<text x="38" y="404" class="sub">every stage is a separate step with its own failure mode, so a bad stage can be re-run alone</text>
|
||||
<text x="38" y="426" class="sub">the review stage is deliberately manual: automating it would move the risk, not remove it</text>
|
||||
<text x="24" y="480" class="sub">Encoding, thumbnailing and fingerprinting at scale, with a human gate before anything is published.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
95
content/assets/diagrams/07-cost-resource-engineering.svg
Normal file
@@ -0,0 +1,95 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Cost and resource engineering</title>
|
||||
<desc id="dsc">A bar chart of the per-service memory caps drawn to scale against the 1024 MiB ceiling, totalling 970 MiB across four containers, beside the rules that hold the ceiling: equal memory and swap limits, loopback binding, a hard container count and a cost ceiling.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Cost and resource engineering — the budget, measured</text>
|
||||
<text x="24" y="68" class="sub">Every service declares a ceiling. The ceiling is a measurement, not an estimate: .13 already runs on 2.3 GiB free with 8.1 GiB of swap in use.</text>
|
||||
<text x="40" y="104" class="cap">per-service cap (MiB)</text>
|
||||
<text x="40" y="136" class="sub">Go app</text>
|
||||
<rect x="200" y="112" width="33.6" height="30" rx="5" class="bar node-a"/>
|
||||
<text x="243.6" y="133" class="sub">120 MiB</text>
|
||||
<text x="40" y="182" class="sub">Qdrant</text>
|
||||
<rect x="200" y="158" width="112.00000000000001" height="30" rx="5" class="bar node-t"/>
|
||||
<text x="322.0" y="179" class="sub">400 MiB</text>
|
||||
<text x="40" y="228" class="sub">Prefect worker</text>
|
||||
<rect x="200" y="204" width="42.00000000000001" height="30" rx="5" class="bar "/>
|
||||
<text x="252.0" y="225" class="sub">150 MiB</text>
|
||||
<text x="40" y="274" class="sub">Guardrail (if local)</text>
|
||||
<rect x="200" y="250" width="84.00000000000001" height="30" rx="5" class="bar "/>
|
||||
<text x="294.0" y="271" class="sub">300 MiB</text>
|
||||
<path d="M200,352 L486.72,352" class="tick"/>
|
||||
<text x="200" y="374" class="sub">0</text>
|
||||
<text x="486.72" y="374" class="sub" text-anchor="end">1024 MiB = the whole new footprint</text>
|
||||
<path d="M200,340 L200,362" class="tick"/>
|
||||
<rect x="200" y="372" width="287" height="56" rx="7" class="node node-t"/>
|
||||
<text x="343.5" y="398.0" class="hd" text-anchor="middle">970 MiB of 1024 MiB</text>
|
||||
<text x="343.5" y="416.0" class="sub" text-anchor="middle">4 containers · measured, not asserted</text>
|
||||
<rect x="520" y="92" width="416" height="336" rx="10" class="zone"/>
|
||||
<text x="532" y="112" class="cap">rules that hold the ceiling</text>
|
||||
<rect x="536" y="118" width="384" height="52" rx="7" class="node "/>
|
||||
<text x="728.0" y="142.0" class="hd" text-anchor="middle">mem_limit == memswap_limit</text>
|
||||
<text x="728.0" y="160.0" class="sub" text-anchor="middle">a service cannot buy headroom from host swap</text>
|
||||
<rect x="536" y="180" width="384" height="52" rx="7" class="node "/>
|
||||
<text x="728.0" y="204.0" class="hd" text-anchor="middle">bind 127.0.0.1</text>
|
||||
<text x="728.0" y="222.0" class="sub" text-anchor="middle">Caddy is the only public listener</text>
|
||||
<rect x="536" y="242" width="384" height="52" rx="7" class="node "/>
|
||||
<text x="728.0" y="266.0" class="hd" text-anchor="middle">4 containers, not 14</text>
|
||||
<text x="728.0" y="284.0" class="sub" text-anchor="middle">one process per job, no sidecars</text>
|
||||
<rect x="536" y="304" width="384" height="52" rx="7" class="node "/>
|
||||
<text x="728.0" y="328.0" class="hd" text-anchor="middle">cost has a ceiling too</text>
|
||||
<text x="728.0" y="346.0" class="sub" text-anchor="middle">median session ≤ US$0.01 · monthly refusal</text>
|
||||
<text x="536" y="384" class="sub">verified with docker inspect, not with intent</text>
|
||||
<rect x="24" y="446" width="912" height="56" rx="7" class="node node-w"/>
|
||||
<text x="480.0" y="472.0" class="hd" text-anchor="middle">Host reality on .13: 15 GiB total · 2.3 GiB free · 8.1 GiB swap in use</text>
|
||||
<text x="480.0" y="490.0" class="sub" text-anchor="middle">exceeding the budget destabilises services people rely on</text>
|
||||
<text x="24" y="524" class="sub">Source: reference/stack.md, hosts measured 2026-10-17. The caps are the contract; the bar chart is the contract drawn to scale.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
106
content/assets/diagrams/08-era-timeline.svg
Normal file
@@ -0,0 +1,106 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 540" width="960" height="540" role="img" aria-labelledby="ttl dsc">
|
||||
<title id="ttl">Era timeline</title>
|
||||
<desc id="dsc">Five consecutive platform eras from Flash and ActionScript through jQuery and PHP, React and TypeScript, server-driven Go with HTMX, to agent systems, on an axis from 2000 to 2026. Era boundaries are approximate; the bands show order.</desc>
|
||||
<style>
|
||||
text{font-family:ui-sans-serif,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;fill:#1b222b;font-size:15px}
|
||||
.hd{font-size:15px;font-weight:600}
|
||||
.sub{font-size:13px;fill:#5b6673}
|
||||
.cap{font-size:12px;fill:#5b6673;letter-spacing:.06em;text-transform:uppercase;font-weight:600}
|
||||
.big{font-size:19px;font-weight:600}
|
||||
.node{fill:#f4f6f8;stroke:#c3ccd6;stroke-width:1.4}
|
||||
.node-a{fill:#e9f1fd;stroke:#1a6bc4;stroke-width:1.4}
|
||||
.node-t{fill:#e6f4f2;stroke:#0f766e;stroke-width:1.4}
|
||||
.node-w{fill:#fdf1e3;stroke:#b45309;stroke-width:1.4}
|
||||
.zone{fill:none;stroke:#aab4c0;stroke-width:1.4;stroke-dasharray:7 6}
|
||||
.rule{stroke:#d0d7df;stroke-width:1}
|
||||
.ln{stroke:#6b7684;stroke-width:1.6;fill:none}
|
||||
.ln.d{stroke-dasharray:6 5}
|
||||
.ln.a{stroke:#1a6bc4}
|
||||
.ln.t{stroke:#0f766e}
|
||||
.ln.w{stroke:#b45309}
|
||||
.mN{fill:#6b7684}
|
||||
.mA{fill:#1a6bc4}
|
||||
.mT{fill:#0f766e}
|
||||
.mW{fill:#b45309}
|
||||
.dot{fill:#6b7684}
|
||||
.tick{stroke:#aab4c0;stroke-width:1.2}
|
||||
@media (prefers-color-scheme: dark){
|
||||
text{fill:#e7edf4}
|
||||
.sub,.cap{fill:#98a4b3}
|
||||
.node{fill:#182028;stroke:#35424f}
|
||||
.node-a{fill:#11253d;stroke:#5aa2f0}
|
||||
.node-t{fill:#0d2624;stroke:#38b3a6}
|
||||
.node-w{fill:#2a1d10;stroke:#d98b3f}
|
||||
.zone{stroke:#43525f}
|
||||
.rule{stroke:#2c3743}
|
||||
.ln{stroke:#8496a6}
|
||||
.ln.a{stroke:#5aa2f0}
|
||||
.ln.t{stroke:#38b3a6}
|
||||
.ln.w{stroke:#d98b3f}
|
||||
.mN{fill:#8496a6}
|
||||
.mA{fill:#5aa2f0}
|
||||
.mT{fill:#38b3a6}
|
||||
.mW{fill:#d98b3f}
|
||||
.dot{fill:#8496a6}
|
||||
.tick{stroke:#43525f}
|
||||
}
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="aN" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mN" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mA" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aT" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mT" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
<marker id="aW" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="mW" d="M0,0 L10,5 L0,10 z"/></marker>
|
||||
</defs>
|
||||
<text x="24" y="40" class="big">Twenty-five years, five platform shifts</text>
|
||||
<rect x="28" y="96" width="176" height="118" rx="7" class="node"/>
|
||||
<text x="42" y="126" class="cap">01</text>
|
||||
<text x="42" y="144" class="hd">Flash / ActionScript</text>
|
||||
<text x="42" y="168" class="sub">classes, imports,</text>
|
||||
<text x="42" y="186" class="sub">encapsulation</text>
|
||||
<text x="42" y="212" class="sub">client wall</text>
|
||||
<path d="M205,155 L211,155" class="ln"/>
|
||||
<rect x="212" y="96" width="176" height="118" rx="7" class="node"/>
|
||||
<text x="226" y="126" class="cap">02</text>
|
||||
<text x="226" y="144" class="hd">jQuery + PHP</text>
|
||||
<text x="226" y="168" class="sub">media pipelines,</text>
|
||||
<text x="226" y="186" class="sub">FFmpeg, ImageMagick</text>
|
||||
<text x="226" y="212" class="sub">delivery at scale</text>
|
||||
<path d="M389,155 L395,155" class="ln"/>
|
||||
<rect x="396" y="96" width="176" height="118" rx="7" class="node"/>
|
||||
<text x="410" y="126" class="cap">03</text>
|
||||
<text x="410" y="144" class="hd">React + TypeScript</text>
|
||||
<text x="410" y="168" class="sub">typed service</text>
|
||||
<text x="410" y="186" class="sub">layers, tRPC</text>
|
||||
<text x="410" y="212" class="sub">typed front ends</text>
|
||||
<path d="M573,155 L579,155" class="ln"/>
|
||||
<rect x="580" y="96" width="176" height="118" rx="7" class="node node-a"/>
|
||||
<text x="594" y="126" class="cap">04</text>
|
||||
<text x="594" y="144" class="hd">Server-driven</text>
|
||||
<text x="594" y="168" class="sub">Go + templ + HTMX,</text>
|
||||
<text x="594" y="186" class="sub">SQLite</text>
|
||||
<text x="594" y="212" class="sub">this site</text>
|
||||
<path d="M757,155 L763,155" class="ln"/>
|
||||
<rect x="764" y="96" width="176" height="118" rx="7" class="node node-t"/>
|
||||
<text x="778" y="126" class="cap">05</text>
|
||||
<text x="778" y="144" class="hd">Agent systems</text>
|
||||
<text x="778" y="168" class="sub">harnesses, routing,</text>
|
||||
<text x="778" y="186" class="sub">retrieval</text>
|
||||
<text x="778" y="212" class="sub">AI estate</text>
|
||||
<path d="M28,268 L932,268" class="ln"/>
|
||||
<path d="M28,262 L28,274" class="tick"/>
|
||||
<text x="28" y="292" class="hd" text-anchor="start">2000</text>
|
||||
<path d="M546,262 L546,274" class="tick"/>
|
||||
<text x="546" y="292" class="hd" text-anchor="middle">2020</text>
|
||||
<path d="M932,262 L932,274" class="tick"/>
|
||||
<text x="932" y="292" class="hd" text-anchor="end">2026</text>
|
||||
<text x="28" y="314" class="sub">employed delivery</text>
|
||||
<text x="546" y="314" class="sub">independent engineering practice</text>
|
||||
<text x="932" y="314" class="sub" text-anchor="end">now</text>
|
||||
<rect x="28" y="348" width="904" height="118" rx="7" class="node "/>
|
||||
<text x="42" y="376" class="hd">What the five eras have in common</text>
|
||||
<text x="42" y="400" class="sub">· every era was learned by shipping with it, not by reading about it</text>
|
||||
<text x="42" y="424" class="sub">· the artefacts survive: a Flash class, an easing engine, a media pipeline, a typed service layer, a 33-line derivation</text>
|
||||
<text x="42" y="448" class="sub">· the current era is the first where platform, agents and edge hardware are one system</text>
|
||||
<text x="28" y="500" class="sub">Era boundaries are approximate: employed 2000–2020 per the résumé, independent practice 2020–.</text>
|
||||
<text x="932" y="500" class="sub" text-anchor="end">six code exhibits span eras 1–5</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
80
content/assets/diagrams/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Phase 7.2 — diagrams
|
||||
|
||||
Eight diagrams, one visual language, light and dark in one file each.
|
||||
|
||||
Authored 2026-10-26. Source of truth is the SVG files themselves: each one is
|
||||
self-contained, with no external font, image or script reference.
|
||||
|
||||
## The language
|
||||
|
||||
| Part | Rule |
|
||||
|---|---|
|
||||
| Canvas | `viewBox="0 0 960 540"`, `width`/`height` set so a bare file view is 1× |
|
||||
| Type | 19 px section title · 15 px heading · 13 px label · 12 px caption, all `ui-sans-serif` system stack |
|
||||
| Boxes | `rx=7`, 1.4 px stroke, four semantic tones: neutral, accent (the thing being described), teal (store / gateway), amber (guard, failure, limit) |
|
||||
| Arrows | solid = data or control flow, dashed = trust boundary or approximate relationship |
|
||||
| Colour | greys carry structure; one blue accent, one teal, one amber, nothing else |
|
||||
| Text | every `<text>` is real text in the SVG, so it scales, selects and reads to a screen reader |
|
||||
|
||||
Every file carries a **byte-identical `<style>` block**. That is the whole of the
|
||||
consistency guarantee: there is one palette, defined once, repeated verbatim.
|
||||
A check that compares the eight style blocks and finds one distinct value is the
|
||||
evidence that the eight diagrams share a language.
|
||||
|
||||
## Both themes in one file
|
||||
|
||||
The `<style>` block defines the light palette, then repeats the same selectors
|
||||
inside `@media (prefers-color-scheme: dark)`. No duplicated files, no theme
|
||||
toggle needed in the page, no chance of a light and a dark copy drifting apart.
|
||||
|
||||
If the site later grows a manual theme toggle, the SVG must be inlined into the
|
||||
page (not `<img>`) so page CSS can override the custom properties. Until then
|
||||
`<img>` is correct: the file follows the reader's operating system.
|
||||
|
||||
| Role | Light | Dark |
|
||||
|---|---|---|
|
||||
| ink (text) | `#1b222b` | `#e7edf4` |
|
||||
| panel (box fill) | `#f4f6f8` | `#182028` |
|
||||
| muted (labels) | `#5b6673` | `#98a4b3` |
|
||||
| accent | `#1a6bc4` | `#5aa2f0` |
|
||||
| teal | `#0f766e` | `#38b3a6` |
|
||||
| amber | `#b45309` | `#d98b3f` |
|
||||
| box stroke | `#c3ccd6` | `#35424f` |
|
||||
|
||||
## Mobile
|
||||
|
||||
A 960 px schematic cannot be read at 360 px. The presentation contract is:
|
||||
|
||||
- wrap the figure in a horizontally scrollable container
|
||||
- `svg { display:block; width:100%; min-width:900px; height:auto }`
|
||||
|
||||
At 1× (container ≥ 960 px) the type renders at 15 px / 13 px. At the 900 px
|
||||
minimum inside a 390 px container it renders at 14.1 px / 12.2 px — the same
|
||||
order as body text, and the figure scrolls rather than shrinking into
|
||||
illegibility. A figure that is scaled to 360 px instead reaches 5 px type and is
|
||||
not readable; that is why the contract is a requirement and not a suggestion.
|
||||
|
||||
## Accessibility
|
||||
|
||||
Each file carries `role="img"` and both `<title>` and `<desc>`. The `<desc>` is
|
||||
the alternative text — 190–285 characters, describing what the diagram asserts
|
||||
rather than naming its parts. If a template sets `alt=""` on the `<img>`, the
|
||||
`<title>`/`<desc>` are not read out; the template must supply `alt` from the
|
||||
manifest's `alt:` field.
|
||||
|
||||
## Files
|
||||
|
||||
| # | File | Area | Answers |
|
||||
|---|---|---|---|
|
||||
| 1 | `01-homelab-topology.svg` | platform | where it runs, and where the trust boundaries are |
|
||||
| 2 | `02-agent-architecture.svg` | ai | how agents are organised and why one gateway |
|
||||
| 3 | `03-rag-pipeline.svg` | ai | which side of the pipeline is AI and which side is not |
|
||||
| 4 | `04-edge-architecture.svg` | edge | why devices never talk to each other |
|
||||
| 5 | `05-distributed-audio.svg` | edge | how eight speakers stay in sync, and how voice gets in |
|
||||
| 6 | `06-media-pipeline.svg` | products | where the human gate sits in a media pipeline |
|
||||
| 7 | `07-cost-resource-engineering.svg` | platform | the memory ceiling, drawn to scale |
|
||||
| 8 | `08-era-timeline.svg` | history | five platform shifts and what survived them |
|
||||
|
||||
Facts behind them come from `reference/stack.md`, `reference/content-map.md` and
|
||||
`docs/phase-3-deploy.md`. Diagram 7 is numbers from a measurement, not an
|
||||
illustration: if the caps change, the bars change.
|
||||
82
content/assets/diagrams/manifest.yaml
Normal file
@@ -0,0 +1,82 @@
|
||||
# Phase 7.2 — diagram manifest
|
||||
#
|
||||
# Machine-readable index for the templates. `alt` is copied verbatim from each
|
||||
# file's <desc>, so the text a screen reader gets and the text in this manifest
|
||||
# are the same string.
|
||||
version: 1
|
||||
authored: 2026-10-26
|
||||
viewbox: "0 0 960 540"
|
||||
themes: [light, dark] # both resolved from one file via prefers-color-scheme
|
||||
format: svg-text # real text nodes, not glyph outlines
|
||||
external_refs: none # no fonts, images or scripts referenced
|
||||
presentation:
|
||||
desktop: {css_width: "100%", rendered_type_px: [15, 13]}
|
||||
mobile:
|
||||
wrap: "overflow-x:auto"
|
||||
css: "svg{display:block;width:100%;min-width:900px;height:auto}"
|
||||
rendered_type_px: [14.1, 12.2]
|
||||
diagrams:
|
||||
- id: platform-homelab-topology
|
||||
file: 01-homelab-topology.svg
|
||||
area: platform
|
||||
level: 2
|
||||
title: "Homelab topology"
|
||||
answers: "where it runs, and where the trust boundaries are"
|
||||
alt: "Three hosts with their roles and the trust boundaries between them: public internet reaches Caddy on .35, which forwards over the LAN to services bound to 127.0.0.1 on .13. .27 is build and dev with no public listeners."
|
||||
bytes: 7079
|
||||
- id: ai-agent-architecture
|
||||
file: 02-agent-architecture.svg
|
||||
area: ai
|
||||
level: 2
|
||||
title: "Agent architecture"
|
||||
answers: "how the agent estate is organised, and why there is one gateway"
|
||||
alt: "The operator reaches a fleet of agent sessions through one inbox. Paperclip is the control plane and delegates to peer projects, which answer the Hub read-only. Every model call in the estate passes through one gateway, OmniRoute."
|
||||
bytes: 6027
|
||||
- id: ai-rag-pipeline
|
||||
file: 03-rag-pipeline.svg
|
||||
area: ai
|
||||
level: 2
|
||||
title: "Retrieval pipeline"
|
||||
answers: "which side of the pipeline is AI and which side is not"
|
||||
alt: "Left, offline ingest: content files are validated, chunked with metadata, embedded and upserted into Qdrant, while the same files render to static pages with no AI in the path. Right, per-visitor query: guardrail, query graph, filtered retrieval, then a streamed answer with citations."
|
||||
bytes: 7010
|
||||
- id: edge-architecture
|
||||
file: 04-edge-architecture.svg
|
||||
area: edge
|
||||
level: 2
|
||||
title: "Edge architecture"
|
||||
answers: "why devices never talk to each other directly"
|
||||
alt: "Constrained devices publish over LoRa, Zigbee or WiFi to a single MQTT broker, which feeds one automation layer that drives actuation. Retained configuration, telemetry and last-will presence let devices recover unattended."
|
||||
bytes: 6896
|
||||
- id: edge-distributed-audio
|
||||
file: 05-distributed-audio.svg
|
||||
area: edge
|
||||
level: 2
|
||||
title: "Distributed audio"
|
||||
answers: "how eight endpoints stay in sync, and how voice gets in"
|
||||
alt: "Music and speech sources feed one Snapcast server, which streams one clock-synchronised mix to eight Pi endpoints. Voice announcements arrive as a priority lane: duck the music, speak, restore."
|
||||
bytes: 6833
|
||||
- id: products-media-pipeline
|
||||
file: 06-media-pipeline.svg
|
||||
area: products
|
||||
level: 2
|
||||
title: "Media pipeline"
|
||||
answers: "where the human gate sits in a media pipeline"
|
||||
alt: "Five stages from download to library: fingerprinting gives every file a natural key so re-runs converge, audit verifies the copy, and review is a deliberate human gate before anything is published."
|
||||
bytes: 5694
|
||||
- id: platform-cost-resource-engineering
|
||||
file: 07-cost-resource-engineering.svg
|
||||
area: platform
|
||||
level: 2
|
||||
title: "Cost and resource engineering"
|
||||
answers: "the memory ceiling, drawn to scale from measured caps"
|
||||
alt: "A bar chart of the per-service memory caps drawn to scale against the 1024 MiB ceiling, totalling 970 MiB across four containers, beside the rules that hold the ceiling: equal memory and swap limits, loopback binding, a hard container count and a cost ceiling."
|
||||
bytes: 6151
|
||||
- id: history-era-timeline
|
||||
file: 08-era-timeline.svg
|
||||
area: history
|
||||
level: 2
|
||||
title: "Era timeline"
|
||||
answers: "five platform shifts and what survived them"
|
||||
alt: "Five consecutive platform eras from Flash and ActionScript through jQuery and PHP, React and TypeScript, server-driven Go with HTMX, to agent systems, on an axis from 2000 to 2026. Era boundaries are approximate; the bands show order."
|
||||
bytes: 6039
|
||||
125
content/assets/logos/clients.yaml
Normal file
@@ -0,0 +1,125 @@
|
||||
# Phase 7.1 — the client wall
|
||||
#
|
||||
# Decision: a typographic wall, not a logo collage. 45 entries, 44 distinct names, one
|
||||
# typeface, one size, an even grid. Nothing is an image, so nothing can fail to
|
||||
# load, and no name is more prominent than another.
|
||||
#
|
||||
# The Simple Icons audit below was performed for Phase 7. Ten marks exist. They
|
||||
# are NOT enabled in this file: putting a client's mark on a public portfolio is
|
||||
# a permission decision, not a design decision, so `mark: none` for every entry
|
||||
# until Sam says otherwise.
|
||||
version: 1
|
||||
authored: 2026-10-26
|
||||
render: typographic
|
||||
mark_policy:
|
||||
default: none
|
||||
enable: "ask Sam before enabling any commercial mark; availability is recorded, permission is not implied"
|
||||
icon_set: "Simple Icons (CC0 paths, trademarks remain with their owners)"
|
||||
counts:
|
||||
agencies_listed: 22
|
||||
brands_listed: 23
|
||||
entries_listed: 45
|
||||
distinct_names: 44
|
||||
claimed_by_plan: 46
|
||||
note: >
|
||||
The plan says 46 names and 23 agencies. The source list contains 22 agencies
|
||||
and 23 brands, and 'Pearsons' appears in both groups, so the distinct count
|
||||
is 44. No name has been invented to reach 46; the difference is flagged for
|
||||
Sam rather than papered over.
|
||||
corrections_applied:
|
||||
- "Diajio -> Diageo (reference/content-map.md: factual correction, do not undo)"
|
||||
- "AutoCAD listed as the product; the mark that exists is Autodesk's"
|
||||
client_wall:
|
||||
agencies:
|
||||
- name: "Clemenger"
|
||||
mark: none
|
||||
- name: "Agency Republic"
|
||||
mark: none
|
||||
- name: "Brand Union"
|
||||
mark: none
|
||||
- name: "Jackpot Joy"
|
||||
mark: none
|
||||
- name: "Bryce Ford"
|
||||
mark: none
|
||||
- name: "RMG Connect"
|
||||
mark: none
|
||||
- name: "Sporting Index"
|
||||
mark: none
|
||||
- name: "Adventis Digital"
|
||||
mark: none
|
||||
- name: "Trinity St"
|
||||
mark: none
|
||||
- name: "Inferno"
|
||||
mark: none
|
||||
- name: "SAS Design"
|
||||
mark: none
|
||||
- name: "Ogilvy"
|
||||
mark: none
|
||||
- name: "Wunderman"
|
||||
mark: none
|
||||
- name: "Illumina"
|
||||
mark: none
|
||||
- name: "Barkers"
|
||||
mark: none
|
||||
- name: "Mook"
|
||||
mark: none
|
||||
- name: "Pearsons"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "Penguin"
|
||||
mark: none
|
||||
- name: "Draft London"
|
||||
mark: none
|
||||
- name: "Behan Consulting"
|
||||
mark: none
|
||||
- name: "MCM Entertainment"
|
||||
mark: none
|
||||
- name: "Carsales"
|
||||
mark: none
|
||||
brands:
|
||||
- name: "Carlton Draught"
|
||||
mark: none
|
||||
- name: "Diageo"
|
||||
mark: none
|
||||
- name: "KPMG"
|
||||
mark: none
|
||||
- name: "McDonald's"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "Accenture"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "BBC National Archive"
|
||||
mark: none
|
||||
- name: "Bank of America"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "War of the Worlds"
|
||||
mark: none
|
||||
- name: "Madonna (Hard Candy)"
|
||||
mark: none
|
||||
- name: "PlayStation"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "HSBC"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "DHL"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "Reuters"
|
||||
mark: none
|
||||
- name: "Volvic"
|
||||
mark: none
|
||||
- name: "AutoCAD"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
attribution: "Autodesk" if n == "AutoCAD"
|
||||
- name: "Channel 4"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "Pearsons"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "Shell"
|
||||
mark: none # available in Simple Icons; not enabled
|
||||
- name: "Stella Artois"
|
||||
mark: none
|
||||
- name: "Baileys Irish Cream"
|
||||
mark: none
|
||||
- name: "Parkgate Homes"
|
||||
mark: none
|
||||
- name: "Carlton Dry"
|
||||
mark: none
|
||||
- name: "Cottees Cordial"
|
||||
mark: none
|
||||