Files
Void-Homelab/docs/superpowers/plans/2026-06-05-void-docs-consolidation.md

786 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Void Docs Consolidation & Wiki Restructure — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Collapse the Void's 7 overlapping spaces into 3 meaningful ones (Wiki / The Void / Bookmarks), turn the Wiki into an ordered, sectioned "traditional lab documentation" area that is the single source of truth, preserve the Void 1.x record + doc lineage, and retire BookStack to a passive secondary mirror.
**Architecture:** Most work is data manipulation on the **prod** Postgres DB (`void` @ `192.168.1.215`). One small code change adds page ordering + sectioned rendering to the `void-v2` app. Pure structural moves (space/parent/position changes, deletes) are done via SQL transactions; content **merges** (where two copies of a doc diverge) are done through the Void web page-editor so `body_html` + embeddings regenerate. Lineage between projects (The Void) and service docs (Wiki) is expressed with `entity_links`.
**Tech Stack:** PostgreSQL 16 + pgvector (prod DB on CT 310 `.215`), Node 22 / Express app (`void-v2`, CT 311 `.216`), `node lib/db/migrate.js up`, deploy via `deploy/push.sh`. PVE snapshots for safety (CT 310 + 311 on host Z).
**Authoritative reference data** (gathered 2026-06-05; re-verify counts before destructive steps):
- Spaces today: `void` (1 pg, 3 proj), `void1` (25 pg, 17 proj, 30 convo), `wiki` (24 pg), `plans` (6 pg), `void3` (8 pg), `bookmarks` (2 refs), `external-research` (empty).
- Wiki root page: slug `hynesy-homelab`, title "Hynesy Homelab" (all 23 other wiki pages are its direct children).
- DB creds for this work: `DATABASE_URL` in `/project/src/void-v2/.env` (`postgres://void:…@192.168.1.215:5432/void`). Prod app OWNER_TOKEN lives in `/opt/void-server/.env` on CT 311 (`.216`) — fetch at execution for API edits (the `.env` token in the repo is dev-only and is rejected by prod).
- Latest applied migration: `019_project_research.sql`. Next: `020`.
- `projects.status` CHECK allows: `idea | active | paused | done | abandoned`.
---
## Pre-flight verdict table (void1 ↔ wiki duplicates)
Classification computed by line-diff on 2026-06-05. Drives Task 5. "Drop v1" = delete the void1 copy, wiki copy is canonical. "Merge" = fold void1-only facts into the wiki copy first.
| void1 page | wiki counterpart | verdict | action |
|---|---|---|---|
| Next steps & follow-ups | (same) | IDENTICAL | drop v1 |
| Open WebUI LXC (103) | (same) | IDENTICAL | drop v1 |
| OpenClaw VM (200) | (same) | IDENTICAL | drop v1 |
| BookStack LXC (104) | (same) | ±5 lines | drop v1 (cosmetic) |
| Claude Code LXC deployer (agentic.sh) | (same) | ±2 lines | drop v1 (cosmetic) |
| Iventoy Pxe Lxc | iVentoy PXE LXC (107) | ±2 lines | drop v1 (cosmetic) |
| Mediastack Lxc | Mediastack LXC (100) | ±2 lines | drop v1 (cosmetic) |
| Usb Autosync | USB auto-sync drive | ±3 lines | drop v1 (cosmetic) |
| Operations notes | (same) | wiki ⊇ v1 (+26/-4) | merge 4 v1-only lines → wiki, drop v1 |
| Overview | (same) | wiki fuller (+10/-11) | merge 11 v1-only lines → wiki, drop v1 |
| Network map | (same) | wiki fuller (+34/-19) | merge 19 v1-only lines → wiki, drop v1 |
| Master Index | (same) | wiki fuller (+48/-28) | rewrite (Task 9), drop both old |
| Gramps Web — CT 109 | Gramps LXC (109) | wiki 14.5KB ⊇ v1 1.8KB | salvage ≤29 v1 lines → wiki, drop v1 |
| **Ollama LXC (102)** | (same) | **DIVERGE (conflicting facts)** | **real merge → wiki, drop v1** |
| Claude LXC deployer | (intra-v1 dup of agentic.sh, same md5) | EXACT DUP | drop v1 (pure cruft) |
void1 pages with **no** wiki counterpart (these are Void-app dev docs, salvaged in Task 6, not deleted): Active Projects Status, Agent Roster & Personas, Cron Tasks & Schedules, Deployment Guide, Path B — Build Log, Security Posture & Known Issues, The Void — Architecture & Agent System, The Void — Dashboard Progress (2026-05-14), Homelab Topology, Dashboard rebuild & Orthos overhaul (2026-05-16, also in wiki — moves to The Void, removed from wiki).
---
## Helper: SQL shell used throughout
All SQL steps assume this is exported in the working shell:
```bash
export PGPASSWORD=eIvGMUmYxlVdWnkSZmRQ0EJ8v81
Q() { psql -h 192.168.1.215 -U void -d void -X "$@"; } # interactive
QT(){ psql -h 192.168.1.215 -U void -d void -X -A -t "$@"; } # scalar
```
Resolve space UUIDs once and reuse (slugs are stable):
```sql
-- run: Q -c "SELECT slug,id FROM spaces ORDER BY slug;"
```
---
## Phase 0 — Safety net
### Task 0: Snapshot + logical backup
**Files:** none (infra)
- [ ] **Step 1: Snapshot both Void CTs on host Z** (standing rule: backup before major changes)
Run (on Z, `.124`):
```bash
ts=$(date +%Y%m%d_%H%M)
pct snapshot 310 predocs_${ts}
pct snapshot 311 predocs_${ts}
```
Expected: two snapshots created, no error.
- [ ] **Step 2: Logical dump of the prod DB to this box**
Run:
```bash
export PGPASSWORD=eIvGMUmYxlVdWnkSZmRQ0EJ8v81
pg_dump -h 192.168.1.215 -U void -d void -Fc -f /project/backups/void-predocs-$(date +%Y%m%d_%H%M).dump
ls -lh /project/backups/void-predocs-*.dump
```
Expected: a >0-byte custom-format dump. (`mkdir -p /project/backups` first if missing.)
- [ ] **Step 3: Record current counts as the rollback baseline**
Run:
```bash
Q -c "SELECT s.slug, (SELECT count(*) FROM pages p WHERE p.space_id=s.id) pages,
(SELECT count(*) FROM projects pr WHERE pr.space_id=s.id) projects
FROM spaces s ORDER BY s.slug;"
```
Expected: matches the inventory in this plan's header (void 1/3, void1 25/17, wiki 24/0, plans 6/0, void3 8/0, bookmarks 0/0, external-research 0/0). If it does **not** match, STOP — the data drifted; re-audit before proceeding.
---
## Phase 1 — Page ordering foundation (code)
This is the only code change. It gives every space a stable, intentional order instead of alphabetical-by-title, which is the root cause of "Overview is buried."
### Task 1: Add `position` column + order by it
**Files:**
- Create: `lib/db/migrations/020_page_position.sql`
- Modify: `lib/db/repos/pages.js:46-52` (`listBySpace`)
- Modify: `lib/api/routes/pages.js` (allow `position` in the update schema)
- Test: `tests/repos/pages_position.test.js`
- [ ] **Step 1: Write the migration**
`lib/db/migrations/020_page_position.sql`:
```sql
-- 020: explicit page ordering within a space (and within a parent).
ALTER TABLE pages ADD COLUMN IF NOT EXISTS position integer NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_pages_space_position ON pages (space_id, position, title);
```
- [ ] **Step 2: Write the failing test**
`tests/repos/pages_position.test.js`:
```js
import { describe, it, expect, beforeAll } from 'vitest';
import { create, listBySpace, update } from '../../lib/db/repos/pages.js';
import { create as createSpace } from '../../lib/db/repos/spaces.js';
describe('page ordering', () => {
let sid;
beforeAll(async () => { sid = (await createSpace({ slug: 'ord-'+Date.now(), name: 'Ord' }, 'test')).id; });
it('orders by position then title', async () => {
const a = await create({ space_id: sid, slug: 'a', title: 'Zzz', body_md: '' }, 'test');
const b = await create({ space_id: sid, slug: 'b', title: 'Aaa', body_md: '' }, 'test');
await update(a.id, { position: 1 }, 'test');
await update(b.id, { position: 9 }, 'test');
const list = await listBySpace(sid);
expect(list.map(p => p.title)).toEqual(['Zzz', 'Aaa']); // position 1 before 9 despite title
});
});
```
- [ ] **Step 3: Run it, verify it fails**
Run: `cd /project/src/void-v2 && npx vitest run tests/repos/pages_position.test.js`
Expected: FAIL (`update` rejects `position`, or order is alphabetical).
- [ ] **Step 4: Update `listBySpace` ordering**
In `lib/db/repos/pages.js`, change the `listBySpace` query (currently `ORDER BY title`) to:
```js
export async function listBySpace(space_id) {
const { rows } = await pool.query(
`SELECT id, space_id, slug, title, parent_id, position, updated_at
FROM pages WHERE space_id=$1 ORDER BY position, title`, [space_id]
);
return rows;
}
```
- [ ] **Step 5: Allow `position` through update**
In `lib/db/repos/pages.js` `update()`, add `'position'` to the updatable `fields` array (currently `['slug','title','body_md','body_html','parent_id','embedding']`):
```js
const fields = ['slug','title','body_md','body_html','parent_id','embedding','position'];
```
In `lib/api/routes/pages.js`, add to the page update zod schema (next to `body_html`):
```js
position: z.number().int().optional(),
```
- [ ] **Step 6: Run tests, verify pass**
Run: `npx vitest run tests/repos/pages_position.test.js`
Expected: PASS.
- [ ] **Step 7: Full suite green**
Run: `npx vitest run`
Expected: all pass (no regressions from the ordering change).
- [ ] **Step 8: Commit**
```bash
git add lib/db/migrations/020_page_position.sql lib/db/repos/pages.js lib/api/routes/pages.js tests/repos/pages_position.test.js
git commit -m "feat(pages): explicit position ordering within a space"
```
### Task 2: Sectioned page rendering in the space view
**Files:**
- Modify: `public/views/space.js:14-18,49-52,77-85`
Currently `space.js` renders pages as a single flat table. Change it to render a **page tree**: top-level pages (no parent, or the space's root page) as section headers, their children indented beneath. Backward-compatible — flat spaces (all pages parent-null) render as before, just ordered.
- [ ] **Step 1: Add a tree renderer** (replace `tableRow` helper, lines 14-18)
```js
function pageLink(p) {
return el('a', { href: '#/page/' + p.id }, p.title || '(untitled)');
}
// Build parent→children map and render top-level pages as section blocks.
function renderPageTree(pages, refs) {
const byParent = new Map();
for (const p of pages) {
const k = p.parent_id || '__root__';
if (!byParent.has(k)) byParent.set(k, []);
byParent.get(k).push(p);
}
const roots = byParent.get('__root__') || [];
const blocks = [];
for (const r of roots) {
const kids = byParent.get(r.id) || [];
blocks.push(el('div', { class: 'doc-section' },
el('h4', { style: { margin: '12px 0 4px' } }, pageLink(r)),
kids.length
? el('ul', { class: 'plain', style: { margin: '0 0 0 14px' } },
kids.map(k => {
const gk = byParent.get(k.id) || [];
return el('li', {}, pageLink(k),
gk.length ? el('ul', { class: 'plain', style: { margin: '0 0 0 14px' } },
gk.map(g => el('li', {}, pageLink(g)))) : null);
}))
: null));
}
if (refs.length) blocks.push(el('div', { class: 'doc-section' },
el('h4', { style: { margin: '12px 0 4px' } }, 'References'),
el('ul', { class: 'plain', style: { margin: '0 0 0 14px' } },
refs.map(rf => el('li', {}, el('a', { href: '#/ref/' + rf.id }, rf.title || rf.source_url))))));
return blocks;
}
```
- [ ] **Step 2: Use it in the Pages card** (replace the `rows` block, lines 49-52 + 77-85)
Remove the `const rows = [...]` block. Replace the final "Pages & references" card with:
```js
el('div', { class: 'card' },
el('h3', {}, `Pages & references${(pages.length + refs.length) ? ` (${pages.length + refs.length})` : ''}`),
(pages.length + refs.length)
? el('div', {}, renderPageTree(pages, refs))
: el('p', { class: 'muted' }, 'Nothing here yet.'))
```
- [ ] **Step 3: Smoke locally against prod data (read-only)**
Run the app pointed at prod read-only, or eyeball after deploy. Minimum: `node -e "require('./public/views/space.js')"` won't run (browser ESM) — instead verify via deploy in Task 3.
- [ ] **Step 4: Commit**
```bash
git add public/views/space.js
git commit -m "feat(space-view): render pages as ordered sectioned tree"
```
### Task 3: Deploy the code change
**Files:** none (deploy)
- [ ] **Step 1: Bump version** in `package.json` + `server.js` VERSION const + add a CHANGELOG entry (alpha-20).
- [ ] **Step 2: Deploy + migrate**
Run:
```bash
cd /project/src/void-v2
./deploy/push.sh # rsync + npm i --omit=dev + restart (TARGET defaults root@192.168.1.216)
ssh root@192.168.1.216 'cd /opt/void-server && npm run migrate' # applies 020
ssh root@192.168.1.216 'systemctl restart void-server'
```
- [ ] **Step 3: Verify**
Run:
```bash
curl -s http://192.168.1.216:3000/health
Q -c "\d pages" | grep position
Q -c "SELECT name FROM schema_migrations WHERE name='020_page_position.sql';"
```
Expected: health = `2.0.0-alpha.20`; `position` column present; migration row present. Open `void.hynesy.com` → a space → confirm pages render as a tree (still alphabetical until positions are set in Phase 2).
---
## Phase 2 — Wiki → traditional documentation area
Make `wiki` the ordered, sectioned source of truth, and remove Void-app dev docs that don't belong in a homelab wiki (they move to The Void in Phase 4/6).
### Task 4: Section the Wiki + set order
**Files:** none (SQL on prod)
- [ ] **Step 1: Move Void-app dev docs OUT of `wiki` into `void` (The Void)**
These 6 "Void 2.0 — *" pages + "Dashboard rebuild & Orthos overhaul" are project history, not homelab infra:
```sql
WITH w AS (SELECT id FROM spaces WHERE slug='wiki'),
v AS (SELECT id FROM spaces WHERE slug='void')
UPDATE pages SET space_id=(SELECT id FROM v), parent_id=NULL
WHERE space_id=(SELECT id FROM w)
AND title IN (
'Void 2.0 — Deployment (CT 310 + 311)',
'Void 2.0 — Plan 1 Foundation (complete)',
'Void 2.0 — Plan 2 API + UI (complete)',
'Void 2.0 — Plan 3 Capture (complete)',
'Void 2.0 — Plan 4 Workers (complete)',
'Dashboard rebuild & Orthos overhaul (2026-05-16)'
);
```
(These are re-parented under The Void's "Void 2.0 — Build Log" in Task 7.)
- [ ] **Step 2: Create three section parent-pages under the wiki root**
```sql
WITH w AS (SELECT id FROM spaces WHERE slug='wiki'),
root AS (SELECT id FROM pages WHERE space_id=(SELECT id FROM w) AND slug='hynesy-homelab')
INSERT INTO pages (space_id, slug, title, body_md, parent_id, position)
SELECT (SELECT id FROM w), x.slug, x.title,
'_Section index. Pages in this section are listed below._', (SELECT id FROM root), x.pos
FROM (VALUES
('sec-start-here','Start Here',0),
('sec-hosts-services','Hosts & Services',1),
('sec-operations','Operations & Infrastructure',2)
) AS x(slug,title,pos)
ON CONFLICT (space_id, slug) DO NOTHING;
```
- [ ] **Step 3: Re-parent each content page under the right section + set positions**
```sql
-- helper CTE pattern: set parent + position by title
WITH w AS (SELECT id sid FROM spaces WHERE slug='wiki'),
sec AS (SELECT slug, id FROM pages WHERE space_id=(SELECT sid FROM w) AND slug LIKE 'sec-%')
UPDATE pages p SET
parent_id = (SELECT id FROM sec WHERE sec.slug = m.section),
position = m.pos
FROM (VALUES
-- Start Here
('Overview', 'sec-start-here', 0),
('Network map', 'sec-start-here', 1),
('Master Index', 'sec-start-here', 2),
-- Hosts & Services
('Mediastack LXC (100)', 'sec-hosts-services', 0),
('Ollama LXC (102)', 'sec-hosts-services', 1),
('Open WebUI LXC (103)', 'sec-hosts-services', 2),
('BookStack LXC (104)', 'sec-hosts-services', 3),
('iVentoy PXE LXC (107)', 'sec-hosts-services', 4),
('Gramps LXC (109)', 'sec-hosts-services', 5),
('OpenClaw VM (200)', 'sec-hosts-services', 6),
('Jellyfin plugins', 'sec-hosts-services', 7),
('USB auto-sync drive', 'sec-hosts-services', 8),
('Claude Code LXC deployer (agentic.sh)', 'sec-hosts-services', 9),
-- Operations & Infrastructure
('Cluster and HA', 'sec-operations', 0),
('Operations notes', 'sec-operations', 1),
('AI Usage', 'sec-operations', 2),
('Next steps & follow-ups', 'sec-operations', 3)
) AS m(title, section, pos)
WHERE p.space_id=(SELECT sid FROM w) AND p.title = m.title;
```
- [ ] **Step 4: Set section order on the root's children** (sections already have positions 0/1/2 from Step 2; verify root page sorts first)
```sql
WITH w AS (SELECT id FROM spaces WHERE slug='wiki')
UPDATE pages SET position=-1 WHERE space_id=(SELECT id FROM w) AND slug='hynesy-homelab';
```
- [ ] **Step 5: Verify the tree**
```sql
WITH w AS (SELECT id FROM spaces WHERE slug='wiki')
SELECT COALESCE(par.title,'(root)') AS parent, p.position, p.title
FROM pages p LEFT JOIN pages par ON par.id=p.parent_id
WHERE p.space_id=(SELECT id FROM w)
ORDER BY COALESCE(par.position,-2), par.title NULLS FIRST, p.position, p.title;
```
Expected: root → Start Here(Overview, Network map, Master Index) → Hosts & Services(...) → Operations(...). Open `void.hynesy.com` → Wiki space → confirm **Overview is at the top**, grouped under sections. This resolves the user's "out of order" complaint.
---
## Phase 3 — Dedup void1 ↔ wiki
Per the verdict table. Merges (content edits) go through the **web page editor** (regenerates html/embedding); structural drops via SQL.
### Task 5: Salvage divergent facts, then drop void1 duplicates
**Files:** none (web editor + SQL on prod)
- [ ] **Step 1: Merge the DIVERGE page — Ollama LXC (102)**
Open the **wiki** "Ollama LXC (102)" page in the Void editor. Reconcile against the void1 copy using these authoritative facts (void1's are newer; cross-checked vs memory `reference-ollama-ct102`):
- Binary path: **`/usr/share/ollama`** for models; service user **`ollama`** (not root); installed via the community-script then upgraded. (Resolve the `/usr/bin` vs `/usr/local/bin` conflict by verifying live: `pct exec 102 -- which ollama` — use the real path; do not leave both.)
- Keep wiki-only content: the **PATH gotcha**, the **keep-alive / multi-model tuning after the 62 GiB RAM upgrade**, and the **driver 595.58.03** detail.
- Keep void1-only content: the **"CRITICAL: preserving config across upgrades"** section (systemd drop-in for `0.0.0.0:11434` binding) — this is the most important salvage; verify it's present in the merged page.
- Add a one-line lineage note at top: `> Consolidated from the Void 1 and BookStack copies, 2026-06-05.`
Save. Confirm the page renders and a search for "preserving config" returns it.
- [ ] **Step 2: Merge the wiki-superset pages** (Operations notes, Overview, Network map, Gramps LXC (109))
For each, diff the void1 copy and append any **void1-only** lines that carry real facts into the wiki copy via the editor. Use this to see exactly what's void1-only:
```bash
diff <(QT -c "SELECT body_md FROM pages p JOIN spaces s ON s.id=p.space_id WHERE s.slug='void1' AND p.title='<VOID1 TITLE>'") \
<(QT -c "SELECT body_md FROM pages p JOIN spaces s ON s.id=p.space_id WHERE s.slug='wiki' AND p.title='<WIKI TITLE>'")
```
Title pairs: Operations notes↔Operations notes; Overview↔Overview; Network map↔Network map; "Gramps Web — CT 109 (2026-05-25)"↔"Gramps LXC (109)". Lines prefixed `<` are void1-only — judge each; most will be stale and can be skipped. Save each edited wiki page.
- [ ] **Step 3: Fold Homelab Topology into wiki Network map**
void1 "Homelab Topology" has no wiki twin but overlaps "Network map". Diff it against wiki Network map; salvage any unique device/IP rows into Network map, then it's covered by the void1 drop in Step 5.
- [ ] **Step 4: Verify nothing unique is about to be lost**
```sql
-- void1 pages that will be dropped, with their wiki survivor lengths for a final sanity glance
WITH v AS (SELECT id FROM spaces WHERE slug='void1'),
w AS (SELECT id FROM spaces WHERE slug='wiki')
SELECT p.title, length(p.body_md) v1len,
(SELECT length(body_md) FROM pages wp WHERE wp.space_id=(SELECT id FROM w)
AND wp.title = CASE p.title
WHEN 'Mediastack Lxc' THEN 'Mediastack LXC (100)'
WHEN 'Iventoy Pxe Lxc' THEN 'iVentoy PXE LXC (107)'
WHEN 'Usb Autosync' THEN 'USB auto-sync drive'
WHEN 'Gramps Web — CT 109 (2026-05-25)' THEN 'Gramps LXC (109)'
ELSE p.title END) wikilen
FROM pages p WHERE p.space_id=(SELECT id FROM v) ORDER BY p.title;
```
Expected: every row that is being dropped has a non-null `wikilen` (a survivor exists). Investigate any NULL before deleting.
- [ ] **Step 5: Delete the void1 duplicate + intra-dup pages**
```sql
WITH v AS (SELECT id FROM spaces WHERE slug='void1')
DELETE FROM pages WHERE space_id=(SELECT id FROM v) AND title IN (
'Next steps & follow-ups','Open WebUI LXC (103)','OpenClaw VM (200)',
'BookStack LXC (104)','Claude Code LXC deployer (agentic.sh)','Claude LXC deployer',
'Iventoy Pxe Lxc','Mediastack Lxc','Usb Autosync',
'Operations notes','Overview','Network map','Master Index',
'Gramps Web — CT 109 (2026-05-25)','Ollama LXC (102)','Homelab Topology'
);
```
Expected: `DELETE 16`. (Leaves only the 9 Void-app-specific pages in void1, salvaged next.)
---
## Phase 4 — Collapse void1 → The Void
### Task 6: Salvage void1's unique dev docs into The Void
**Files:** none (SQL on prod)
- [ ] **Step 1: Create a "Void 1.x — Final State / Retrospective" page** (the record of where Void 1 ended)
```sql
WITH v AS (SELECT id sid FROM spaces WHERE slug='void')
INSERT INTO pages (space_id, slug, title, body_md, position)
VALUES ((SELECT sid FROM v), 'void1-retrospective', 'Void 1.x — Final State / Retrospective',
$md$# Void 1.x Final State / Retrospective
**Status at retirement (2026-06-05):** Void 1 ran on **CT 301**, `192.168.1.11:2424`, `void.hynesy.com`.
At the 8b cutover (2026-06-05, alpha-18) `void.hynesy.com` was repointed to **Void 2** (CT 311, `.216`).
CT 301 is **legacy-but-running**, kept as instant rollback; scheduled to be vzdump'd then retired after the grace period.
## What Void 1 was
Cradle-themed homelab dashboard (Node + Express + `node:sqlite`, gridstack "Sacred Valley", 8 character agents).
Architecture, agent system, dashboard progress and the Path B rebuild log are preserved as child pages of this page.
## Projects completed / migrated
- Sacred Valley dashboard, agent roster (Dross/Yerin/Little Blue/Orthos/Eithan/Lindon/Mercy), cron briefings.
- All knowledge migrated into Void 2 spaces (Wiki + The Void) on 2026-06-0405.
## Lineage
Services first documented/managed under Void 1 now live in the **Wiki** (source of truth) and, where they evolved,
the improved state is noted there — e.g. **Mediastack (CT 100)** began under Void-1 docs and is now Z→Z3 replicated and
documented under Wiki Hosts & Services with a Void-2 lineage note. See linked references.
$md$, 1)
ON CONFLICT (space_id, slug) DO NOTHING;
```
- [ ] **Step 2: Move the remaining void1 pages into The Void, nested under the retrospective**
```sql
WITH v1 AS (SELECT id FROM spaces WHERE slug='void1'),
vd AS (SELECT id FROM spaces WHERE slug='void'),
retro AS (SELECT id FROM pages WHERE space_id=(SELECT id FROM vd) AND slug='void1-retrospective')
UPDATE pages SET space_id=(SELECT id FROM vd), parent_id=(SELECT id FROM retro)
WHERE space_id=(SELECT id FROM v1) AND title IN (
'The Void — Architecture & Agent System',
'The Void — Dashboard Progress (2026-05-14)',
'Path B — Build Log',
'Deployment Guide',
'Cron Tasks & Schedules',
'Security Posture & Known Issues',
'Active Projects Status',
'Agent Roster & Personas'
);
```
Expected: `UPDATE 8`. (Agent Roster stays a live reference under The Void; the rest are historical children of the retrospective.)
### Task 7: Migrate void1 projects + reparent the moved build-log pages
**Files:** none (SQL on prod)
- [ ] **Step 1: Move real homelab projects into The Void with corrected status**
```sql
WITH v1 AS (SELECT id FROM spaces WHERE slug='void1'),
vd AS (SELECT id FROM spaces WHERE slug='void')
UPDATE projects p SET space_id=(SELECT id FROM vd), status=m.st
FROM (VALUES
('Homelab','active'),
('Homelab Atlas','active'),
('Knowledge Pipeline','active'),
('Network Rebuild','paused'),
('Sacred Valley Widgets — Phase 4 Follow-up','done'),
('USB Auto-Sync Drive','idea'),
('VM 203 Docker Stack Rebuild','active'),
('Win10 → Mediastack Migration','idea'),
('Z2 → Cluster Migration','active'),
('iVentoy PXE Server','paused')
) AS m(name, st)
WHERE p.space_id=(SELECT id FROM v1) AND p.name=m.name;
```
Expected: `UPDATE 10`.
- [ ] **Step 2: Salvage agent persona one-liners into the Agent Roster page, then delete the 7 agent "projects"**
The 7 agent-named projects (Dross, Eithan, Lindon, Little Blue, Mercy, Orthos, Yerin) are personas, not projects. Their descriptions are 1-liners. Append any not already on "Agent Roster & Personas" (now in The Void), then:
```sql
WITH v1 AS (SELECT id FROM spaces WHERE slug='void1')
DELETE FROM projects WHERE space_id=(SELECT id FROM v1)
AND name IN ('Dross','Eithan','Lindon','Little Blue','Mercy','Orthos','Yerin');
```
Expected: `DELETE 7`. (Live agents already exist in the `agents` table; these project stubs are redundant.)
- [ ] **Step 3: Reparent the 6 Void-2 dev docs moved in Task 4/Step 1 under the Build Log**
```sql
WITH vd AS (SELECT id FROM spaces WHERE slug='void'),
bl AS (SELECT id FROM pages WHERE space_id=(SELECT id FROM vd) AND title='Void 2.0 — Build Log')
UPDATE pages SET parent_id=(SELECT id FROM bl)
WHERE space_id=(SELECT id FROM vd) AND title IN (
'Void 2.0 — Deployment (CT 310 + 311)',
'Void 2.0 — Plan 1 Foundation (complete)',
'Void 2.0 — Plan 2 API + UI (complete)',
'Void 2.0 — Plan 3 Capture (complete)',
'Void 2.0 — Plan 4 Workers (complete)',
'Dashboard rebuild & Orthos overhaul (2026-05-16)'
);
```
Expected: `UPDATE 6`.
### Task 8: Handle void1 conversations, then delete the void1 space
**Files:** none (SQL on prod)
- [ ] **Step 1: Drop the 30 conversations** (historical Dross/companion chat logs — user confirmed 2026-06-05: drop)
```sql
WITH v1 AS (SELECT id FROM spaces WHERE slug='void1')
DELETE FROM conversations WHERE space_id=(SELECT id FROM v1);
-- (messages cascade via FK)
```
- [ ] **Step 2: Confirm void1 is empty of survivors**
```sql
WITH v1 AS (SELECT id FROM spaces WHERE slug='void1')
SELECT
(SELECT count(*) FROM pages WHERE space_id=(SELECT id FROM v1)) pages,
(SELECT count(*) FROM projects WHERE space_id=(SELECT id FROM v1)) projects,
(SELECT count(*) FROM conversations WHERE space_id=(SELECT id FROM v1)) convos,
(SELECT count(*) FROM refs WHERE space_id=(SELECT id FROM v1)) refs;
```
Expected: all `0`. If not, STOP and reconcile.
- [ ] **Step 3: Delete the void1 space**
```sql
DELETE FROM spaces WHERE slug='void1';
```
Expected: `DELETE 1` (cascades any stragglers).
### Task 9: Rebuild a single Master Index
**Files:** none (web editor on prod)
- [ ] **Step 1:** The wiki "Master Index" survived (wiki copy was fuller). Open it in the editor and update links so they point at the new sectioned Wiki structure + The Void hub; remove dead links to deleted void1 pages. Save.
---
## Phase 5 — Fold void3 into the Void 3.0 project
### Task 10: Move void3 pages into The Void under a Void 3.0 hub page, link to the project
**Files:** none (SQL on prod)
- [ ] **Step 1: Create a Void 3.0 hub page in The Void**
```sql
WITH vd AS (SELECT id sid FROM spaces WHERE slug='void')
INSERT INTO pages (space_id, slug, title, body_md, position)
VALUES ((SELECT sid FROM vd),'void3-hub','Void 3.0 — Roadmap & Audit (2026-06-02)',
'_Point-in-time homelab evolution audit + roadmap, migrated from the former void3 space. Child pages are the original audit notes._', 2)
ON CONFLICT (space_id, slug) DO NOTHING;
```
- [ ] **Step 2: Move all 8 void3 pages under the hub**
```sql
WITH v3 AS (SELECT id FROM spaces WHERE slug='void3'),
vd AS (SELECT id FROM spaces WHERE slug='void'),
hub AS (SELECT id FROM pages WHERE space_id=(SELECT id FROM vd) AND slug='void3-hub')
UPDATE pages SET space_id=(SELECT id FROM vd), parent_id=(SELECT id FROM hub)
WHERE space_id=(SELECT id FROM v3);
```
Expected: `UPDATE 8`.
- [ ] **Step 3: Link the hub page to the "Void 3.0" project**
```sql
WITH vd AS (SELECT id FROM spaces WHERE slug='void'),
pr AS (SELECT id FROM projects WHERE space_id=(SELECT id FROM vd) AND name='Void 3.0'),
hub AS (SELECT id FROM pages WHERE space_id=(SELECT id FROM vd) AND slug='void3-hub')
INSERT INTO entity_links (from_type, from_id, to_type, to_id, relation)
VALUES ('project',(SELECT id FROM pr),'page',(SELECT id FROM hub),'reference');
```
- [ ] **Step 4: Move the void3 conversation (if any) then delete the space**
```sql
WITH v3 AS (SELECT id FROM spaces WHERE slug='void3')
DELETE FROM conversations WHERE space_id=(SELECT id FROM v3); -- user confirmed: drop
DELETE FROM spaces WHERE slug='void3';
```
Expected: `DELETE 1`.
---
## Phase 6 — Relocate `plans`, decide `external-research`
**User decision (2026-06-05): KEEP the plan pages** — give them a home in The Void (not the Wiki, since they are project artifacts not infra reference). Move them under an "Implementation Plans" hub page in `void` and link each to its related project.
### Task 11: Move plan pages into The Void under an "Implementation Plans" hub, then delete the empty plans space
**Files:** none (SQL on prod)
- [ ] **Step 1: Create the hub page in The Void**
```sql
WITH vd AS (SELECT id sid FROM spaces WHERE slug='void')
INSERT INTO pages (space_id, slug, title, body_md, position)
VALUES ((SELECT sid FROM vd),'implementation-plans','Implementation Plans',
'_Implementation plans for homelab + Void projects, migrated from the former plans space. Canonical copies also live in the repo `docs/superpowers/plans/` and `~/.claude/plans/`. Child pages below; each is linked to its related project._', 3)
ON CONFLICT (space_id, slug) DO NOTHING;
```
- [ ] **Step 2: Move all 6 plan pages under the hub**
```sql
WITH p AS (SELECT id FROM spaces WHERE slug='plans'),
vd AS (SELECT id FROM spaces WHERE slug='void'),
hub AS (SELECT id FROM pages WHERE space_id=(SELECT id FROM vd) AND slug='implementation-plans')
UPDATE pages SET space_id=(SELECT id FROM vd), parent_id=(SELECT id FROM hub)
WHERE space_id=(SELECT id FROM p);
```
Expected: `UPDATE 6`.
- [ ] **Step 3: Link each plan page to its related project** (where one exists in The Void)
```sql
WITH vd AS (SELECT id sid FROM spaces WHERE slug='void')
INSERT INTO entity_links (from_type, from_id, to_type, to_id, relation)
SELECT 'project', pr.id, 'page', pg.id, 'reference'
FROM (VALUES
('Plan: Homelab USB Auto-Sync Drive', 'USB Auto-Sync Drive'),
('Plan: The Void — Path B Rebuild (Mastra Agent Framework)', 'Void 1.x'),
('Plan: Homepage-style Service Cards in Sacred Valley','Void 2.0'),
('Plan: Replace BookStack with Mercy''s Records (Option B)', 'Void 3.0')
) AS m(page_title, project_name)
JOIN pages pg ON pg.space_id=(SELECT sid FROM vd) AND pg.title=m.page_title
JOIN projects pr ON pr.space_id=(SELECT sid FROM vd) AND pr.name=m.project_name;
```
(The two Farm plans — "Farm (won) Failure-Recovery Plan", "Farm Timelapse System — Implementation Plan" — have no matching project in The Void; they remain under the hub, unlinked. Optionally create Farm projects later.)
- [ ] **Step 4: Confirm plans space is empty, then delete it**
```sql
SELECT count(*) FROM pages WHERE space_id=(SELECT id FROM spaces WHERE slug='plans'); -- expect 0
WITH p AS (SELECT id FROM spaces WHERE slug='plans')
DELETE FROM conversations WHERE space_id=(SELECT id FROM p); -- drop the placeholder convo
DELETE FROM spaces WHERE slug='plans';
```
Expected: count `0`, then `DELETE 1`.
### Task 12: Decide `external-research`
**Files:** none
- [ ] **Step 1:** `external-research` is empty but is the bound Space for the MCP external-research agent (`reference-void-mcp-endpoint`). Check it's still in use:
```sql
SELECT a.slug, a.scopes FROM agents a WHERE a.scopes->>'space_id' = (SELECT id::text FROM spaces WHERE slug='external-research');
```
- [ ] **Step 2:** If an agent is bound → **keep** the space (add a one-line description page explaining its purpose). If no agent → `DELETE FROM spaces WHERE slug='external-research';`.
---
## Phase 7 — Lineage cross-links
### Task 13: Express doc lineage between The Void projects and Wiki service docs
**Files:** none (SQL + web editor on prod)
- [ ] **Step 1: Mediastack lineage (the user's example)** — add an evolution note + link
In the editor, append to Wiki "Mediastack LXC (100)":
```
> **Lineage:** First documented under Void 1. Now CT 100 on Z, Donatello/Leonardo pools, syncoid daily replica Z→Z3, and the Void-2 ingress host. See The Void Void 2.0.
```
Then link the Void 2.0 project → the Mediastack page:
```sql
WITH vd AS (SELECT id FROM spaces WHERE slug='void'),
pr AS (SELECT id FROM projects WHERE space_id=(SELECT id FROM vd) AND name='Void 2.0'),
w AS (SELECT id FROM spaces WHERE slug='wiki'),
pg AS (SELECT id FROM pages WHERE space_id=(SELECT id FROM w) AND title='Mediastack LXC (100)')
INSERT INTO entity_links (from_type, from_id, to_type, to_id, relation)
VALUES ('project',(SELECT id FROM pr),'page',(SELECT id FROM pg),'reference');
```
- [ ] **Step 2: Repeat for the other materially-evolved services** — Ollama (GPU userspace driver + sharing), Cluster and HA (qdevice, replication). Same pattern: add a one-line lineage note in the Wiki page + an `entity_links` row from the relevant Void project.
---
## Phase 8 — Verify, re-embed, finalize
### Task 14: Final verification
**Files:** none
- [ ] **Step 1: Space inventory is the target shape**
```sql
SELECT s.slug, (SELECT count(*) FROM pages p WHERE p.space_id=s.id) pages,
(SELECT count(*) FROM projects pr WHERE pr.space_id=s.id) projects
FROM spaces s ORDER BY s.slug;
```
Expected: `bookmarks`, `void` (≈12 pages incl. retrospective+build-log children+void3 hub; ~13 projects), `wiki` (≈21 pages: root + 3 sections + 17 content), and `external-research` only if kept. No `void1`/`void3`/`plans`.
- [ ] **Step 2: No orphaned page references** (links pointing at deleted pages)
```sql
SELECT count(*) FROM entity_links el
WHERE el.to_type='page' AND NOT EXISTS (SELECT 1 FROM pages p WHERE p.id=el.to_id);
```
Expected: `0`. Delete any orphans found.
- [ ] **Step 3: Re-embed edited/moved pages so hybrid search stays accurate**
Content edits made via the web editor already re-embed. For pages changed only by SQL (parent/position/space moves) embeddings are still valid (body unchanged). If any body was edited via SQL, run the app's embed backfill:
```bash
ssh root@192.168.1.216 'cd /opt/void-server && node lib/db/backfill_embeddings.js 2>/dev/null || echo "no backfill script — re-save the page in the editor"'
```
- [ ] **Step 4: UI walkthrough** — open `void.hynesy.com`:
- Wiki space: Overview at top, three sections in order, no Void-2 plan docs.
- The Void space: projects Void 1.x/2.0/3.0 + the 10 homelab projects with corrected statuses; Void 1.x retrospective with its historical children; Void 3.0 hub with the 8 audit pages; Build Log with its dev-doc children.
- Search "preserving config across upgrades" → returns the merged Ollama page.
- [ ] **Step 5: Update the project memory** — note the consolidation done (spaces 7→3, wiki sectioned, void1/void3/plans retired) in `void-v2-roadmap` / `void-v2-execution-in-progress`, since the roadmap's "clean up old void1/void3/plans spaces" item is now complete.
### Task 15: BookStack — minimal (per user: secondary mirror only)
**Files:** none
- [ ] **Step 1:** Leave BookStack content as-is (it's already Overview-first). Do **not** invest in re-chaptering. Optionally add one note to BookStack "Overview": `> Primary docs now live in The Void Wiki (void.hynesy.com). This BookStack is a secondary reference mirror.` Skip if not wanted.
---
## Self-review notes
- **Spec coverage:** ordering fix (Tasks 1-3, Phase 2), dedup with lineage-aware merges (Phase 3), collapse void1 + preserve Void 1.x record (Phase 4), void3 fold (Phase 5), plans/external-research (Phase 6), lineage cross-links incl. mediastack example (Phase 7), Wiki-as-source-of-truth + BookStack demoted (Phase 2 + Task 15). All requested scope covered.
- **Destructive steps** (Tasks 5,7,8,10,11) are each gated by a verification query proving a survivor exists before delete, and the whole run is recoverable from Task 0's snapshot + pg_dump.
- **Confirm-with-user gates:** conversation deletion (Task 8/10/11), external-research deletion (Task 12), keeping plan pages browsable (Task 11).
- **Reversibility:** all Phase 1 code is additive (nullable/defaulted column); content moves are reversible from the dump.