summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/plans/2026-11-05-hardenedpass-docs.md
blob: b5d408ca49630823197b86d74087c487c9e750fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# HardenedPass Docs Section 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:** Add a bilingual, wiki-like HardenedPass documentation section to danix.xyz.

**Architecture:** A new Hugo section `hardenedpass` with its own `list` and `single` layouts in the theme submodule. A sidebar shows the page tree and table of contents; on mobile both collapse into native `<details>`. Pages carry a unique `weight` that gives one reading order across subsections.

**Tech Stack:** Hugo extended, Tailwind 3 (`npm run build`), TOML front matter, Go templates.

**Spec:** `docs/superpowers/specs/2026-11-05-hardenedpass-docs-design.md`

**Deviations from spec, found while planning:**
- `<details>` cannot be open on desktop and closed on mobile without JavaScript, so the tree renders twice, from a separate `docs-tree.html`. Prev/next is in `docs-pager.html`, because it sits in the content column.
- The menu entry moves to the last task. A `pageRef` to a draft page has no target, so adding it earlier could break the menu if `master` is deployed before the pages are published.

**Rules for every task:**
- `SITE` is `~/Programming/GIT/danix.xyz-hacker-theme`, `THEME` is `$SITE/themes/danix-xyz-hacker`, `APP` is `~/Programming/GIT/HardenedPass`.
- Build into a scratch directory, never `public/`: `hugo -D --gc --minify -d "$OUT"`, with `OUT` a temporary directory.
- Never touch the `production` branch. Commit GPG signed. Do not commit `.claude/settings.local.json`. Push only when the maintainer asks.
- All new pages have `draft = true`.

---

### Task 1: Skeleton pages

**Files:**
- Create: `content/{en,it}/hardenedpass/_index.md`
- Create: `content/{en,it}/hardenedpass/{getting-started,using,reference}/_index.md`
- Create: `content/{en,it}/hardenedpass/getting-started/{concepts,install,create-store,connect}/index.md`
- Create: `content/{en,it}/hardenedpass/using/{entries,otp,sync,autofill}/index.md`
- Create: `content/{en,it}/hardenedpass/reference/{settings,security,troubleshooting}/index.md`

- [ ] **Step 1: Generate the 30 files**

```bash
cd "$SITE"
python3 - <<'EOF'
import pathlib
secs = {
 "getting-started": (10, "Getting started", "Per iniziare", [
  ("concepts", 10, "Concepts", "Concetti"), ("install", 20, "Install", "Installazione"),
  ("create-store", 30, "Create a store", "Crea un archivio"), ("connect", 40, "Connect the app", "Collega l'app")]),
 "using": (20, "Using the app", "Usare l'app", [
  ("entries", 50, "Entries", "Voci"), ("otp", 60, "OTP codes", "Codici OTP"),
  ("sync", 70, "Sync", "Sincronizzazione"), ("autofill", 80, "Autofill", "Autofill")]),
 "reference": (30, "Reference", "Riferimento", [
  ("settings", 90, "Settings", "Impostazioni"), ("security", 100, "Security", "Sicurezza"),
  ("troubleshooting", 110, "Troubleshooting", "Risoluzione dei problemi")]),
}
def put(path, title, weight=None):
    w = f"weight = {weight}\n" if weight else ""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(f'+++\ntitle = "{title}"\ndate = 2026-11-05T00:00:00Z\ndescription = ""\n{w}draft = true\n+++\n\nPlaceholder.\n')
for i, lang in enumerate(("en", "it")):
    root = pathlib.Path("content") / lang / "hardenedpass"
    put(root / "_index.md", "HardenedPass")
    for slug, (w, en, it, pages) in secs.items():
        put(root / slug / "_index.md", (en, it)[i], w)
        for p, pw, pen, pit in pages:
            put(root / slug / p / "index.md", (pen, pit)[i], pw)
EOF
find content -path '*hardenedpass*' -name '*.md' | wc -l
```
Expected: `30`

No commit yet: Task 2 commits these with the layouts that render them.

---

### Task 2: Layouts

**Files (all in `THEME`):**
- Create: `layouts/hardenedpass/single.html`, `layouts/hardenedpass/list.html`
- Create: `layouts/partials/docs-nav.html`, `layouts/partials/docs-tree.html`, `layouts/partials/docs-pager.html`
- Modify: `layouts/index.json:2-3`

**Files (in `SITE`):**
- Modify: `i18n/en.yaml`, `i18n/it.yaml` (append)

- [ ] **Step 1: Write the failing check**

