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

36 KiB
Raw Permalink Blame History

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:

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):

-- 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):

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:

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:

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:

-- 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:

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:

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']):

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):

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
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)
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:

    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
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:

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:

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:

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
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
-- 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)
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
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:

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
-- 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
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)
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
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
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:

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
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)
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
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
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

Files: none (SQL on prod)

  • Step 1: Create a Void 3.0 hub page in The Void
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
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
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
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
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
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)
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
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:
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';.

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:

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
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)
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:

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.