phase: 01_fix_history_xss
Build and Push Container / build-and-push (push) Successful in 26s

**Phase 01 — final verification pass (all criteria green)**

- Verified M-1 fix in `src/terminal.js`: ArrowUp / ArrowDown(`>0`) / ArrowDown-else now route through `updateDisplay()` (`textContent`); no `innerHTML` sink touches `cmdLine`/`commandHistory`.
- `grep -n innerHTML src/terminal.js` → 12 hits, all allowed (static prompt literals, `clear`/prank wipes, `outLine.innerHTML = output` gated on the static canned `neofetch` string). **C1 ✓**
- XSS regression test at `tests/e2e/02-terminal.spec.js:156` present + passing; re-proved it is a real guard: re-injected the old `innerHTML` recall into `dist/terminal.*.js` → test failed (`#xss-marker` received 1); rebuild → byte-identical to `src/`. **C2 ✓**
- `./build.sh` → `dist/` OK; `npm test` → **45 passed (0 failed)**; `bash .agents/validate.sh` → **exit 0** (suite + repo-hygiene gates). **C3 ✓**
- Branch coverage of modified code: all 3 recall arms asserted (ArrowUp ×2 specs, ArrowDown `>0` in the existing history spec, ArrowDown-else → empty prompt in the new spec) → >90% gate met; no JS coverage/lint toolchain exists in the locked stack, so lint = `node --check src/*.js tests/e2e/*.js playwright.config.js` + `bash -n build.sh .agents/validate.sh` — clean.
- C4 ✓ terminal/vim/achievements/menu specs all green; only the three recall branches changed.
- Docs (step 6, full README/AGENTS review): README — run section now states the image already contains the built `dist/` (mount `dist/`, never `src/`); new “Serving & configuration” section for `nginx.conf`, incl. **empirically measured** (built image on 127.0.0.1:8124) that `/`, `/index.html` and hashed assets currently return *only* cache headers — the `add_header` non-merge gap = finding M-4/Phase 03; TLS/HSTS-at-edge rule; Quadlet section fixed (unit is not in this repo; Quadlet generates `homepage.service`, so `… start homepage.service`, and `systemctl enable` is a no-op for transient Quadlet units → `[Install]` + `daemon-reload` + linger); Development gained the Node 20+ floor, `npx playwright install chromium`, and `.agents/validate.sh`. AGENTS.md — added the validate.sh gate, flagged `./scripts/check-headers.sh` as not yet in the repo (Phase 03), and scoped the M-1 rule with the allowed-`innerHTML` list.
- Notable: `.agents/PLAN.md` still says “44 tests” (3 places) and its §7 roadmap omits phases 04/05 — left untouched per the no-PLAN-edit rule; flag for the next PLAN review. No live-infra touched (temp podman image built/removed for the header measurement only).
- Next pending phase: `02_simulate_fetch_commands`.
This commit is contained in:
2026-10-22 22:45:40 -04:00
parent a2f331e526
commit dcd066d953
30 changed files with 2343 additions and 27 deletions
@@ -1,38 +0,0 @@
# Phase 01 — Fix terminal history-recall XSS (audit M-1)
**Story:** n/a
**Context:** `.agents/PLAN.md` §4 (terminal modules), `.agents/remediation_plan.md` finding M-1
## Objective
Eliminate the DOM-XSS sink in the fake terminal's command-history recall:
ArrowUp/ArrowDown currently rebuild the prompt line with
`lastLine.innerHTML = '<span class="terminal-prompt">$</span> ' + cmdLine + ' '`
where `cmdLine` is raw user input from `commandHistory`. Replace it with
textContent-based construction so terminal input can never be injected as
HTML, and lock the fix with a Playwright regression test.
## Dependencies
— (none)
## Tasks
1. `01_fix_history_recall.md` — Rewrite the ArrowUp/ArrowDown recall to build the prompt line with `createElement`/`textContent` (mirror the existing `updateDisplay()` pattern).
2. `02_xss_regression_test.md` — Add a Playwright test proving a markup payload typed into the terminal is recalled as literal text, not DOM.
## Testing & Quality
- Unit/integration: the new regression test must cover (a) a payload that
would create a DOM node if injected (`<b id="xss-marker">x</b>`), (b) a
payload with an inline event handler (`<img src=x onerror=...`) asserting
no script execution, and (c) a benign command (`whoami`) still recalled
and executable.
- Coverage: **>90%** on new/modified code (the two recall branches in
`src/terminal.js`).
- E2E: full Playwright suite (`npm test`) green — it already exercises
history recall with benign commands and must not regress.
## Completion Criteria
- [ ] `grep -n "innerHTML" src/terminal.js` shows no occurrence combining
`innerHTML` with `cmdLine`/history data (static prompt literals and the
neofetch canned-output assignment are the only allowed `innerHTML` uses)
- [ ] new XSS regression test present in `tests/e2e/02-terminal.spec.js` and passing
- [ ] `./build.sh && npm test` fully green (44+ tests)
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
@@ -1,49 +0,0 @@
# Task 01 — Rewrite history recall with the safe display path
**Phase:** `01_fix_history_xss` · **Story:** n/a
## Objective
Remove the `innerHTML` sink from the terminal's ArrowUp/ArrowDown history
recall by routing recall through the existing `updateDisplay(text)` function,
which already builds the prompt line with `textContent`/`createElement`.
## Work
1. `src/terminal.js` — in the terminal `keydown` handler, replace the three
`lastLine.innerHTML = '<span class="terminal-prompt">$</span> ' + cmdLine + ' '`
constructions with `updateDisplay(...)` calls:
- **ArrowUp branch** (`if (historyIndex < commandHistory.length - 1)`):
delete the `innerHTML` line, the `newCursor` creation, and the
`appendChild` — keep `historyIndex++` and `const cmdLine = ...`, then:
```js
updateDisplay(cmdLine);
```
- **ArrowDown branch, first case** (`if (historyIndex > 0)`): same
replacement — `updateDisplay(cmdLine);`
- **ArrowDown branch, else case** (`historyIndex = -1`): replace the
`innerHTML`/cursor block with:
```js
updateDisplay('');
```
`updateDisplay` already: removes any stale `.terminal-cursor`, clears the
line via `textContent`, appends a `.terminal-prompt` span (respecting
`isRoot` → `#` vs `$`), the text, and a fresh cursor (red when root).
Note the two intentional, behavior-preserving improvements this brings:
(a) recalled prompts match the live typing state (cursor directly after
the text, no trailing space); (b) after `sudo su -`, recall shows `#`
instead of a stale `$`. Both are consistency fixes, not regressions.
## Testing & Quality
- Unit/integration: no new unit code — the existing E2E test
`02-terminal.spec.js › history: ArrowUp/ArrowDown recall previous commands`
exercises recall and execution of recalled commands and must stay green.
- Coverage: **>90%** on the modified branches (covered by that E2E test plus
the task-02 regression test).
## Completion Criteria
- [ ] `grep -n "innerHTML" src/terminal.js` — no remaining occurrence that
involves `cmdLine` or history data
- [ ] `./build.sh && npm test` fully green
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
@@ -1,65 +0,0 @@
# Task 02 — Add the XSS regression test
**Phase:** `01_fix_history_xss` · **Story:** n/a
## Objective
Add a Playwright E2E test that proves a markup payload typed into the
terminal is recalled (ArrowUp) as **literal text**, never as DOM, and never
executes. This test fails on the pre-fix (innerHTML) code and passes on the
fixed code, so it guards against regressions/reverts.
## Work
1. `tests/e2e/02-terminal.spec.js` — add the following test inside the
`test.describe('Fake terminal — state and special commands', ...)` block
(it reuses `openTerminal`, `typeCommand`, `currentPromptLine` from
`./helpers`):
```js
test('history recall renders markup payloads as literal text (no DOM injection)', async ({ page }) => {
await openTerminal(page);
// 1) A payload that would create a DOM node if innerHTML-injected.
await typeCommand(page, '<b id="xss-marker">pwned</b>');
await page.keyboard.press('ArrowUp');
await expect(page.locator('#xss-marker')).toHaveCount(0);
await expect(currentPromptLine(page)).toContainText('<b id="xss-marker">pwned</b>');
// 2) An event-handler payload must not execute. `data:,` is an empty
// data URI so the <img> fails to decode deterministically (no
// network dependency) and onerror would fire if injected as HTML.
await page.evaluate(() => { window.__xssFired = false; });
await typeCommand(page, '<img src="data:," onerror="window.__xssFired=true">');
await page.keyboard.press('ArrowUp');
await page.waitForTimeout(200); // allow onerror to fire if vulnerable
expect(await page.evaluate(() => window.__xssFired)).toBe(false);
await expect(page.locator('img[onerror]')).toHaveCount(0);
await expect(currentPromptLine(page)).toContainText('<img src="data:," onerror="window.__xssFired=true">');
// 3) Benign recall still works and is executable.
await page.keyboard.press('ArrowDown');
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
await expect(page.locator('.terminal-content > div').nth(-2)).toHaveText('optional');
});
```
Notes for the executor:
- Step 3 walks back down through history (`<img…>`, then `<b…>`, then the
prior real command) and executes it; adjust the final assertion to the
benign command that is actually on top of the history at that point if
the exact stack differs when you run it. The point is that recall +
execute of a benign command still works.
- Do **not** add an `onerror`-based assertion that depends on a real
network fetch; keep it self-contained with `data:,`.
## Testing & Quality
- Unit/integration: this test **is** the regression coverage for the M-1 fix.
It must: fail on innerHTML-based recall, pass on textContent-based recall.
- Coverage: **>90%** on the modified recall branches (both payload classes +
benign path covered).
## Completion Criteria
- [ ] new test present in `tests/e2e/02-terminal.spec.js`
- [ ] `./build.sh && npm test` fully green (45+ tests)
- [ ] (sanity) temporarily reverting task 01's fix makes this test fail —
confirms it is a genuine regression guard, then re-apply the fix
@@ -0,0 +1,50 @@
# Phase 04 — Add Huntington National Bank role to work experience
**Story:** n/a
**Context:** `src/index.html` `#experience` timeline, `.agents/PLAN.md` §2 (static-first — content-only change, no new technology)
## Objective
Update the work-experience timeline with the user's current role: **Lead MLOps
Engineer at Huntington National Bank (Jun 2026 – Present)**, inserted as the
new first (most recent) timeline entry with three bullet points covering the
agentic vulnerability-remediation toolkit, LLM harness skills for business
automation, and the 6-person offshore team building the developer landing
zone. The previous Nimbis "DevOps Manager" entry is updated from
`Jan 2022 – Present` to `Jan 2022 – Apr 2026`. Lock the change with a
Playwright regression test.
## Dependencies
— (none)
## Tasks
1. `01_add_timeline_entry.md` — Insert the new Huntington National Bank timeline entry at the top of the `#experience` timeline and update the Nimbis DevOps Manager end date.
2. `02_experience_regression_test.md` — Add a Playwright test proving the new entry is first, complete, and correctly dated, and that the Nimbis entry no longer claims "Present".
## Testing & Quality
- Unit/integration: no JS logic changes in this phase — `src/index.html`
content only. The quality gate is the E2E regression test (task 2) plus the
full suite.
- Coverage: **>90%** on new/modified code — vacuously satisfied (no
JS/CSS/TS code is modified); the E2E suite is the binding gate.
- E2E: full Playwright suite (`npm test`) green.
- The phase is "Complete" only when the **entire** project test suite passes
100% (every test in the project, not just this phase's — pre-existing
failures included) and achieves **>90% code coverage** on new/modified code.
## Documentation & Consistency
- The final pass reviews `README.md` and `AGENTS.md` in full against the
project as it stands after this phase — fixing every inconsistency found,
**including pre-existing ones not caused by this phase**.
- Tasks update `README.md`/`AGENTS.md` immediately when their own changes are
user-visible (setup, commands, configuration, features, conventions).
## Completion Criteria
- [ ] `grep -c "Huntington National Bank" dist/index.html` returns 1 and
`grep -n "Jun 2026" dist/index.html` shows the `– Present` date span
- [ ] Nimbis "DevOps Manager" date reads `Jan 2022 – Apr 2026` in
`dist/index.html` (no second `– Present` entry in `#experience`)
- [ ] new regression test present in `tests/e2e/01-page.spec.js` and passing
- [ ] `./build.sh && npm test` fully green (45+ tests)
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
- [ ] README.md and AGENTS.md are accurate and consistent with the whole
project (verified in the final pass)
@@ -0,0 +1,65 @@
# Task 01 — Add Huntington National Bank timeline entry
**Phase:** `04_huntington_role` · **Story:** n/a
## Objective
Insert the new top work-experience entry (Lead MLOps Engineer, Huntington
National Bank, Jun 2026 – Present) into the `#experience` timeline and close
out the Nimbis "DevOps Manager" date span.
## Work
1. `src/index.html` — inside `<section id="experience">`, insert the block
below **immediately before** the existing first
`<div class="timeline-item fade-in" role="listitem">` (the Nimbis
"DevOps Manager" entry), keeping the file's existing 16/20/24/28-space
indentation exactly:
```html
<div class="timeline-item fade-in" role="listitem">
<div class="timeline-marker"></div>
<div class="timeline-content">
<div class="timeline-header">
<h3>Lead MLOps Engineer</h3>
<span class="timeline-company">Huntington National Bank</span>
</div>
<span class="timeline-date">Jun 2026 – Present</span>
<ul class="timeline-list">
<li>Building an enterprise agentic vulnerability remediation
toolkit and workflow to resolve millions of vulnerabilities.</li>
<li>Creating and distributing LLM harness skills that automate
business problems — including ServiceNow incident response —
at machine speed.</li>
<li>Leading a team of 6 offshore engineers to build a developer
landing zone with agentic tools that build pipelines, plan
work, and make documentation searchable.</li>
</ul>
</div>
</div>
```
(Use the en dash `–` U+2013 in the date span, matching every other
`timeline-date` value; use the em dash `—` only in the middle bullet,
matching the copy above. Wrap long `<li>` lines at the same width as
neighboring entries, 4-space continuation indent.)
2. `src/index.html` — in the Nimbis "DevOps Manager" entry, change its
`<span class="timeline-date">Jan 2022 – Present</span>` to
`<span class="timeline-date">Jan 2022 – Apr 2026</span>`.
3. `./build.sh` — rebuild `dist/`.
## Testing & Quality
- Unit/integration: content-only change — no new JS logic; the E2E regression
test is task 2, and the full suite is the gate.
- Coverage: **>90%** on this task's new/modified code — vacuously satisfied
(HTML content only, no JS/CSS modified).
## Completion Criteria
- [ ] `grep -c "Huntington National Bank" src/index.html` returns 1
- [ ] `grep -n "Jun 2026 – Present" src/index.html` matches the new entry
- [ ] `grep -n "Jan 2022 – Apr 2026" src/index.html` matches the Nimbis
"DevOps Manager" entry; `grep -c "– Present" src/index.html` returns 1
- [ ] `./build.sh` succeeds and the same three greps pass on
`dist/index.html`
- [ ] `npm test` fully green (44 tests — the new test lands in task 2)
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes
(the phase's final pass re-checks both project-wide)
- [ ] no behavior change in completed work
@@ -0,0 +1,55 @@
# Task 02 — Experience timeline regression test
**Phase:** `04_huntington_role` · **Story:** n/a
## Objective
Lock the work-experience change with a Playwright regression test: the
Huntington National Bank entry is the first, most-recent timeline item with
its three bullets and `Jun 2026 – Present` date, and the Nimbis
"DevOps Manager" entry now ends `Apr 2026` instead of `Present`.
## Work
1. `tests/e2e/01-page.spec.js` — inside the existing
`test.describe('Page structure', ...)` block (after the
"all major sections exist and are in the DOM" test), add one test,
matching the file's existing style:
```js
test('experience timeline leads with Huntington National Bank role', async ({ page }) => {
const items = page.locator('#experience .timeline-item');
await expect(items).toHaveCount(5);
const first = items.first().locator('.timeline-content');
await expect(first.locator('h3')).toHaveText('Lead MLOps Engineer');
await expect(first.locator('.timeline-company')).toHaveText('Huntington National Bank');
await expect(first.locator('.timeline-date')).toHaveText('Jun 2026 – Present');
const bullets = first.locator('.timeline-list li');
await expect(bullets).toHaveCount(3);
await expect(bullets.nth(0)).toContainText('agentic vulnerability remediation');
await expect(bullets.nth(1)).toContainText('LLM harness skills');
await expect(bullets.nth(1)).toContainText('ServiceNow');
await expect(bullets.nth(2)).toContainText('developer landing zone');
// previous role's end date was closed out
const nimbisManager = items.nth(1).locator('.timeline-content');
await expect(nimbisManager.locator('h3')).toHaveText('DevOps Manager');
await expect(nimbisManager.locator('.timeline-date')).toHaveText('Jan 2022 – Apr 2026');
});
```
(Use the en dash `–` U+2013 in the expected date strings to match the
markup.)
2. `./build.sh && npm test` — rebuild and run the full suite.
## Testing & Quality
- Unit/integration: the new test covers (a) entry order (HNB first, Nimbis
DevOps Manager second, 5 entries total), (b) title/company/date of the new
entry, (c) all three bullets' key phrases, and (d) the Nimbis entry's
closed-out end date.
- Coverage: **>90%** on this task's new/modified code — test-only change;
the E2E suite is the binding gate.
## Completion Criteria
- [ ] the new test is present in `tests/e2e/01-page.spec.js` and passes
- [ ] `./build.sh && npm test` fully green (45 tests)
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes
(the phase's final pass re-checks both project-wide)
- [ ] no behavior change in completed work
@@ -0,0 +1,62 @@
# Phase 05 — Update Featured Projects (Brain of Reese, ServWaf, Local AI Stack)
**Story:** n/a
**Context:** `src/index.html` `#projects` section (6 `project-card`s), `~/Projects/Personal/brain_of_reese/README.md`, `~/Projects/StackExpected/ServWall/README.md`, `~/Projects/Experimental/gufo/README.md`
## Objective
Refresh the Featured Projects grid: (a) replace the "Self-Hosted Services"
card with a **Brain of Reese** card (RAG chatbot over homelab docs;
pgvector ∪ Postgres full-text hybrid search with RRF fusion; self-hosted
LLM; source `https://gitea.reeseapps.com/Experimental/brain-of-reese` —
verified live) and move it to the **first** slot; (b) replace the
"Kubernetes Cluster" card with a **ServWaf** card (Go network WAF —
declarative YAML rules, rate limiting, attack-pattern inspection, opt-in
local-AI verdict, TLS/HTTP3 edge — **closed source: no source link**,
user decision) in its place; (c) update the **Local AI Stack** card to
replace Ollama with **vLLM, Gufo, and llama.cpp** (copy, tech tags, and
code preview). Lock with a Playwright regression test.
## Dependencies
— (none)
## Tasks
1. `01_brain_of_reese_card.md` — Delete the Self-Hosted Services card; insert the Brain of Reese card as the first card in `#projects`.
2. `02_servwaf_card.md` — Replace the Kubernetes Cluster card with the ServWaf card (description only, no source link).
3. `03_update_local_ai_stack.md` — Update the Local AI Stack card: replace Ollama with vLLM/Gufo/llama.cpp in the copy, tech tags, and code preview.
4. `04_projects_regression_test.md` — Add a Playwright test locking card count, order, the updated Local AI Stack tags, and the absence of the two removed card titles.
## Testing & Quality
- Unit/integration: no JS logic changes in this phase — `src/index.html`
content only. The quality gate is the E2E regression test (task 4) plus
the full suite.
- Coverage: **>90%** on new/modified code — vacuously satisfied (no
JS/CSS/TS code is modified); the E2E suite is the binding gate.
- E2E: full Playwright suite (`npm test`) green.
- The phase is "Complete" only when the **entire** project test suite passes
100% (every test in the project, not just this phase's — pre-existing
failures included) and achieves **>90% code coverage** on new/modified code.
## Documentation & Consistency
- The final pass reviews `README.md` and `AGENTS.md` in full against the
project as it stands after this phase — fixing every inconsistency found,
**including pre-existing ones not caused by this phase**.
- Tasks update `README.md`/`AGENTS.md` immediately when their own changes are
user-visible (setup, commands, configuration, features, conventions).
## Completion Criteria
- [ ] `grep -c "project-card fade-in" src/index.html` returns 6, and the
same holds in `dist/index.html`
- [ ] first `project-card` in `#projects` (in `dist/index.html`) is
"Brain of Reese" and its card links to
`https://gitea.reeseapps.com/Experimental/brain-of-reese`
- [ ] `grep -c "Self-Hosted Services\|Kubernetes Cluster" src/index.html`
returns 0 (project-card titles removed; the words may still appear in
the out-of-scope About/Skills sections — e.g. "Kubernetes" skill tag)
- [ ] the Local AI Stack card (in `dist/index.html`) contains vLLM, Gufo,
and llama.cpp and no "Ollama"
- [ ] new regression test present in `tests/e2e/01-page.spec.js` and passing
- [ ] `./build.sh && npm test` fully green (45+ tests)
- [ ] no change to the Skills section, About section, or any other section
outside `#projects`
- [ ] README.md and AGENTS.md are accurate and consistent with the whole
project (verified in the final pass)
@@ -0,0 +1,76 @@
# Task 01 — Brain of Reese card (first slot)
**Phase:** `05_update_projects_section` · **Story:** n/a
## Objective
Remove the "Self-Hosted Services" project card and insert the new
**Brain of Reese** card as the **first** card in `#projects` (immediately
before the "Reverse Proxy Infrastructure" card).
## Work
1. `src/index.html` — inside `<section id="projects">`, delete the entire
`project-card` block whose `<h3>` is "Self-Hosted Services" (the card
containing the `- Immich / - Jellyfin / …` code block).
2. `src/index.html` — insert the block below **immediately before** the
existing first `project-card` (the "Reverse Proxy Infrastructure"
card), matching the file's 16/20/24/28-space indentation. For the
`<div class="project-links">…</div>` (Gitea SVG) block, copy the
existing one from the "Reverse Proxy Infrastructure" card verbatim and
change only the `href` to
`https://gitea.reeseapps.com/Experimental/brain-of-reese`:
```html
<div class="project-card fade-in" role="listitem">
<div class="project-preview">
<div class="code-block">
<div class="code-line"><span class="code-comment"># Hybrid search: pgvector cosine ∪ Postgres full-text</span></div>
<div class="code-line"><span class="code-keyword">def</span> <span class="code-function">retrieve</span>(<span class="code-variable">query</span>, <span class="code-variable">k</span>=<span class="code-variable">5</span>):</div>
<div class="code-line">&nbsp;&nbsp;<span class="code-variable">vec</span> = <span class="code-function">pgvector</span>.<span class="code-function">cosine</span>(<span class="code-variable">query</span>)</div>
<div class="code-line">&nbsp;&nbsp;<span class="code-variable">fts</span> = <span class="code-function">postgres</span>.<span class="code-function">full_text</span>(<span class="code-variable">query</span>)</div>
<div class="code-line">&nbsp;&nbsp;<span class="code-variable">chunks</span> = <span class="code-function">rrf_fuse</span>(<span class="code-variable">vec</span>, <span class="code-variable">fts</span>, <span class="code-variable">k</span>)</div>
<div class="code-line">&nbsp;&nbsp;<span class="code-keyword">return</span> <span class="code-function">llm</span>.<span class="code-function">chat</span>(<span class="code-function">document</span>(<span class="code-variable">chunks</span>))</div>
<div class="code-line">&nbsp;</div>
</div>
</div>
<div class="project-info">
<h3>Brain of Reese</h3>
<p>A chippy, honest RAG chatbot over homelab documentation — git repositories, local
directories, and uploaded archives. Hybrid search (pgvector cosine ∪ Postgres
full-text search, Reciprocal Rank Fusion) feeds the whole relevant document to a
self-hosted LLM and streams back a grounded answer; when it has no notes it
admits it and suggests questions it can answer.</p>
<div class="project-meta">
<div class="project-tech">
<span>FastAPI</span>
<span>pgvector</span>
<span>Postgres</span>
<span>Playwright</span>
</div>
<div class="project-links">
<!-- copied from the Reverse Proxy Infrastructure card, href changed -->
</div>
</div>
</div>
</div>
```
3. `./build.sh` — rebuild `dist/`.
## Testing & Quality
- Unit/integration: content-only change — no new JS logic; the E2E
regression test is task 4, and the full suite is the gate.
- Coverage: **>90%** on this task's new/modified code — vacuously satisfied
(HTML content only, no JS/CSS modified).
## Completion Criteria
- [ ] `grep -c "project-card fade-in" src/index.html` returns 6
- [ ] `grep -n "Brain of Reese" src/index.html` matches once, and that card
is the first `project-card` inside `#projects`
- [ ] `grep -c "Self-Hosted Services" src/index.html` returns 0
- [ ] `grep -n "gitea.reeseapps.com/Experimental/brain-of-reese"
src/index.html` matches inside the new card
- [ ] `./build.sh` succeeds and the same greps pass on `dist/index.html`
- [ ] `npm test` fully green (44 tests — new test lands in task 4)
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes
(the phase's final pass re-checks both project-wide)
- [ ] no behavior change in completed work
@@ -0,0 +1,69 @@
# Task 02 — ServWaf card (replaces Kubernetes Cluster)
**Phase:** `05_update_projects_section` · **Story:** n/a
## Objective
Replace the "Kubernetes Cluster" project card, in place (last slot), with
the **ServWaf** card. ServWaf is **closed source** (user decision): the
card carries **no** `project-links` block — description only.
## Work
1. `src/index.html` — inside `<section id="projects">`, replace the entire
`project-card` block whose `<h3>` is "Kubernetes Cluster" (the card
containing the `# Kubernetes with k3s` code block) with the block below,
matching the file's 16/20/24/28-space indentation. Note: this card has
**no** `<div class="project-links">` — the `project-meta` div contains
only `project-tech` (the flex layout handles a single child).
```html
<div class="project-card fade-in" role="listitem">
<div class="project-preview">
<div class="code-block">
<div class="code-line"><span class="code-comment"># ServWaf — declarative rules</span></div>
<div class="code-line"><span class="code-variable">ratelimit</span>:</div>
<div class="code-line">&nbsp;&nbsp;<span class="code-variable">per_ip</span>: <span class="code-string">20 rps, burst 40</span></div>
<div class="code-line"><span class="code-variable">rules</span>:</div>
<div class="code-line">&nbsp;&nbsp;- <span class="code-keyword">deny</span> <span class="code-variable">if</span> <span class="code-variable">user_agent</span> =~ <span class="code-string">"curl|wget"</span></div>
<div class="code-line">&nbsp;&nbsp;- <span class="code-keyword">flag</span> <span class="code-variable">if</span> <span class="code-variable">body</span> =~ <span class="code-string">"(?i)union\s+select"</span></div>
<div class="code-line"><span class="code-variable">ai</span>:</div>
<div class="code-line">&nbsp;&nbsp;<span class="code-variable">endpoint</span>: <span class="code-string">"http://aipi.reeseapps.com/v1"</span></div>
</div>
</div>
<div class="project-info">
<h3>ServWaf</h3>
<p>A network Web Application Firewall written in Go, sitting in line between clients
and the origin. Declarative YAML rules (user agent, IP/CIDR, URL and body
regexes), per-IP rate limiting, attack-pattern inspection (SQL injection, XSS,
path traversal, CRLF, scanner probes), opt-in self-hosted local-AI verdicts,
TLS termination, and an opt-in HTTP/3 (QUIC) edge.</p>
<div class="project-meta">
<div class="project-tech">
<span>Go</span>
<span>YAML</span>
<span>HTTP/3</span>
<span>Podman</span>
</div>
</div>
</div>
</div>
```
2. `./build.sh` — rebuild `dist/`.
## Testing & Quality
- Unit/integration: content-only change — no new JS logic; the E2E
regression test is task 4, and the full suite is the gate.
- Coverage: **>90%** on this task's new/modified code — vacuously satisfied
(HTML content only, no JS/CSS modified).
## Completion Criteria
- [ ] `grep -c "project-card fade-in" src/index.html` returns 6
- [ ] `grep -n "ServWaf" src/index.html` matches the new card's `<h3>`
- [ ] `grep -c "Kubernetes Cluster" src/index.html` returns 0
- [ ] the ServWaf card (last card in `#projects`) contains no
`project-links` div and no `gitea.reeseapps.com` href
- [ ] `./build.sh` succeeds and the same greps pass on `dist/index.html`
- [ ] `npm test` fully green (44 tests — new test lands in task 4)
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes
(the phase's final pass re-checks both project-wide)
- [ ] no behavior change in completed work
@@ -0,0 +1,59 @@
# Task 03 — Update Local AI Stack card (vLLM, Gufo, llama.cpp)
**Phase:** `05_update_projects_section` · **Story:** n/a
## Objective
Update the "Local AI Stack" project card so Ollama is replaced by **vLLM,
Gufo, and llama.cpp** in the description, tech tags, and code preview.
Everything else on the card (LocalAI, LiteLLM, Bifrost, Langfuse, CUDA/
ROCm) stays.
## Work
1. `src/index.html` — in the "Local AI Stack" card, replace the `<p>` copy
with:
```html
<p>Complete local AI infrastructure: vLLM, Gufo, and llama.cpp for inference, LiteLLM as a
unified API proxy, Bifrost for model routing, and Langfuse for observability. Supports
both CUDA and ROCm.</p>
```
2. `src/index.html` — in that card's `project-tech` div, replace the
`<span>Ollama</span>` with three spans in this order:
`<span>vLLM</span> <span>Gufo</span> <span>llama.cpp</span>`
(final tag list: vLLM, Gufo, llama.cpp, LocalAI, LiteLLM, Langfuse).
3. `src/index.html` — in that card's `code-block`, replace the whole block
(the `from ollama import Client` … `# LiteLLM proxy for unified API`
lines) with:
```html
<div class="code-line"><span class="code-comment"># Inference engines</span></div>
<div class="code-line"><span class="code-variable">engines</span> = {</div>
<div class="code-line">&nbsp;&nbsp;<span class="code-string">"vllm"</span>: <span class="code-string">"cuda"</span>, <span class="code-comment"># high-throughput serving</span></div>
<div class="code-line">&nbsp;&nbsp;<span class="code-string">"gufo"</span>: <span class="code-string">"rocm"</span>, <span class="code-comment"># Strix Halo inference</span></div>
<div class="code-line">&nbsp;&nbsp;<span class="code-string">"llama.cpp"</span>: <span class="code-string">"gguf"</span>, <span class="code-comment"># CPU/GPU fallback</span></div>
<div class="code-line">}</div>
<div class="code-line">&nbsp;</div>
<div class="code-line"><span class="code-comment"># LiteLLM proxy for unified API</span></div>
```
4. `./build.sh` — rebuild `dist/`.
## Testing & Quality
- Unit/integration: content-only change — no new JS logic; the E2E
regression test is task 4, and the full suite is the gate.
- Coverage: **>90%** on this task's new/modified code — vacuously satisfied
(HTML content only, no JS/CSS modified).
## Completion Criteria
- [ ] within the Local AI Stack card in `src/index.html`: no "Ollama"
anywhere, and "vLLM", "Gufo", "llama.cpp" each present (copy, tags,
and preview)
- [ ] "LocalAI", "LiteLLM", "Langfuse" (and "Bifrost" in the copy) are
still present on the card
- [ ] `./build.sh` succeeds and the same checks pass on
`dist/index.html`
- [ ] `npm test` fully green (44 tests — new test lands in task 4)
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes
(the phase's final pass re-checks both project-wide)
- [ ] no behavior change in completed work
@@ -0,0 +1,64 @@
# Task 04 — Projects grid regression test
**Phase:** `05_update_projects_section` · **Story:** n/a
## Objective
Lock the Featured Projects change with a Playwright regression test: six
cards in the new order (Brain of Reese first, ServWaf last), the Local AI
Stack card showing vLLM/Gufo/llama.cpp with no Ollama, and the removed
"Self-Hosted Services" / "Kubernetes Cluster" titles absent.
## Work
1. `tests/e2e/01-page.spec.js` — inside the existing
`test.describe('Page structure', ...)` block, add one test, matching the
file's existing style:
```js
test('featured projects show the six updated cards in order', async ({ page }) => {
const cards = page.locator('#projects .project-card');
await expect(cards).toHaveCount(6);
const titles = await cards.locator('.project-info h3').allTextContents();
expect(titles).toEqual([
'Brain of Reese',
'Reverse Proxy Infrastructure',
'Dynamic DNS Service',
'Local AI Stack',
'Deployment Automation',
'ServWaf',
]);
// Brain of Reese leads, with its source link
const brain = cards.first();
await expect(brain.locator('.project-tech span')).toHaveCount(4);
await expect(brain).toHaveText(/RAG chatbot/i);
await expect(brain.locator('a[href="https://gitea.reeseapps.com/Experimental/brain-of-reese"]')).toBeAttached();
// Local AI Stack: Ollama replaced by vLLM, Gufo, llama.cpp
const ai = cards.nth(3);
await expect(ai.locator('.project-tech')).toContainText(['vLLM', 'Gufo', 'llama.cpp']);
await expect(ai.locator('.project-tech')).toContainText(['LocalAI', 'LiteLLM', 'Langfuse']);
await expect(ai).not.toContainText('Ollama');
// ServWaf is closed source — description only, no source link
await expect(cards.last()).toHaveText(/Web Application Firewall/i);
await expect(cards.last().locator('.project-links')).not.toBeAttached();
// removed cards are gone from the grid
expect(titles).not.toContain('Self-Hosted Services');
expect(titles).not.toContain('Kubernetes Cluster');
});
```
2. `./build.sh && npm test` — rebuild and run the full suite.
## Testing & Quality
- Unit/integration: the new test covers (a) card count (6) and exact title
order, (b) Brain of Reese first with its Gitea source link, (c) Local AI
Stack tech tags (new + retained, no Ollama), (d) ServWaf last with no
source link, and (e) the two removed titles absent.
- Coverage: **>90%** on this task's new/modified code — test-only change;
the E2E suite is the binding gate.
## Completion Criteria
- [ ] the new test is present in `tests/e2e/01-page.spec.js` and passes
- [ ] `./build.sh && npm test` fully green (45 tests: 44 pre-existing + 1
new)
- [ ] README.md/AGENTS.md updated for anything user-visible this task changes
(the phase's final pass re-checks both project-wide)
- [ ] no behavior change in completed work