```bash
cat > "$OUT_CHECK" <<'EOF'
#!/bin/bash
# Usage: check.sh <build dir>. Fails if the docs section is not wired up.
set -e
d=$1
c=$d/en/hardenedpass/getting-started/concepts/index.html
[ -f "$c" ] || c=$d/hardenedpass/getting-started/concepts/index.html
k=${c/concepts/connect}
u=${c/getting-started\/concepts/using\/entries}
grep -q 'rel=.\?next' "$c"
grep -Eq 'href="?[^" >]*/install/"?[^>]*rel=.?next' "$c"
grep -Eq 'href="?[^" >]*/using/entries/"?[^>]*rel=.?next' "$k"
grep -Eq 'href="?[^" >]*/connect/"?[^>]*rel=.?prev' "$u"
grep -q 'aria-current' "$c"
[ "$(grep -o 'id="\?TableOfContents' "$c" | wc -l)" = 0 ]
echo PASS
EOF
```
`OUT_CHECK` is a scratch path outside the repo. Run it on a build: `hugo -D --gc --minify -d "$OUT" && bash "$OUT_CHECK" "$OUT"`.
Expected: exits non-zero, because the default layout has no `rel=next`.

- [ ] **Step 2: Create `layouts/partials/docs-tree.html`**

```go-html-template
{{ $current := . }}
{{ $root := site.GetPage "/hardenedpass" }}
<ul class="space-y-4 text-sm">
  <li><a href="{{ $root.RelPermalink }}" class="font-mono text-accent">{{ $root.Title }}</a></li>
  {{ range $root.Sections.ByWeight }}
  <li>
    <a href="{{ .RelPermalink }}" class="font-mono text-accent">{{ .Title }}</a>
    <ul class="mt-2 space-y-1 border-l border-border pl-3">
      {{ range .RegularPages.ByWeight }}
      <li>
        {{ if $current.Eq . }}
        <a href="{{ .RelPermalink }}" aria-current="page" class="text-accent font-bold">{{ .Title }}</a>
        {{ else }}
        <a href="{{ .RelPermalink }}" class="text-text-dim hover:text-accent">{{ .Title }}</a>
        {{ end }}
      </li>
      {{ end }}
    </ul>
  </li>
  {{ end }}
</ul>
```

- [ ] **Step 3: Create `layouts/partials/docs-nav.html`**

The tree and table of contents render twice, so the `id` Hugo puts on the table of contents is removed to keep ids unique.

```go-html-template
{{ $toc := replace .TableOfContents ` id="TableOfContents"` "" | safeHTML }}
{{ $hasToc := gt (len .Fragments.Headings) 0 }}
<div class="lg:hidden mb-8 space-y-2">
  <details class="border border-border p-3">
    <summary class="font-mono cursor-pointer">{{ i18n "docsMenu" }}</summary>
    <div class="mt-3">{{ partial "docs-tree.html" . }}</div>
  </details>
  {{ if $hasToc }}
  <details class="border border-border p-3">
    <summary class="font-mono cursor-pointer">{{ i18n "docsOnThisPage" }}</summary>
    <div class="mt-3 text-sm text-text-dim">{{ $toc }}</div>
  </details>
  {{ end }}
</div>
<aside class="hidden lg:block lg:sticky lg:top-24 lg:self-start lg:max-h-[calc(100vh-7rem)] lg:overflow-y-auto"
       aria-label="{{ i18n "docsMenu" }}">
  {{ partial "docs-tree.html" . }}
  {{ if $hasToc }}
  <div class="mt-8 border-t border-border pt-4 text-sm text-text-dim">
    <p class="font-mono text-accent mb-2">{{ i18n "docsOnThisPage" }}</p>
    {{ $toc }}
  </div>
  {{ end }}
</aside>
```

- [ ] **Step 4: Create `layouts/partials/docs-pager.html`**

`Pages.Next` steps toward index 0, so the list is reversed to make "next" the following page in reading order.

```go-html-template
{{ $pages := (site.GetPage "/hardenedpass").RegularPagesRecursive.ByWeight.Reverse }}
{{ $prev := $pages.Prev . }}
{{ $next := $pages.Next . }}
{{ if or $prev $next }}
<nav class="flex justify-between gap-4 border-t border-border pt-6 font-mono text-sm" aria-label="{{ i18n "docsPager" }}">
  <span>{{ with $prev }}<a href="{{ .RelPermalink }}" rel="prev" class="text-accent">◄ {{ .Title }}</a>{{ end }}</span>
  <span class="text-right">{{ with $next }}<a href="{{ .RelPermalink }}" rel="next" class="text-accent">{{ .Title }} ►</a>{{ end }}</span>
</nav>
{{ end }}
```

