docs: fix all internal cross-references after restructure

This commit is contained in:
2026-06-21 12:48:04 +02:00
parent 93aa6d9b42
commit 65597de00d
29 changed files with 4738 additions and 85 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,309 @@
# GPX Manager 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:** Build a protected admin page at `/gpx-manager` that lists all trip GPX files and supports upload and deletion via the Grav API.
**Architecture:** A Grav page (`user/pages/03.gpx-manager/`) with a custom Twig template. Access is enforced by the Login plugin via `access.admin.login: true` in page frontmatter. The template renders a section per trip using the Grav page tree, then vanilla JavaScript calls the existing Grav API (`/api/v1/pages{route}/media`) using the browser's live session cookie — no JWT or separate login needed.
**Tech Stack:** Grav 2.0 Twig, Vanilla JS (fetch API), Grav API plugin v1, Grav Login plugin (page access control)
## Global Constraints
- Grav 2.0.0-rc.9 + Admin2 v2.0.0-rc.15; theme `intotheeast` at `user/themes/intotheeast/`
- API base URL: `/api/v1` (`route: /api`, `version_prefix: v1` in `user/plugins/api/api.yaml`)
- Session auth: all fetch calls use `credentials: 'include'` — no JWT handling (`session_enabled: true` in api.yaml)
- API media routes (confirmed from `user/plugins/api/classes/Api/ApiRouter.php:333`):
- `GET /api/v1/pages{route}/media` — list; response `{ data: [{ filename, size, modified, type }] }`
- `POST /api/v1/pages{route}/media` — multipart file upload
- `DELETE /api/v1/pages{route}/media/{filename}` — delete single file
- `{route}` is the full Grav route including leading slash, e.g. `/trips/italy-2025`
- Style: teal `#1F6B5A`, warm border `#e0ddd6`, font-family `'DM Sans', sans-serif` — match existing theme tokens
- No new plugins, no npm, no build step. All changes inside `user/` only.
- The page must be `visible: false` — must not appear in site navigation.
- Trip pages live at `user/pages/01.trips/<slug>/`; retrieved via `grav.pages.find('/trips').children.published()`
---
### Task 1: Page definition
**Files:**
- Create: `user/pages/03.gpx-manager/gpx-manager.md`
**Interfaces:**
- Produces: Grav page routed at `/gpx-manager`, protected by Login plugin, hidden from nav, using template `gpx-manager`
- [ ] **Step 1: Create the page file**
Create `user/pages/03.gpx-manager/gpx-manager.md` with this exact content:
```
---
title: 'GPX Manager'
template: gpx-manager
visible: false
routable: true
access:
admin.login: true
---
```
- [ ] **Step 2: Verify protection (no template yet)**
With the dev server running, open `http://localhost:8081/gpx-manager` while **logged out** of admin. You should be redirected to the login page. While **logged in**, you'll see a blank page or a Twig error (template missing) — that's fine at this stage.
- [ ] **Step 3: Commit**
```bash
git -C user add pages/03.gpx-manager/gpx-manager.md
git -C user commit -m "feat: add gpx-manager page definition (access-protected)"
```
---
### Task 2: Template — layout and trip sections
**Files:**
- Create: `user/themes/intotheeast/templates/gpx-manager.html.twig`
**Interfaces:**
- Consumes: `grav.pages.find('/trips').children.published()` — each trip object exposes `.route` (string, e.g. `/trips/italy-2025`), `.title` (string), `.slug` (string, e.g. `italy-2025`)
- Produces: one `.gpx-trip[data-route]` section per trip; `data-route` = full route string (e.g. `/trips/italy-2025`); `data-trip-route` on upload form = same value
- [ ] **Step 1: Create the template**
Create `user/themes/intotheeast/templates/gpx-manager.html.twig`:
```twig
{% extends 'partials/base.html.twig' %}
{% block content %}
{% set trips_page = grav.pages.find('/trips') %}
{% set trips = trips_page ? trips_page.children.published() : [] %}
<div class="gpx-manager">
<h1 class="gpx-manager__title">GPX Files</h1>
{% if trips is empty %}
<p>No trips found.</p>
{% else %}
{% for trip in trips %}
<section class="gpx-trip" data-route="{{ trip.route }}">
<h2 class="gpx-trip__name">{{ trip.title }}</h2>
<div class="gpx-file-list" id="files-{{ trip.slug }}">
<p class="gpx-loading">Loading…</p>
</div>
<form class="gpx-upload-form" data-trip-route="{{ trip.route }}">
<label class="gpx-upload-label">
<input type="file" accept=".gpx,application/gpx+xml" name="file" class="gpx-file-input">
</label>
<button type="submit" class="gpx-upload-btn">Upload</button>
<span class="gpx-status"></span>
</form>
</section>
{% endfor %}
{% endif %}
</div>
<style>
.gpx-manager { max-width: 720px; margin: 2rem auto; padding: 0 1rem; font-family: 'DM Sans', sans-serif; }
.gpx-manager__title { font-family: 'DM Serif Display', serif; font-size: 1.75rem; margin-bottom: 2rem; }
.gpx-trip { border: 1px solid #e0ddd6; border-radius: 8px; padding: 1.25rem; margin-bottom: 1.5rem; }
.gpx-trip__name { font-size: 1.1rem; font-weight: 600; margin: 0 0 1rem; }
.gpx-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; margin-bottom: 1rem; }
.gpx-table th { text-align: left; color: #666; font-weight: 500; padding: 0.25rem 0.5rem; border-bottom: 1px solid #e0ddd6; }
.gpx-table td { padding: 0.5rem; border-bottom: 1px solid #f0ede8; }
.gpx-empty, .gpx-loading { color: #888; font-size: 0.875rem; margin-bottom: 0.75rem; }
.gpx-upload-form { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.75rem; }
.gpx-upload-btn { background: #1F6B5A; color: #fff; border: none; border-radius: 5px; padding: 0.4rem 1rem; font-size: 0.875rem; cursor: pointer; }
.gpx-upload-btn:disabled { opacity: 0.5; cursor: default; }
.gpx-delete { background: none; border: 1px solid #ccc; border-radius: 4px; padding: 0.2rem 0.5rem; font-size: 0.8rem; cursor: pointer; color: #c0392b; }
.gpx-delete:disabled { opacity: 0.5; }
.gpx-status { font-size: 0.8rem; color: #555; }
.gpx-status.error { color: #c0392b; }
</style>
<script>
/* GPX manager JS — added in Task 3 */
</script>
{% endblock %}
```
- [ ] **Step 2: Verify trip sections render**
Open `http://localhost:8081/gpx-manager` while logged in. You should see:
- Heading "GPX Files"
- One card per trip (Italy 2025, Japan-Korea 2026) each showing "Loading…" and an upload form with a file picker and Upload button.
- The page header/nav from `base.html.twig` is present.
- [ ] **Step 3: Commit**
```bash
git -C user add themes/intotheeast/templates/gpx-manager.html.twig
git -C user commit -m "feat: gpx-manager template layout with trip sections"
```
---
### Task 3: JavaScript — list, upload, delete
**Files:**
- Modify: `user/themes/intotheeast/templates/gpx-manager.html.twig` — replace `/* GPX manager JS — added in Task 3 */` inside the existing `<script>` tag
**Interfaces:**
- Consumes: `.gpx-trip[data-route]` and `.gpx-upload-form[data-trip-route]` from Task 2
- Consumes: Grav API at `/api/v1` (session cookie auth)
- API list response: `{ data: [{ filename: string, size: number, modified: string, type: string }] }`
- API upload: multipart `FormData` with field name `file`
- API delete: `DELETE /api/v1/pages{route}/media/{encodedFilename}` → 200 or 204 on success
- [ ] **Step 1: Replace the placeholder comment with the full script**
In `user/themes/intotheeast/templates/gpx-manager.html.twig`, replace `/* GPX manager JS — added in Task 3 */` with:
```javascript
const API = '/api/v1';
function formatSize(bytes) {
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
return (bytes / 1024).toFixed(0) + ' KB';
}
function formatDate(iso) {
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
}
async function apiFetch(url, options) {
const res = await fetch(url, { credentials: 'include', ...options });
if (res.status === 401) { window.location.href = '/admin'; return null; }
return res;
}
async function loadFiles(tripRoute) {
const res = await apiFetch(`${API}/pages${tripRoute}/media`);
if (!res || !res.ok) return [];
const data = await res.json();
return (data.data || []).filter(f => f.filename.toLowerCase().endsWith('.gpx'));
}
async function renderTrip(tripEl) {
const route = tripEl.dataset.route;
const list = tripEl.querySelector('.gpx-file-list');
list.innerHTML = '<p class="gpx-loading">Loading…</p>';
const files = await loadFiles(route);
if (files.length === 0) {
list.innerHTML = '<p class="gpx-empty">No GPX files.</p>';
return;
}
const rows = files.map(f =>
`<tr>
<td>${f.filename}</td>
<td>${formatSize(f.size)}</td>
<td>${formatDate(f.modified)}</td>
<td><button class="gpx-delete" data-filename="${f.filename}">Delete</button></td>
</tr>`
).join('');
list.innerHTML = `<table class="gpx-table">
<thead><tr><th>File</th><th>Size</th><th>Modified</th><th></th></tr></thead>
<tbody>${rows}</tbody>
</table>`;
list.querySelectorAll('.gpx-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm(`Delete ${btn.dataset.filename}?`)) return;
btn.disabled = true;
const res = await apiFetch(
`${API}/pages${route}/media/${encodeURIComponent(btn.dataset.filename)}`,
{ method: 'DELETE' }
);
if (res && (res.ok || res.status === 204)) {
await renderTrip(tripEl);
} else {
btn.disabled = false;
alert('Delete failed — check console.');
}
});
});
}
function initUpload(formEl) {
formEl.addEventListener('submit', async e => {
e.preventDefault();
const route = formEl.dataset.tripRoute;
const fileInput = formEl.querySelector('input[type=file]');
const file = fileInput.files[0];
const status = formEl.querySelector('.gpx-status');
const btn = formEl.querySelector('.gpx-upload-btn');
if (!file) { status.textContent = 'Choose a file first.'; return; }
status.textContent = 'Uploading…';
status.className = 'gpx-status';
btn.disabled = true;
const fd = new FormData();
fd.append('file', file);
const res = await apiFetch(`${API}/pages${route}/media`, { method: 'POST', body: fd });
btn.disabled = false;
if (res && res.ok) {
status.textContent = 'Uploaded!';
fileInput.value = '';
await renderTrip(formEl.closest('.gpx-trip'));
setTimeout(() => { status.textContent = ''; }, 3000);
} else {
const err = res ? await res.json().catch(() => ({})) : {};
status.textContent = 'Error: ' + (err.detail || (res ? res.statusText : 'network error'));
status.className = 'gpx-status error';
}
});
}
document.querySelectorAll('.gpx-trip').forEach(renderTrip);
document.querySelectorAll('.gpx-upload-form').forEach(initUpload);
```
- [ ] **Step 2: Test file listing**
Open `http://localhost:8081/gpx-manager` while logged in. Open DevTools → Network tab.
Expected:
- `GET /api/v1/pages/trips/italy-2025/media` → 200, Italy 2025 section shows a table with 3 rows (day-5, day-6, day-8) with sizes (~1.8 MB, ~2.2 MB, ~1.9 MB) and dates.
- `GET /api/v1/pages/trips/japan-korea-2026/media` → 200, Japan-Korea 2026 section shows "No GPX files."
- [ ] **Step 3: Test upload**
In the Japan-Korea 2026 section: click the file input, select any `.gpx` file from disk, click Upload.
Expected:
- Status shows "Uploading…" then "Uploaded!"
- The file table re-renders with the new file listed.
- DevTools shows `POST /api/v1/pages/trips/japan-korea-2026/media` → 200.
- [ ] **Step 4: Test delete**
Click Delete on the file just uploaded. Confirm the dialog.
Expected:
- The row disappears immediately.
- DevTools shows `DELETE /api/v1/pages/trips/japan-korea-2026/media/<filename>` → 200 or 204.
- Reload the page — file is gone.
- [ ] **Step 5: Test 401 redirect**
Log out of Admin2. In a new tab, navigate to `http://localhost:8081/gpx-manager`.
Expected: redirected to login page (Login plugin enforces `access.admin.login: true` before the page renders, so the JS never runs).
- [ ] **Step 6: Commit**
```bash
git -C user add themes/intotheeast/templates/gpx-manager.html.twig
git -C user commit -m "feat: gpx-manager list, upload, delete via Grav API session auth"
```
@@ -1,6 +1,6 @@
# Stats Redesign — Implementation Plan
*Derived from spec: docs/superpowers/specs/2026-06-19-stats-redesign.md*
*Derived from spec: docs/working/specs/2026-06-19-stats-redesign.md*
> **For agentic workers:** Use superpowers:subagent-driven-development to execute this plan task-by-task.
@@ -2,6 +2,8 @@
> **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.
**Status:** 🔄 In progress — Task 1 complete (skip link), Tasks 26 open
**Goal:** Fix all eight WCAG 2.1 AA failures identified in the accessibility audit and add axe-core Playwright regression tests.
**Architecture:** Six sequential tasks — each implements one audit finding (or related group), writes a Playwright test first, then implements the fix in the relevant template/CSS/JS files. All tests go into a new `tests/ui/accessibility.spec.js` file that grows task by task. Task 6 adds axe-core automated scans on top of the feature-specific checks.
@@ -11,7 +11,7 @@
## Global Constraints
- All file moves use `git mv` — never `mv` — so git history is preserved.
- The existing spec file (`docs/superpowers/specs/2026-06-21-documentation-restructure-design.md`) is itself one of the files being moved — move it in Task 1 with the rest.
- The existing spec file (`docs/working/specs/2026-06-21-documentation-restructure-design.md`) is itself one of the files being moved — move it in Task 1 with the rest.
- Do NOT modify any file inside `user/` — that is a separate git repo.
- Do NOT touch memory files outside of Task 9.
- CLAUDE.md lives at repo root and stays there.
@@ -43,12 +43,12 @@ mkdir -p /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/docs/research
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
# All specs
for f in docs/superpowers/specs/*.md; do
for f in docs/working/specs/*.md; do
git mv "$f" "docs/working/specs/$(basename "$f")"
done
# All plans
for f in docs/superpowers/plans/*.md; do
for f in docs/working/plans/*.md; do
git mv "$f" "docs/working/plans/$(basename "$f")"
done
```
@@ -57,10 +57,10 @@ done
```bash
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git mv docs/milestone-1-spec.md docs/working/milestones/milestone-1.md
git mv docs/milestone-2-spec.md docs/working/milestones/milestone-2.md
git mv docs/milestone-3-spec.md docs/working/milestones/milestone-3.md
git mv docs/milestone-4-spec.md docs/working/milestones/milestone-4.md
git mv docs/working/milestones/milestone-1.md docs/working/milestones/milestone-1.md
git mv docs/working/milestones/milestone-2.md docs/working/milestones/milestone-2.md
git mv docs/working/milestones/milestone-3.md docs/working/milestones/milestone-3.md
git mv docs/working/milestones/milestone-4.md docs/working/milestones/milestone-4.md
```
- [ ] **Step 4: Move QA docs**
@@ -103,10 +103,10 @@ rmdir docs/design # now empty
```bash
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git mv docs/posting-pipeline.md docs/guides/posting.md
git mv docs/guides/posting.md docs/guides/posting.md
```
- [ ] **Step 9: Verify — no files remain at docs/ root, no docs/superpowers/ exists**
- [ ] **Step 9: Verify — no files remain at docs/ root, no docs/working/ exists**
```bash
find /home/mischa/Nextcloud/Projects/travel-blog-intotheeast/docs -maxdepth 1 -type f
@@ -137,27 +137,27 @@ git commit -m "docs: restructure docs/ into guides/ reference/ working/ research
**Files:**
- Modify: all files under `docs/working/` and `docs/research/` that reference old paths
After the moves, links inside plan and spec files still point to old paths like `docs/superpowers/plans/...` and `docs/milestone-1-spec.md`. This task fixes them all.
After the moves, links inside plan and spec files still point to old paths like `docs/working/plans/...` and `docs/working/milestones/milestone-1.md`. This task fixes them all.
- [ ] **Step 1: Replace docs/superpowers/specs/ references**
- [ ] **Step 1: Replace docs/working/specs/ references**
```bash
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
grep -rl "docs/superpowers/specs/" docs/ | xargs sed -i 's|docs/superpowers/specs/|docs/working/specs/|g'
grep -rl "docs/working/specs/" docs/ | xargs sed -i 's|docs/working/specs/|docs/working/specs/|g'
```
- [ ] **Step 2: Replace docs/superpowers/plans/ references**
- [ ] **Step 2: Replace docs/working/plans/ references**
```bash
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
grep -rl "docs/superpowers/plans/" docs/ | xargs sed -i 's|docs/superpowers/plans/|docs/working/plans/|g'
grep -rl "docs/working/plans/" docs/ | xargs sed -i 's|docs/working/plans/|docs/working/plans/|g'
```
- [ ] **Step 3: Replace docs/superpowers/ catch-all (any remaining bare references)**
- [ ] **Step 3: Replace docs/working/ catch-all (any remaining bare references)**
```bash
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
grep -rl "docs/superpowers/" docs/ | xargs sed -i 's|docs/superpowers/|docs/working/|g'
grep -rl "docs/working/" docs/ | xargs sed -i 's|docs/working/|docs/working/|g'
```
- [ ] **Step 4: Replace milestone spec references**
@@ -181,16 +181,16 @@ grep -rl "docs/posting-pipeline" docs/ | xargs sed -i 's|docs/posting-pipeline\.
```bash
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
sed -i 's|docs/superpowers/specs/|docs/working/specs/|g' CLAUDE.md
sed -i 's|docs/superpowers/plans/|docs/working/plans/|g' CLAUDE.md
sed -i 's|docs/superpowers/|docs/working/|g' CLAUDE.md
sed -i 's|docs/working/specs/|docs/working/specs/|g' CLAUDE.md
sed -i 's|docs/working/plans/|docs/working/plans/|g' CLAUDE.md
sed -i 's|docs/working/|docs/working/|g' CLAUDE.md
```
- [ ] **Step 7: Verify no old paths remain**
```bash
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
grep -r "docs/superpowers/" docs/ CLAUDE.md
grep -r "docs/working/" docs/ CLAUDE.md
# Expected: no output
grep -r "docs/milestone-[0-9]-spec" docs/ CLAUDE.md
# Expected: no output
@@ -794,7 +794,7 @@ Full setup guide: [`docs/guides/local-setup.md`](docs/guides/local-setup.md)
Specs: `docs/working/specs/YYYY-MM-DD-<topic>-design.md`
Plans: `docs/working/plans/YYYY-MM-DD-<topic>.md`
The brainstorming and writing-plans skills default to `docs/superpowers/`; these lines override that default.
The brainstorming and writing-plans skills default to `docs/working/`; these lines override that default.
```
- [ ] **Step 3: Verify CLAUDE.md**
@@ -1016,7 +1016,7 @@ git commit -m "docs: add architecture overview reference"
**Files:**
- Modify: 4 memory files in `~/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/`
Four memory files still reference `docs/superpowers/` paths. Update them to `docs/working/`.
Four memory files still reference `docs/working/` paths. Update them to `docs/working/`.
- [ ] **Step 1: Identify affected lines**
@@ -1032,9 +1032,9 @@ grep -n "docs/superpowers" "$MEMORY_DIR/MEMORY.md" "$MEMORY_DIR/feedback-plan-ex
MEMORY_DIR="/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory"
for f in "$MEMORY_DIR/MEMORY.md" "$MEMORY_DIR/feedback-plan-execution-gap.md" \
"$MEMORY_DIR/project-story-mode-and-maplibre.md" "$MEMORY_DIR/project-homepage-redesign.md"; do
sed -i 's|docs/superpowers/specs/|docs/working/specs/|g' "$f"
sed -i 's|docs/superpowers/plans/|docs/working/plans/|g' "$f"
sed -i 's|docs/superpowers/|docs/working/|g' "$f"
sed -i 's|docs/working/specs/|docs/working/specs/|g' "$f"
sed -i 's|docs/working/plans/|docs/working/plans/|g' "$f"
sed -i 's|docs/working/|docs/working/|g' "$f"
done
```
@@ -1055,7 +1055,7 @@ Verify MEMORY.md now reads correctly:
```bash
grep "plans\|specs\|superpowers\|working" \
"/home/mischa/.claude/projects/-home-mischa-Nextcloud-Projects-travel-blog-intotheeast/memory/MEMORY.md"
# Expected: all plan/spec references point to docs/working/, none to docs/superpowers/
# Expected: all plan/spec references point to docs/working/, none to docs/working/
```
- [ ] **Step 5: Final verification — complete restructure**