- [ ] **Step 5: Create `layouts/hardenedpass/single.html`**

```go-html-template
{{ define "main" }}
<article class="mx-auto px-4 py-12 max-w-7xl">
  <div class="content-grid lg:grid lg:grid-cols-[16rem_minmax(0,1fr)] lg:gap-10">
    {{ partial "docs-nav.html" . }}
    <div class="min-w-0">
      {{ partial "breadcrumb.html" . }}
      <h1 class="text-4xl md:text-5xl font-bold mb-8 text-accent font-oxanium">{{ .Title }}</h1>
      <div class="prose dark:prose-invert max-w-none mb-12">
        {{ .Content }}
      </div>
      {{ partial "docs-pager.html" . }}
    </div>
  </div>
</article>
{{ end }}
```

- [ ] **Step 6: Create `layouts/hardenedpass/list.html`**

```go-html-template
{{ define "main" }}
<section class="mx-auto px-4 py-12 max-w-7xl">
  <div class="content-grid lg:grid lg:grid-cols-[16rem_minmax(0,1fr)] lg:gap-10">
    {{ partial "docs-nav.html" . }}
    <div class="min-w-0">
      {{ partial "breadcrumb.html" . }}
      <h1 class="text-4xl md:text-5xl font-bold mb-8 text-accent font-oxanium">{{ .Title }}</h1>
      {{ with .Content }}
      <div class="prose dark:prose-invert max-w-none mb-12">{{ . }}</div>
      {{ end }}
      <ul class="space-y-6">
        {{ range .Pages.ByWeight }}
        <li>
          <a href="{{ .RelPermalink }}" class="text-xl text-accent font-oxanium">{{ .Title }}</a>
          {{ with .Description }}<p class="text-text-dim">{{ . }}</p>{{ end }}
        </li>
        {{ end }}
      </ul>
    </div>
  </div>
</section>
{{ end }}
```

- [ ] **Step 7: Add the section to `layouts/index.json`**

Replace lines 2-3 with:

```go-html-template
{{- $projects := where .Site.RegularPages "Section" "slackware" -}}
{{- $docs := where .Site.RegularPages "Section" "hardenedpass" -}}
{{- $articles = ($articles | append $projects | append $docs).ByDate.Reverse -}}
```

- [ ] **Step 8: Add i18n keys**

```bash
cd "$SITE"
cat >> i18n/en.yaml <<'EOF'
docsMenu: "Documentation"
docsOnThisPage: "On this page"
docsPager: "Previous and next page"
EOF
cat >> i18n/it.yaml <<'EOF'
docsMenu: "Documentazione"
docsOnThisPage: "In questa pagina"
docsPager: "Pagina precedente e successiva"
EOF
```

- [ ] **Step 9: Rebuild CSS and run the check**

```bash
cd "$SITE" && npm run build && hugo -D --gc --minify -d "$OUT" && bash "$OUT_CHECK" "$OUT"
```
Expected: `PASS`. If the `rel=next` lines fail because prev and next are swapped, the installed Hugo differs from what Step 4 assumes: drop `.Reverse`.

- [ ] **Step 10: Check by eye**

Run `hugo server -D` and open `/hardenedpass/getting-started/concepts/` and `/it/hardenedpass/getting-started/concepts/` at 1280px and 390px width. Expected: sticky left column on desktop; two closed `<details>` on mobile; sticky column clears the site header. Fix `lg:top-24` if the header covers it.

- [ ] **Step 11: Commit theme, then content**

```bash
cd "$THEME"
git add layouts/hardenedpass layouts/partials/docs-nav.html layouts/partials/docs-tree.html layouts/partials/docs-pager.html layouts/index.json assets/css/main.min.css
git commit -m "feat: add layouts for the HardenedPass docs section"
cd "$SITE"
git add content/en/hardenedpass content/it/hardenedpass i18n themes/danix-xyz-hacker docs/superpowers/plans/2026-11-05-hardenedpass-docs.md
git commit -m "content: add HardenedPass docs skeleton as drafts"
```
Each message ends with the `Co-Authored-By` line from the session. **Checkpoint: maintainer reviews the layout.**

---

### Task 3: Emulator for screenshots

- [ ] **Step 1: Start the environment**

```bash
cd "$APP"
export ANDROID_SERIAL=emulator-5554
scripts/e2e-env.sh up
"$ANDROID_HOME/emulator/emulator" -avd aps-test -no-audio -no-boot-anim -no-snapshot-save -gpu swiftshader_indirect &
./gradlew :app:installDebug
```
Windowed: no `-no-window`. Needs the host's `libbsd`; if the emulator fails on it, stop and tell the maintainer. Follow the emulator notes in `$APP/AGENTS.md`: wait for boot and keyguard, disable animations, `settings put secure autofill_service null`, `scripts/e2e-env.sh push-keys`. The app is cloned from `ssh://git@10.0.2.2:2222/srv/git/store.git`. Unlike a real device, the file picker here shows only throwaway files, so the agent drives it.

- [ ] **Step 2: Confirm secure screens capture**

Open entry `example.org` and ask the maintainer to screenshot the emulator window. If the capture is black, secure screens stay text only and the shot lists below drop them.

- [ ] **Step 3: Set language per pass**

Capture everything in English first, then `adb shell "cmd locale set-app-locales xyz.danix.hardenedpass.debug --locales it"` and capture again.

---

### Task 4: getting-started pages

**Files:** `content/{en,it}/hardenedpass/getting-started/_index.md`, and `{concepts,install,create-store,connect}/index.md`.

For each page: read the sources listed, verify each step, write EN and IT, fill `description` so that the first 160 characters name the topic, and use `{{< ref >}}` for internal links. Plain second person, short steps. Images are page bundle files or `/uppies/2026/11/`, through the `image` shortcode with `alt` text. End each page with no recap: the pager links onward.

- [ ] **Step 1: `concepts`.** Password as an encrypted file, folder as a group, the first line is the password and later lines are metadata (`username:`, `url:`, `otpauth://`). OpenPGP public and private key and passphrase. Git as sync and history. How the three fit: `.gpg-id` names the recipients. Sources: `$APP/format/common/`, `https://www.passwordstore.org/`. Link out for depth.

- [ ] **Step 2: `install`.** From GitHub Releases: `HardenedPass_vX.Y.Z.apk` and `.sha256`. `sha256sum -c` on desktop. Certificate SHA-256 from `$APP/AGENTS.md` under Releases, checked with `apksigner verify --print-certs`. Android's unknown sources prompt. Installs next to upstream, which is not migrated. Update by installing the newer APK; the same key is required. Check the exact release asset names against `gh release view v0.2.0 --repo danixland/HardenedPass`.

- [ ] **Step 3: `create-store`.** On a desktop: `gpg --full-generate-key`, `pass init`, `pass git init`, private remote on any host, `pass git push`. Exporting the private key for the phone with `gpg --armor --export-secret-keys` and handling that file safely. Run every command in a temporary `GNUPGHOME` and `PASSWORD_STORE_DIR`, never the real ones. Check in `$APP` whether the app can generate a PGP key or create a store with no desktop, and document the local repository path that the welcome screen offers.

- [ ] **Step 4: `connect`.** Welcome screen, clone, URL forms, SSH key import or generation, password auth, host key prompt, PGP import screen, then the store list. Source: the setup screens in `$APP/app/src/main/java/app/passwordstore/ui/`, and the URL warnings in `strings.xml` (`ssh_scheme_needed_title`, `https_scheme_with_port_title`, `git_scheme_disallowed_title`).

- [ ] **Step 5: Screenshots.** Ask the maintainer for: welcome screen, repository info, SSH key screen, PGP import with recipients, store list. Both languages.

- [ ] **Step 6: Build, check and commit**

```bash
cd "$SITE" && hugo -D --gc --minify -d "$OUT" && bash "$OUT_CHECK" "$OUT"
git add content/en/hardenedpass content/it/hardenedpass static/uppies/2026/11
git commit -m "content: write HardenedPass getting-started pages"
```
Expected: `PASS`, no `REF_NOT_FOUND`. **Checkpoint: maintainer reviews, including the Italian.**

---

### Task 5: using pages

**Files:** `content/{en,it}/hardenedpass/using/_index.md`, and `{entries,otp,sync,autofill}/index.md`. Same method as Task 4.

- [ ] **Step 1: `entries`.** Browsing, search and recursive filtering, opening an entry, copy and clipboard timeout, create, edit, move, delete, the random and diceware generators. Verify each on the emulator.

- [ ] **Step 2: `otp`.** Storing an `otpauth://` line, the live code on the entry screen. The e2e store's `otp/example.com` shows it. The OTP screen never goes idle, so check it by eye.

- [ ] **Step 3: `sync`.** Pull to sync, push, automatic commit on change, rebase on pull, conflict recovery (abort rebase and push a new branch, hard reset). Sources: `$APP/app/src/main/java/app/passwordstore/util/git/`, `BreakOutOfDetached`, `StoreActions`.

- [ ] **Step 4: `autofill`.** Enabling the service, fill by chip or dropdown, the search chip, save flow, the phishing warning from `oreo_autofill_warning_publisher_dataset_title`, supported browsers from `$APP/autofill-parser/`. The emulator cannot do autofill (see `$APP/AGENTS.md`); write from the code and the 2026-11-05 tablet and Pixel runs. Firefox shows its own save prompt too: say so.

- [ ] **Step 5: Screenshots.** Ask for: entry view with test data, editor, generator sheet, OTP code, conflict dialog if reachable. Autofill shots, if wanted, come from a real device with the test page and test store only: ask the maintainer.

- [ ] **Step 6: Build, check and commit**

```bash
cd "$SITE" && hugo -D --gc --minify -d "$OUT" && bash "$OUT_CHECK" "$OUT"
git add content/en/hardenedpass content/it/hardenedpass static/uppies/2026/11
git commit -m "content: write HardenedPass using pages"
```
**Checkpoint: maintainer review.**

---

### Task 6: reference and landing pages

**Files:** `content/{en,it}/hardenedpass/reference/_index.md`, `{settings,security,troubleshooting}/index.md`, `content/{en,it}/hardenedpass/_index.md`, `static/uppies/2026/11/hardenedpass-banner.png` (from `$APP/media/banner.png`).

- [ ] **Step 1: `settings`.** Every setting, grouped as the Settings screen groups them. Start from `pref_*_title` in `$APP/app/src/main/res/values/strings.xml` and `values-it/strings.xml`, and confirm each is still shown in the Settings code: use the app's own Italian labels.

- [ ] **Step 2: `security`.** Secrets are AES-GCM encrypted under Android Keystore keys, the PGP passphrase cache and its options, biometric unlock, `FLAG_SECURE`, no telemetry, no Play Services, the exported activity check on file paths. What it does not protect from: compromised unlocked device, a malicious keyboard, clipboard readers before the timeout, git host metadata (entry names are visible as file names). Sources: `$APP/AGENTS.md`, `KeystorePreferences.kt`. Every claim must be traceable to code.

- [ ] **Step 3: `troubleshooting`.** Real errors from `strings.xml` with their cause and fix: wrong passphrase, no matching key, key without encryption subkey, host key changed, authentication failure, push rejected, detached HEAD. Source each one in the code path that raises it.

- [ ] **Step 4: Landing page.** First person, site tone: why the fork exists, GrapheneOS goal, differences from upstream from `$APP/README.md`, credit to the Android Password Store authors, links to `install`, `connect`, GitHub and the issue tracker. Banner through the `image` shortcode. "GrapheneOS" appears as a goal only, never as part of a product name.

- [ ] **Step 5: Build, check and commit**

```bash
cd "$SITE" && hugo -D --gc --minify -d "$OUT" && bash "$OUT_CHECK" "$OUT"
git add content/en/hardenedpass content/it/hardenedpass static/uppies/2026/11
git commit -m "content: write HardenedPass reference and landing pages"
```
**Checkpoint: maintainer review.**

---

### Task 7: Clean up and publish prep

- [ ] **Step 1: Stop the environment**

```bash
cd "$APP"; adb emu kill; scripts/e2e-env.sh down
```

- [ ] **Step 2: With the maintainer's approval only, flip drafts**

```bash
cd "$SITE"
grep -rl '^draft = true' content/en/hardenedpass content/it/hardenedpass | xargs sed -i 's/^draft = true/draft = false/'
```

- [ ] **Step 3: Add the menu entry**

In `hugo.toml`, for both `languages.it` and `languages.en`, change `legal` to `weight = 7` and add before it:

```toml
    [[languages.en.menus.main]]
      identifier = "hardenedpass"
      pageRef = "/hardenedpass"
      name = "hardenedpass"
      weight = 6
```
Use `languages.it.menus.main` in the Italian block.

- [ ] **Step 4: Final build without `-D`**

```bash
cd "$SITE" && hugo --gc --minify -d "$OUT" && bash "$OUT_CHECK" "$OUT"
grep -c hardenedpass "$OUT/en/index.json" "$OUT/it/index.json" 2>/dev/null || grep -rl 'hardenedpass' "$OUT" --include=index.json
```
Expected: `PASS`, and the search index lists the pages.

- [ ] **Step 5: Commit, then tell the maintainer**

```bash
git add hugo.toml content/en/hardenedpass content/it/hardenedpass
git commit -m "content: publish the HardenedPass docs section"
```
The maintainer pushes and merges `master` into `production`.

- [ ] **Step 6: After the section is live, in `APP`**

Add a `## Documentation` section to `README.md` linking `https://danix.xyz/hardenedpass/`, remove the Website item and the empty Documentation heading from `TODO.md`, and commit.