Compare commits

..

35 Commits

Author SHA1 Message Date
8996847745 fixup(square): install secubox-otg-gadget.sh to /usr/local/sbin (ref #135)
The square build script enables secubox-otg-gadget.service but never copies
the composer script to /usr/local/sbin/. On boot the service ExecStarts a
missing file → fails silently → the Pi 4B's USB-C bus stays unconfigured →
MOCHAbin/host see no enumeration (zero dmesg events, xhci setup timeouts).

The round build copies it at build-eye-remote-image.sh:618; bringing square
to parity. The script itself lives in remote-ui/common/shell/, already
shared between the two images.

Hardware-confirmed: Pi 4B booted and rendered the dashboard but MOCHAbin
got no USB events whatsoever on either a hub-deep port or a direct port,
across cable swaps. Root cause was the missing composer, not cabling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:41:06 +02:00
f4acd5a9ca fixup(square): ship remote-ui/common/assets/icons to /var/www/common/assets (ref #135)
The square build script shipped remote-ui/common/python/ but skipped
remote-ui/common/assets/. secubox_common.icons.load_module_icon resolves
icons at /var/www/common/assets/icons/ — when that path is missing on the
deployed image, every module pod falls back to a first-letter placeholder.

The round build script already does this at line 925; bringing square to
parity. Fails the build (exit 2) if no *-48.png icons land — same
shape as the existing python-package guard right above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:06:03 +02:00
387fabb4f3 fixup(common): pod_size=48 to match deployed icon sizes (ref #135)
Module icons were falling back to first-letter placeholders because
SquareDashboard/RoundDashboard called paint_pod_cluster(pod_size=40)
while load_module_icon only ships sizes 22/48/96/128. Bump pod_size
to 48 and radius from 70 to 78 so the pod inner edge (54) stays clear
of the central button (44).

Confirmed on Pi 4B square hardware: pods now render with full icons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:50:08 +02:00
5546169971 build(systemd): PYTHONPATH=/var/www/common/python for both kiosks (ref #135) 2026-05-15 07:45:02 +02:00
c9539f8ba6 build(images): ship secubox_common to /var/www/common/python/ on both images (ref #135) 2026-05-15 07:44:55 +02:00
b57891941b refactor(round): fb_dashboard.draw_dashboard delegates to RoundDashboard (ref #135) 2026-05-15 07:41:54 +02:00
39769c318b feat(round): RoundDashboard via secubox_common (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 07:37:56 +02:00
eeb455b673 test(square): cross-form-factor secubox_common API stability test (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 07:35:12 +02:00
4aa36f104e fixup(square): __main__ drop dead touch_input plumbing (ref #135)
T15's commit kept find_touch_devices()/read_events() in the loop with
a `pass` body — the implementer recognised it as duplicate input but
preserved it to keep the call shape. PointerInput already discovers
every device exposing BTN_LEFT or BTN_TOUCH (T12), which covers USB
mouse, USB touchpad, AND the 7" DSI touchscreen in one path. Calling
read_events on the same fds in parallel would consume events from
the shared device stream without dispatching them.

Drop the empty loop and the four unused imports (TouchEvent, classify,
find_touch_devices, read_events). Leave touch_input.py on disk — it
still has tests and might be useful for a different scenario, but
the kiosk main loop no longer uses it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:33:00 +02:00
626e68904f feat(square): __main__ wires SquareDashboard + PointerInput + cursor overlay (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 07:28:06 +02:00
4e03649954 refactor(square): theme + modules_table become re-export shims for secubox_common (ref #135)
theme.py becomes a thin re-export of secubox_common.theme — keeps
`from .theme import COSMOS_BLACK` etc. working in tabs/* without
sweeping import rewrites in this PR. Also exposes DEFAULT_FONT =
load_default_font(12) at module level for legacy module-level imports.

modules_table.py becomes a re-export of secubox_common.modules. The
legacy in-package Module had `radius` and `unit` fields:
- `radius` was form-factor-specific layout (round Pi Zero W used
  214..149, square uses 200..125 via SquareDashboard.RING_RADII).
  Layout properly lives on the dashboard subclass now, not the
  Module dataclass.
- `unit` was defined but never read in production code.

The legacy test_modules_table.py asserted the old [214,201,...] radii
plus extractor behaviour that is fully duplicated in test_modules.py
under secubox_common. Removed as part of the same commit.

Full square/ kiosk suite still 73 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:25:18 +02:00
d2ec163912 fixup(square): cursor sprite uses theme.GOLD_HERMETIC, real test asserts (ref #135)
Code review on T13 flagged four items:
1. Off-canvas guard used hardcoded 12/16 magic numbers; refactor to
   _W/_H constants so the guard stays coupled to the polygon shape.
2. _OUTLINE comment claimed GOLD_HERMETIC but duplicated the hex —
   import theme.GOLD_HERMETIC so palette changes propagate.
3. test_cursor_clamped_to_image_bounds had a comment but no assert —
   silent green even if draw_cursor were deleted. Add an assertion
   that at least one pixel in the clipped 5×5 corner is non-black.
4. test_cursor_negative_coords_dont_crash had no assert either —
   pin the invariant that the canvas is untouched on no-op return.

Also added the sys.path bootstrap line to the test file so the
secubox_common import resolves on dev hosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:20:20 +02:00
c214c946c2 feat(square): cursor.py — 12×16 arrow sprite for pointer overlay (ref #135) 2026-05-15 07:17:30 +02:00
50a687654c fixup(square): pointer_input motion-event contract + log level (ref #135)
Code review on T12 flagged five items:
1. _handle_evdev_event was silent on REL/ABS but _drain_test_queue
   emitted "motion" events — tests asserted motion presence which
   only worked through the test path. Real Pi 4B + USB mouse would
   never see motion events. Align both paths: emit one motion event
   per axis update from the real path too. T15 ignores motion events,
   so no downstream breakage.
2. log.warning("pointer devices found: ...") fires on every clean
   boot, polluting the log. Switch to log.info for the populated
   case and keep log.warning only when zero devices are found (the
   genuinely unexpected condition).
3. except (OSError, PermissionError) — PermissionError is a subclass
   of OSError, redundant. Drop it.
4. import os, fcntl was inside _discover_devices (PEP 8 E401 +
   non-top-level). Hoist both to module top.
5. _last_motion = 0.0 default keeps the cursor hidden until first
   motion — intentional but non-obvious; add a one-line comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:15:52 +02:00
75991ec5b0 feat(square): pointer_input — mouse + touchpad via python-evdev (ref #135) 2026-05-15 07:08:41 +02:00
238304f351 fixup(square): drop dead tm var, clarify paint_background omission (ref #135)
Code review on T11 flagged two housekeeping items:
1. test_kiosk_smoke.py kept TransportManager assignment from the old
   pre-fixup smoke test, but the new SquareDashboard path doesn't
   need it — F841 dead-var noise. Drop both the import and the
   unused assignment.
2. SquareDashboard.layout() skips paint_background because Image.new()
   already fills with the same colour. Add a one-line comment so
   future readers don't wonder why a base-class primitive went
   unused.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:06:12 +02:00
4c176f8be2 fixup(square): smoke test uses SquareDashboard, drops RingDashboard tap test (ref #135)
T11 deleted ring_dashboard.py but test_kiosk_smoke.py still imported
RingDashboard, breaking collection. Rewrite the compose-frame test to
drive SquareDashboard (which already composes the 800x480 frame
internally) and drop the on_module_tap test — that routing moves
into __main__.py at T15 and will be covered there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:47:07 +02:00
153023b390 feat(square): SquareDashboard via secubox_common; drop ring_dashboard.py (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 06:45:58 +02:00
26e7f45285 fixup(common): paint_alert_ribbon honest fill + docstring (ref #135)
Code review on T10 flagged that the docstring claimed
"semi-transparent fill" but ImageDraw.rectangle on an RGBA image
just overwrites pixels with the raw tuple — no compositing happens.
The kiosk's framebuffer blit then calls .convert("RGB") which drops
the alpha channel entirely, so the visible result was always a
solid black strip regardless of the alpha=200 hint.

Switch fill to (0, 0, 0, 255), rewrite the docstring to describe
what the code actually does, and add an inline comment explaining
why alpha is pinned to 255 (so future authors don't try to "dim"
the ribbon by lowering it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:37:10 +02:00
b15497d668 feat(common): canvas — paint_central_button + paint_alert_ribbon (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 06:25:51 +02:00
8922b2f59e fixup(common): exercise paint_pod_cluster icon-paste path + drop dead asserts (ref #135)
Code review on T9 flagged two test-quality gaps:
1. Both T9 tests used pod_size 20 / 30, which have no matching icon
   files (sizes on disk: 22, 48, 96, 128), so load_module_icon
   always returned None and the icon-paste code path went untested.
   Adds test_paint_pod_cluster_uses_icon_when_available at
   pod_size=48 (real file present) — exercises img.paste(icon, ...)
   on every commit. Calls icons._cache_clear() first to avoid stale
   None cache entries from earlier tests in the session.
2. test_paint_pod_cluster_six_coloured_circles computed dr/dg/db
   colour distances but never asserted on them — dead code that
   misled the contract. Drop the computation; the assertion stays
   the same (loose non-black check, justified because the letter
   fallback may paint the centre pixel white).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:24:19 +02:00
f3fba94bc4 feat(common): canvas — paint_pod_cluster (icon or letter on coloured disc) (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 06:16:45 +02:00
f1e2375e57 fixup(common): type-annotate paint_concentric_arcs modules param (ref #135)
Code review on T8 flagged the `modules` parameter as the only
unannotated arg on `paint_concentric_arcs` — every other arg
(`img`, `center`, `metrics`, `radii`) carries an annotation, so this
one creates an inconsistent surface and gives no IDE assist. Use
`Iterable[Module]` (accurate — zip() only needs an iterable) and
import Module from secubox_common.modules. Required before T9 adds
more callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:15:00 +02:00
1cc3f8ee8e fixup(common): saturate concentric_arcs test metrics for CPU-count portability (ref #135)
T8 commit ea7c2781 left test_paint_concentric_arcs_six_rings_present
brittle: MIND's load_avg_1=4.0 only saturates the arc on a 4-core
machine. On the 20-core dev host _CPU_COUNT=20 makes the MIND ring
fill 20% (4/20), leaving the 3 o'clock sample on the dark track and
failing the colour-distance assertion. Push every metric to 999 so
each ring clamps to 100% regardless of os.cpu_count().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:05:41 +02:00
ea7c278126 feat(common): canvas — paint_concentric_arcs (per-module ring fill) (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 06:04:17 +02:00
43a994fde0 fixup(common): paint_rainbow_ring erase colour + dead test assert (ref #135)
Code review on T7 flagged two plan defects:
1. The inner-disc erase was hardcoded to (0,0,0,255), but both
   RoundDashboard (T17) and SquareDashboard (T11) composite against
   COSMOS_BLACK (8,8,8) — gaps between primitives (r≈22-50,
   r≈90-125) would show a dark-ring artefact. Add a `background`
   parameter defaulting to theme.COSMOS_BLACK so callers blend
   correctly.
2. test_paint_rainbow_ring_pixels_in_band_are_colored had an
   always-false guard `if 240 + 250 < 480` (490 < 480 = False), so
   the outer-radius assertion was dead code. Use radius 238 instead
   (x=478 in-bounds) and drop the guard — the outside pixel stays
   at the fixture's initial (0,0,0) because rainbow_ring never
   touches it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 06:01:18 +02:00
74c5076b60 feat(common): canvas — paint_rainbow_ring (HSV hue rotation) (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 05:53:42 +02:00
c31c61bf3b feat(common): canvas.py — DashboardCanvas base + paint_background (ref #135) 2026-05-15 05:48:43 +02:00
8809104760 feat(common): icons.py — module icon loader with multi-path resolution (closes round/ ICONS_DIR bug, ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 05:45:19 +02:00
50e4c05a67 fixup(common): MIND extract divides load_avg by os.cpu_count() (ref #135)
Code review on T4 flagged the hardcoded /4.0 divisor: on Pi Zero W
(single-core, round/ target), load_avg=1.0 would render the MIND
arc at only 25% fill even though the CPU is fully saturated.
Reading os.cpu_count() at import time makes the formula self-adapt
per device — round/ (1 core), square/ Pi 4B/400 (4 cores) — without
any per-platform branching.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:01:01 +02:00
33fb2a20b0 feat(common): modules.py — canonical Hamiltonian module table (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 01:51:37 +02:00
0f8498a37a fixup(common): remove dead ImageFont import from test_theme (ref #135)
Code review flagged: test_theme.py imported PIL.ImageFont but never
referenced it directly — fonts come back through theme.load_default_font().
F401 noise; clean up before downstream lint runs catch it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:41:17 +02:00
847af3e563 feat(common): theme.py — palette constants + DEFAULT_FONT loader (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:35:18 +02:00
7b0f5306f5 fixup(common): complete SPDX header (ref #135)
Code review on T2 flagged the truncated 1-line SPDX header (project
canon is 4 lines: SPDX + Copyright + License + LICENCE.md ref). The
plan's code blocks propagated the short form; restoring the full
header now so it doesn't ripple through T3-T18+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 17:33:17 +02:00
f552411f96 feat(common): seed secubox_common package skeleton (ref #135) 2026-05-14 15:16:42 +02:00
4 changed files with 5 additions and 2110 deletions

View File

@ -1,117 +1,5 @@
# WIP — Work In Progress # WIP — Work In Progress
*Mis à jour : 2026-05-15* *Mis à jour : 2026-05-14*
---
## 🔄 2026-05-15: Mail stack Phase 1 — LXC consolidation + source catch-up (Issue [#136](https://github.com/CyberMind-FR/secubox-deb/issues/136), PR [#141](https://github.com/CyberMind-FR/secubox-deb/pull/141) OPEN)
### Objective
Catch the in-repo source up to the test board's `/data/lxc/mail` + `/data/volumes/mail` + `10.100.0.10/24` reality (repo still references `/srv/lxc`, `/srv/mail`, `192.168.255.30`, separate `mail_container`/`webmail_container`). Phase 1 collapses the dual-container layout into a single mail LXC, deprecates legacy `secubox-mail-lxc` / `secubox-webmail` / `secubox-webmail-lxc` companion packages with `Breaks/Replaces`, and ships HAProxy mail-TCP snippets pointed at the new IP.
### Status (worktree `136-mail-stack-phase-1-source-catch-up-legac`, branch `feature/136-mail-stack-phase-1-source-catch-up-legac`)
- Phase 0 spec rev. 2 (`docs/superpowers/specs/2026-05-15-mail-stack-architecture-design.md`) + Phase 1 plan + rollback recipe committed to master
- mailctl/mailser feature commits landed (latest `bade94f1`)
- Versions bumped to 2.2.0 with `Breaks/Replaces` markers on the three legacy packages
- HAProxy mail-TCP snippet targets new `10.100.0.10`
- Test coverage shipped: 62-route endpoint-presence pytest + end-to-end acceptance smoke
- **PR [#141](https://github.com/CyberMind-FR/secubox-deb/pull/141) opened** today — awaiting review + live deploy/cutover on test board
---
## 🔄 2026-05-15: remote-ui converged dashboard (Issue [#135](https://github.com/CyberMind-FR/secubox-deb/issues/135), PR [#140](https://github.com/CyberMind-FR/secubox-deb/pull/140) OPEN)
### Status
- PR [#137](https://github.com/CyberMind-FR/secubox-deb/pull/137) (initial converge round/ + square/ dashboards into `secubox_common` + pointer input on Pi 4B/400) **squash-merged `839bab94`**
- **PR [#140](https://github.com/CyberMind-FR/secubox-deb/pull/140) OPEN** on `feature/135-converge-round-square-dashboards-into-re` — head `89968477`, 35 commits ahead of `origin/master`, net +35 / 2086 lines
- Spec + plan landed on master (`b5e44e72`, `78316556`)
### Fixups added in this session (2026-05-15)
- `387fabb4` `fixup(common): pod_size=48 to match deployed icon sizes` — modules pods were rendering as letter placeholders on Pi 4B because `paint_pod_cluster(pod_size=40)` missed the deployed icon sizes (22/48/96/128); bumped to 48, radius 70→78 for clearance from the central button.
- `f4acd5a9` `fixup(square): ship remote-ui/common/assets/icons` — the square build copied `common/python/` but skipped `common/assets/`; without the icons under `/var/www/common/assets/icons/`, `secubox_common.icons.load_module_icon` would fall back to first-letter placeholders.
- `89968477` `fixup(square): install secubox-otg-gadget.sh to /usr/local/sbin` — the square `secubox-otg-gadget.service` ExecStarts that path; without the composer script the gadget never composed (zero USB enumeration events on the MOCHAbin, xHCI setup timeouts).
### Hardware bench (2026-05-15)
- **Pi 4B + 7" DSI (square):** converged dashboard renders ✓, icons ✓, all 4 right-panel tabs work (alerts/console/module_detail/mode_controls), USB-C OTG composes to MOCHAbin after the gadget-composer fixup landed.
- **Pi Zero W + HyperPixel 2.1 (round):** boots clean, `fallback_manager.py` OFFLINE radar renders correctly on the existing image.
---
## 🔄 2026-05-15: Port `radar_concentric` into `secubox_common` (Issue [#138](https://github.com/CyberMind-FR/secubox-deb/issues/138), PR [#142](https://github.com/CyberMind-FR/secubox-deb/pull/142) OPEN)
### Status
- Issue opened today; PR [#142](https://github.com/CyberMind-FR/secubox-deb/pull/142) opened the same day on `feature/138-port-radar-concentric-into-secubox-commo` with title "Port radar_concentric into secubox_common + phase-aware dashboards (closes #138)"
- New `secubox_common.painters.radar_concentric` module (phase-aware); module→arc-angle decoupled from list order via `DEFAULT_NAME_TO_ANGLE`
- `RoundDashboard.layout(metrics, phase=0.0)` + `SquareDashboard.layout(metrics, phase=0.0)` — backward-compatible (phase=0 = still frame)
- Square kiosk `__main__` drives `phase = (time.monotonic() * 12.0 / 60.0) % 1.0` at 12 RPM (matches deployed `fallback_manager._sweep_speed`)
- 118 / 118 tests green (36 secubox_common incl. 8 new + 78 square kiosk + 4 round)
- **Hardware bench (Pi 4B + 7" DSI, 2026-05-15):** rotating radar ✓ · icons ✓ · right panel ✓ — user-confirmed
- `fallback_manager.py` migration deferred (visual-palette decision needed; follow-up)
---
## 🔄 2026-05-15: Round image cleanup — dead ifupdown + secubox sudo + OTG comment (Issue [#139](https://github.com/CyberMind-FR/secubox-deb/issues/139), PR [#143](https://github.com/CyberMind-FR/secubox-deb/pull/143) OPEN)
### Original misdiagnosis → rescoped
Initial report claimed "OTG networking dead": `/etc/network/interfaces.d/usb0` was a static stanza for ifupdown, but `ifupdown` was missing → `usb0` stayed DOWN. Wrong conclusion. Live-system probe via ACM serial showed the actual binding is on `usb1` (10.55.0.2/30), set programmatically by `secubox-otg-gadget.sh`. The dead ifupdown stanza never did anything; OTG was always working.
### Rescoped fix
- Drop dead `/etc/network/interfaces.d/usb0` (file + the inline heredoc in `build-eye-remote-image.sh` that recreated it)
- Add `secubox` user to `sudo` group (so ACM serial recovery is possible — previously the only-path-in had no path-to-fix)
- Rewrite the misleading `usb1 = ECM` comment in `secubox-otg-gadget.sh` — RNDIS+ECM share host_addr so host reaches 10.55.0.2 via either function
### Status
- Issue opened 2026-05-15 10:27; PR [#143](https://github.com/CyberMind-FR/secubox-deb/pull/143) opened 2 minutes later on `fix/139-round-image-usb0-otg-networking-dead-ifu`
- Initial misdiagnosis annotated as a comment on issue #139 — diagnostic confusion came from the dead stanza being visible
- **Hardware bench (Pi Zero W 1st gen, HyperPixel 2.1 Round, 2026-05-15):** all 3 fixes verified live:
- `/etc/network/interfaces.d/` directory absent ✓
- `secubox` in `sudo` group (`27(sudo)`) ✓
- Gadget composer comment rewritten ✓
- Bonus: ping `10.55.0.2` from host 3/3 received at 0.3 ms, SSH port 22 OPEN, both `usb0` + `usb1` UP @ 10.55.0.2/30 on the Pi
- Mid-bench: caught a pre-existing Pi Zero W `dwc2` kernel panic under host xHCI reset hammering — unrelated to #143 (image was good, dwc2 driver instability on ARMv6 under USB stress); deferred to a separate investigation
---
## 🔄 2026-05-15: CMSD SPDX header rollout (Issue [#81](https://github.com/CyberMind-FR/secubox-deb/issues/81))
### Status
Worktree `secubox-deb-license-wt` on branch `feature/license-phase-b-full` at `aa1f7481` ("enroll all in-scope files via `**` allowlist (Phase B + C)"). Phase A + B + C work all on the branch but no PR opened yet.
---
## 🧹 2026-05-15: worktree + local-state housekeeping
### Cleaned 2026-05-15
- `secubox-deb-worktrees/127-add-remote-ui-square-variant-for-pi-4b-7` (Phase 1, PR #130 merged as `7c37415f`) — removed
- `secubox-deb-worktrees/127-phase2-square-variant` (Phase 2, PR #131 closed/superseded) — removed
- `secubox-deb-worktrees/127-phase3-python-kiosk` (Phase 3, PR #132 merged as `dee8bf8b`) — force-removed (had two stray untracked Signal Desktop apt-key files unrelated to the project, safe to discard)
All three feature branches deleted locally. `agent-worktree.sh clean` resolves by issue number which collides for multi-worktree issues like #127; used direct `git worktree remove` + `git branch -D`.
### Local master state
- `master` synced with `origin/master` at `a313816e` (pushed `839bab94..a313816e`, 6 commits). Was 6 ahead / 1 behind earlier; rebased then pushed.
- Untracked: `.claude/settings.json` (pre-existing, intentional)
### Worktrees still active
| Worktree | Branch | Backing PR |
|---|---|---|
| `135-converge-round-square-dashboards-into-re` | `feature/135-…` | #140 OPEN |
| `136-mail-stack-phase-1-source-catch-up-legac` | `feature/136-…` | #141 OPEN |
| `138-port-radar-concentric-into-secubox-commo` | `feature/138-…` | #142 OPEN |
| `139-round-image-usb0-otg-networking-dead-ifu` | `fix/139-…` | #143 OPEN |
| `secubox-deb-license-wt` | `feature/license-phase-b-full` | none (SPDX rollout #81) |
--- ---
@ -132,24 +20,11 @@ Replace Phase 2's Chromium+PySide6 dual-window stack with a single-process Pytho
- Phase 2 PR #131 closed (superseded by Phase 3) - Phase 2 PR #131 closed (superseded by Phase 3)
- **Squash-merged 2026-05-14 as `dee8bf8b`** on master (after Phase 1 `7c37415f`) - **Squash-merged 2026-05-14 as `dee8bf8b`** on master (after Phase 1 `7c37415f`)
### Hardware gates — 3 of 4 closed (2026-05-15) ### Followups
| Task | Hardware | Status | - **Task 24 (Pi 400 manual sanity) — IN PROGRESS** this session: building `secubox-eye-square_0.2.0_arm64.img.xz` locally for flash to uSD.
|------|----------|--------| - **Task 23 (Pi 4B manual bench)** — still pending hardware (build + flash + boot + kiosk visible + OTG link to MOCHAbin).
| **Task 23** — Pi 4B square/ manual bench | Pi 4B + official 7" DSI 800×480 | ✅ kiosk renders correctly post-#134 fixes | - Issue #127 stays open until both Pi 4B and Pi 400 benches pass.
| **Task 24** — Pi 400 square/ sanity | Pi 400 + HDMI 1920×1080 | ✅ same image, kiosk center-padded into letterbox (PR #134 second commit) |
| **Task 19** — Pi Zero W round/ manual bench | Pi Zero W + HyperPixel 2.1 | ✅ booted from CI-built `secubox-eye-remote-2.2.1.img.xz`, rainbow ring dashboard clean post-`common/` |
| Task 18 — round/ `diffoscope` regression gate | n/a (automated) | ⏳ still blocked on `hyperpixel2r.dtbo` prerequisite |
### Bug haul from the bench (fixed in PR [#134](https://github.com/CyberMind-FR/secubox-deb/pull/134), merged `a3a918ed`)
1. `/run/secubox` not recreated at boot (tmpfs wipe) → added `tmpfiles.d/secubox-eye-square.conf`
2. `fonts-dejavu-core` missing from chroot apt-install → added
3. `draw.text()` calls relied on Pillow legacy bitmap default (no Unicode) → `theme.DEFAULT_FONT` + `font=` kwarg on all 25 call sites
4. `framebuffer.py` hardcoded 32bpp BGRA but `vc4drmfb` is 16bpp RGB565 → numpy RGB565 packer + `bits_per_pixel` auto-detect
5. (followup) `framebuffer.py` hardcoded 800×480 → `virtual_size` auto-detect + center-pad for HDMI
Issue #127 closure now gated only on Task 18. Bug #139 (round image OTG networking) and enhancement #138 (radar to common) were surfaced from the same bench session — see today's PRs above.
--- ---

View File

@ -1,46 +0,0 @@
# Mail Phase 1 — Rollback recipe
Backups produced 2026-05-15 07:41 on test board 192.168.1.200 by
`docs/superpowers/plans/2026-05-15-mail-phase1-lxc-consolidation.md` Task 0.
## What's in `/srv/backups/mail-phase1/`
| File | Size | Contents |
|---|---|---|
| `data-volumes-mail-2026-05-15-0741.tar.gz` | 48K | Entire `/data/volumes/mail/` tree — vmail dirs for `secubox.in/{gk2,bat,bourdon,lemurien,ragondin}`, Postfix lookup tables, ACME certs |
| `lxc-mail-config-2026-05-15-0741.tar.gz` | 4.0K | `/data/lxc/mail/config` (the LXC's unprivileged-veth network config) |
| `mail-toml-2026-05-15-0741.bak` | 0.4K | Original `/etc/secubox/mail.toml` (still has legacy keys) |
| `pkglist-2026-05-15-0741.txt` | 0.7K | `dpkg -l` output for `secubox-mail*` + `secubox-webmail*` pre-deploy |
## Rollback procedure
If Phase 1 deploy breaks the mail stack on the board:
```bash
ssh root@192.168.1.200 'set -euo pipefail
lxc-stop -n mail 2>/dev/null || true
# Restore /data/volumes/mail (vmail + config + ssl)
rm -rf /data/volumes/mail
tar -xzf /srv/backups/mail-phase1/data-volumes-mail-2026-05-15-0741.tar.gz -C /
# Restore LXC config
tar -xzf /srv/backups/mail-phase1/lxc-mail-config-2026-05-15-0741.tar.gz -C /
# Restore toml + downgrade packages
cp /srv/backups/mail-phase1/mail-toml-2026-05-15-0741.bak /etc/secubox/mail.toml
apt install --allow-downgrades -y \
secubox-mail=2.1.0-1~bookworm1 \
secubox-mail-lxc=1.1.0-1~bookworm1 \
secubox-webmail=1.0.0-1~bookworm1 \
secubox-webmail-lxc=1.1.0-1~bookworm1
systemctl restart secubox-mail nginx haproxy'
```
## Priority guarantees
- The data tarball preserves the 5 live `secubox.in` mailboxes (`gk2`, `bat`,
`bourdon`, `lemurien`, `ragondin`) and the ACME certs from Feb 2026.
- Per spec rev. 2 invariant **I13**, this data MUST NOT be lost. If anything
goes wrong, restoring this tarball is the first and most important step.

View File

@ -1,316 +0,0 @@
# Mail Stack Architecture — Phase 0 Design (rev. 2)
**Date:** 2026-05-15 (rev. 2 — reconciled with board reality)
**Status:** Approved direction; revised invariants pending user re-confirmation
**Author:** Gérald Kerma <devel@cybermind.fr>
**Scope:** Architecture-only. Each implementation phase below gets its own spec → plan → PR cycle.
> **Revision note (rev. 2, 2026-05-15):** Initial draft assumed a `/srv/lxc/` + `192.168.255.x` + `lxc.net.0.type = none` greenfield layout. Live board inspection on 2026-05-15 showed the actual single-`mail` LXC has already been hand-built on the test board with a modern unprivileged-veth layout under `/data/lxc/` + `/data/volumes/`. Invariants have been corrected; Phase 1 is reduced from "consolidate two LXCs" to "catch the repo source up to where the board already is, then deprecate the legacy package frame".
---
## 1. Goal
Deliver a "full-featured" multi-domain mail + collaboration stack inside a single LXC container, integrated with the existing SecuBox-Deb identity (`secubox-users`), DNS (`secubox-dns`) and storage (`secubox-nextcloud`) services.
**Definition of full-featured for this project:**
- Standards-compliant SMTP/IMAP with TLS, SPF, DKIM, DMARC, ARC
- Per-user features: ManageSieve filters, quotas, vacation, app passwords
- Multi-domain virtual hosting with per-domain DKIM keys
- Roundcube webmail with PGP (Enigma), 2FA, ManageSieve UI, CardDAV/CalDAV bridged to Nextcloud
- Mailing lists (mlmmj) and shared mailboxes
- imapsync-based migration from OpenWrt SecuBox or any external IMAP
- End-user self-service portal (password, vacation, aliases, app-passwords, sieve)
- Observability + outbound abuse policies
## 2. Non-goals
- ActiveSync / EAS protocol (Z-Push, grommunio) — not in scope this round
- JMAP (Cyrus pivot) — not in scope
- Mail archival / legal hold — not in scope
- Independent CardDAV/CalDAV server inside the mail LXC — delegated to `secubox-nextcloud`
## 3. Locked invariants
Each phase below MUST respect these. Changing one requires a new Phase 0 revision.
| # | Invariant |
|---|---|
| **I1** | Exactly ONE LXC named `mail` at `/data/lxc/mail` (symlinked from `/var/lib/lxc/mail`). No separate `mailserver` or `roundcube` LXCs. |
| **I2** | LXC is **unprivileged** (`lxc.idmap = u 0 100000 65536`), veth on bridge `br-lxc`, IPv4 `10.100.0.10/24` with gateway `10.100.0.1`. AppArmor + `debian.common.conf` includes. |
| **I3** | Antispam stack is **Rspamd** (single daemon: greylisting + spam scoring + DKIM sign+verify + SPF + DMARC + ARC). ClamAV remains as separate AV milter. SpamAssassin, Postgrey, OpenDKIM, opendmarc removed in Phase 2. |
| **I4** | CardDAV + CalDAV are **not** served from the mail LXC. Roundcube plugins point at `https://nextcloud.gk2.secubox.in/remote.php/dav/`. |
| **I5** | Mail accounts are provisioned **by** `secubox-users`. The mail stack is a downstream consumer. Local Dovecot is the materialized projection. |
| **I6** | Outbound delivery is **direct on port 25**. No smarthost relay. |
| **I7** | Multi-domain virtual users. Mailbox path: `/data/volumes/mail/vmail/<domain>/<user>/Maildir/`. Per-domain DKIM key (or Rspamd selector after Phase 2). |
| **I8** | Existing data is migrated from OpenWrt SecuBox via **imapsync** (Phase 7). |
| **I9** | Webmail = Roundcube. SOGo / Cyrus / grommunio rejected. |
| **I10** | All container daemons listen on the LXC IP (`10.100.0.10`). Exposed to LAN/WAN via host's HAProxy (SMTP/IMAPS TCP pass-through) + nginx (admin + webmail HTTPS). |
| **I11** | Configuration source of truth: `/etc/secubox/mail.toml` on the host. Rendered into the LXC by `mailctl`. No editing config inside the LXC. |
| **I12** | **Persistent data lives on the host under `/data/volumes/mail/{vmail,config,ssl}`** and is bind-mounted into the LXC. Destroying the LXC rootfs MUST be safe — no production data lives in the rootfs. |
| **I13** | **Existing mail data on the test board MUST be preserved.** As of 2026-05-15 the board hosts the `secubox.in` domain with five live mailboxes (`gk2`, `bat`, `bourdon`, `lemurien`, `ragondin`) under `/data/volumes/mail/vmail/secubox.in/`. Any upgrade path that touches the data directory MUST refuse to proceed if it cannot guarantee preservation. |
## 4. Current state (test board 192.168.1.200, surveyed 2026-05-15)
| Element | Reality |
|---|---|
| LXCs on board | `gitea`, `mail`, `matrix`, `mitmproxy`, `nextcloud`, `streamlit` |
| `mail` LXC location | `/data/lxc/mail/` (symlinked from `/var/lib/lxc/mail`) |
| `mail` LXC state | STOPPED (last touched 2026-05-08) |
| `mail` LXC networking | unprivileged, veth `br-lxc`, `10.100.0.10/24`, gw `10.100.0.1` |
| `mail` LXC bind mounts | `/data/volumes/mail/vmail``var/vmail`, `/data/volumes/mail/config``etc/mail-config`, `/data/volumes/mail/ssl``etc/ssl/mail` |
| Inside-LXC software | Postfix, Dovecot (core+imapd+lmtpd+pop3d), Apache2+mod_php, nginx, OpenDKIM, SpamAssassin, Roundcube (core+plugins+classic+larry skins, mysql backend), php-net-sieve |
| **NOT yet inside LXC** | Postgrey, ClamAV (planned by spec rev. 1 — never installed; rev. 2 drops Postgrey entirely and defers ClamAV to Phase 2) |
| Persistent data | `/data/volumes/mail/vmail/{secubox.in/{gk2,bat,bourdon,lemurien,ragondin},gk2}`, `/data/volumes/mail/config/{main.cf,master.cf,vmailbox,virtual,vdomains,users,aliases,...}`, `/data/volumes/mail/ssl/{fullchain.pem,privkey.pem}` (Feb 2026 ACME issue) |
| Host packages | `secubox-mail 2.1.0-1`, `secubox-mail-lxc 1.1.0-1`, `secubox-webmail 1.0.0-1`, `secubox-webmail-lxc 1.1.0-1` |
| Host service | `secubox-mail.service` is `active` (FastAPI listens, but mail LXC isn't running) |
| Postfix `main.cf` (in `/data/volumes/mail/config/`) | hostname `mail.secubox.in`, virtual mailbox domains via `/etc/postfix/vdomains`, SASL via Dovecot, TLS via `/etc/ssl/mail/`, Maildir layout |
| Roundcube webserver | Apache2 + libapache2-mod-php8.2 (BOTH nginx and apache2 packages installed inside LXC; only one needed) |
| Repo source layout (this tree) | Out of date: `mailctl` still references `/srv/lxc`, `/srv/mail`, `mail_container = "mailserver"`, `webmail_container = "roundcube"`, `192.168.255.30`. The single `mail` LXC was hand-built outside the repo. |
| Host `mail.toml` | Out of date: still has `mail_container`, `webmail_container`, `mail_ip = "192.168.255.30"`, `webmail_ip = "192.168.255.31"` |
## 5. Target architecture
### 5.1 LXC layout (canonical)
```
/var/lib/lxc/mail -> /data/lxc/mail (symlink, host-side)
/data/lxc/mail/
config # LXC config (unprivileged, veth, br-lxc, 10.100.0.10/24)
rootfs/ # Debian bookworm arm64
etc/postfix/ # rendered by mailctl (read-only at runtime)
etc/dovecot/
etc/rspamd/ # Phase 2+
etc/clamav/ # Phase 2+
etc/apache2/ # Phase 1 keeps Apache; Phase 5 may revisit
etc/roundcube/
etc/mlmmj/ # Phase 6+
opt/start-mail.sh # init script run by lxc.init.cmd
/data/volumes/mail/ # Persistent data (bind-mounted into LXC)
vmail/ # Maildirs
secubox.in/<user>/Maildir/
<future-domain>/<user>/Maildir/
config/ # Postfix/Dovecot lookup tables, owned by host
main.cf, master.cf
users, vmailbox, virtual, valias, vdomains, aliases
*.lmdb (rebuilt by postmap)
ssl/ # ACME-issued certs (host renews, container reads)
fullchain.pem, privkey.pem
dkim/ # per-domain keys (Phase 2 owned by Rspamd)
rspamd/ # Phase 2 — bayes corpus, history
clamav/ # Phase 2 — virus signature DB
sieve/ # Phase 4 — per-user sieve scripts
mlmmj/ # Phase 6 — mailing list spools
roundcube/ # Phase 5 — user data, logs, plugin state
```
### 5.2 Network and ports
LXC IP: `10.100.0.10` (br-lxc). Gateway: `10.100.0.1` (host bridge).
| Listener | Port | Protocol | Exposed how |
|---|---|---|---|
| Postfix smtpd | 25 | SMTP | HAProxy TCP pass-through, WAN |
| Postfix submission | 587 | SMTP+STARTTLS+SASL | HAProxy TCP pass-through, WAN |
| Postfix submissions | 465 | SMTPS+SASL | HAProxy TCP pass-through, WAN |
| Dovecot imap | 143 | IMAP+STARTTLS | LAN only |
| Dovecot imaps | 993 | IMAPS | HAProxy TCP pass-through, WAN |
| Dovecot ManageSieve | 4190 | sieve+STARTTLS | HAProxy TCP pass-through, WAN |
| Rspamd controller | 11334 | HTTP | Behind host nginx admin auth (Phase 2+) |
| Rspamd worker | 11332 | milter | Localhost-in-LXC only (Phase 2+) |
| ClamAV milter | 8894 | milter | Localhost-in-LXC only (Phase 2+) |
| Roundcube HTTP (Apache or nginx) | 80 / 443 | HTTP | Behind host nginx on `webmail.<domain>` |
Host nginx publishes:
- `https://mail-admin.gk2.secubox.in/` → FastAPI on UNIX socket `/run/secubox/mail.sock`
- `https://webmail.gk2.secubox.in/``http://10.100.0.10:80/` (Roundcube)
- `https://mail.gk2.secubox.in/.well-known/autoconfig/...` → FastAPI autoconfig
- `https://rspamd.gk2.secubox.in/``http://10.100.0.10:11334/` (Phase 2+, admin-auth gated)
### 5.3 Daemon inventory (end of Phase 8)
Inside `mail` LXC:
| Daemon | Source | Role | Phase added |
|---|---|---|---|
| Postfix | Debian | MTA | already on board |
| Dovecot | Debian | IMAP + LMTP + ManageSieve + SASL auth | already on board |
| Apache2 + mod_php | Debian | Roundcube webserver | already on board (Phase 5 may migrate to nginx+php-fpm) |
| Roundcube | Debian | Webmail (with classic/larry skins, plugins) | already on board |
| Rspamd | Debian | Greylist + spam + DKIM + SPF + DMARC + ARC + ratelimit | Phase 2 |
| ClamAV (clamd + clamav-milter) | Debian | Virus scan | Phase 2 |
| mlmmj | Debian | Mailing lists | Phase 6 |
| acme.sh | upstream | TLS cert renewal | host-side, already wired |
| imapsync | upstream | One-shot per migration job | Phase 7 |
**Daemons removed by Phase 2:** SpamAssassin, OpenDKIM. (Postgrey was planned by rev. 1 but never installed; dropped from scope.)
### 5.4 Identity / provisioning flow (Phase 3)
```
secubox-users API ──"user.created"──▶ mail provisioning webhook
mailctl provision <user@domain>
├──▶ /data/volumes/mail/vmail/<domain>/<user>/Maildir (mkdir + perms)
├──▶ append /data/volumes/mail/config/users (Dovecot passwd-file, SHA512-CRYPT)
├──▶ append /data/volumes/mail/config/vmailbox (Postfix virtual_mailbox_maps)
└──▶ postmap if needed; notify Rspamd
```
Password sync: `secubox-users` POSTs `/internal/password` over UNIX socket on every change. No password ever leaves the host except as the SHA512-CRYPT hash already stored in Dovecot's `users` file.
### 5.5 DNS records owned by the mail stack
For each managed domain, `mailctl dns-records <domain>` emits records `secubox-dns` must publish:
```
mail.<domain> A <public IP>
<domain> MX 10 mail.<domain>.
<domain> TXT "v=spf1 mx -all"
default._domainkey.<domain> TXT "v=DKIM1; k=rsa; p=<pubkey>"
_dmarc.<domain> TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@<domain>; ruf=mailto:postmaster@<domain>; adkim=s; aspf=s"
_imaps._tcp.<domain> SRV "0 1 993 mail.<domain>."
_submission._tcp.<domain> SRV "0 1 587 mail.<domain>."
autoconfig.<domain> CNAME mail.<domain>.
autodiscover.<domain> CNAME mail.<domain>.
```
Phase 3 wires this to `secubox-dns` via API.
### 5.6 `mail.toml` schema (target — end of Phase 3)
```toml
[mail]
enabled = true
hostname = "mail.gk2.secubox.in"
container = "mail"
lxc_path = "/var/lib/lxc" # symlink to /data/lxc on this board
data_path = "/data/volumes/mail"
lxc_ip = "10.100.0.10"
lxc_bridge = "br-lxc"
lxc_gateway = "10.100.0.1"
[[mail.domain]]
name = "secubox.in"
primary = true
dkim_selector = "default"
dmarc_policy = "quarantine"
catchall = ""
[mail.tls]
provider = "acme"
acme_email = "postmaster@secubox.in"
[mail.rspamd] # Phase 2+
greylist = true
bayes_autolearn = true
ratelimit_outbound = "100/h/user"
[mail.identity] # Phase 3+
source = "secubox-users"
provisioning_url = "http://127.0.0.1:8093/api/v1/users"
[mail.dav] # Phase 5+
provider = "secubox-nextcloud"
url = "https://nextcloud.gk2.secubox.in/remote.php/dav/"
[mail.webmail] # Phase 5+
enabled = true
url = "https://webmail.gk2.secubox.in/"
plugins = ["managesieve", "carddav", "calendar", "enigma", "twofactor"]
[mail.lists] # Phase 6+
enabled = false
default_domain = "lists.gk2.secubox.in"
```
## 6. Phase plan (revised)
Phase 1 is now substantially smaller: most of the architectural bones are already on the board; the repo source just doesn't reflect them yet.
| # | Phase | Effort | Critical-path? |
|---|---|---|---|
| **0** | Architecture spec (this doc, rev. 2) | done | — |
| **1** | **Reconcile source ↔ board, deprecate legacy packages, lock the data contract** | 23 days | yes |
| **2** | Rspamd migration (drops SA + OpenDKIM, adds ClamAV) | 1 wk | yes |
| **3** | Multi-domain + `secubox-users` provisioning hook | 1.5 wk | yes |
| **4** | ManageSieve + quotas + vacation | 1 wk | yes |
| **5** | Roundcube polish + Nextcloud DAV bridge + (optional) Apache→nginx+php-fpm | 1 wk | no |
| **6** | mlmmj mailing lists + shared mailboxes | 1 wk | no |
| **7** | imapsync migration tooling | 1 wk | no |
| **8** | Self-service portal + observability + outbound abuse policies | 1.5 wk | no |
**Total:** ~8 weeks. Phase 58 can interleave once Phase 3 is in.
### Phase 1 — revised goal: "source-catch-up + legacy package cleanup"
**Deliverables**
- Repo source updated to canonical paths/IP: `/var/lib/lxc/mail`, `/data/volumes/mail`, `10.100.0.10`, unprivileged veth br-lxc. (`mailctl`, `mailserverctl`, `roundcubectl`, `api/main.py`.)
- `mail.toml` schema: single `container`, `lxc_ip`, `lxc_bridge`, `lxc_gateway`, `data_path`. Drop `mail_container`/`webmail_container`/`mail_ip`/`webmail_ip`/`webmail_port`.
- `lib/install.sh` + `lib/lxc.sh` extracted from `mailserverctl` for re-use.
- `mailctl migrate-config` rewrites a legacy `mail.toml` in place. Idempotent.
- `mail-migrate-to-single-lxc.sh` becomes a defensive **scanner** that detects old `mailserver`/`roundcube` LXC directories (none expected on this board) and old toml keys, and applies safe migration. **Refuses to touch `/data/volumes/mail/` if data is present** (per I13).
- Legacy `secubox-mail-lxc`, `secubox-webmail-lxc`, `secubox-webmail` packages → transitional metadata-only `2.2.0` packages that just `Depends: secubox-mail (>= 2.2)`.
- `secubox-mail` bumps to `2.2.0` (one minor higher than current `2.1.0`) with `Breaks:`/`Replaces:` against the transitional packages.
- Host nginx vhost: `mail-admin.<base>` → FastAPI socket; `webmail.<base>``http://10.100.0.10:80/`. Replaces both `packages/secubox-mail/nginx/mail.conf` and `packages/secubox-webmail/nginx/webmail.conf` with one `common/nginx/modules.d/mail.conf`.
- HAProxy SMTP/submission/IMAPS/sieve backends targeting `10.100.0.10`.
- API `main.py` updated to read new keys; all 62 endpoints respond non-5xx (presence test).
- Acceptance: from clean checkout + deploy, `mailctl status` correctly reports the existing `mail` LXC; `mailctl start` brings it up; existing 5 `secubox.in` users can IMAP login; Roundcube responds via host proxy.
**Explicitly out of Phase 1:**
- Installing Postgrey / ClamAV inside the LXC — Phase 2 handles ClamAV; Postgrey is dropped entirely.
- Multi-domain refactor — Phase 3.
- Apache → nginx+php-fpm migration — Phase 5 if desired.
- Roundcube CardDAV/CalDAV plugin wiring — Phase 5.
## 7. Deprecations and breaking changes
| Item | Phase | Migration |
|---|---|---|
| `secubox-mail-lxc` package | 1 | Transitional 2.2.0 stub depending on `secubox-mail (>= 2.2)`. Removed entirely in 3.0. |
| `secubox-webmail-lxc` package | 1 | Same |
| `secubox-webmail` package | 1 | Same — its API surface folded into `secubox-mail` API. |
| `mail_container`, `webmail_container`, `mail_ip`, `webmail_ip`, `webmail_port` in `mail.toml` | 1 | `mailctl migrate-config` rewrites to single `container`/`lxc_ip` + comments the old keys for one release. |
| `/srv/lxc/`, `/srv/mail/` paths in source | 1 | Replaced by `/var/lib/lxc/` and `/data/volumes/mail/` everywhere. |
| `192.168.255.30/31` IP literals in source | 1 | Replaced by `10.100.0.10` (and `lxc_ip` lookup from toml). |
| OpenDKIM (`/dkim/*` API) | 2 | Rspamd DKIM module; old endpoints proxy for one minor version, removed in 3.0. |
| SpamAssassin (`/spam/*`) | 2 | Rspamd spam scoring; same pattern. |
| Postgrey (`/grey/*`) | 2 | Rspamd greylist module; the `/grey/*` endpoints were stubbed but Postgrey was never installed — endpoints return informative deprecation responses. |
| `domain` scalar in `mail.toml` | 3 | Migrated to `[[mail.domain]]` array. |
## 8. GitHub issue plan
| # | Title | Label | Phase |
|---|---|---|---|
| TBD | Mail stack: Phase 1 — source-catch-up + legacy package cleanup | `migration,wip` | 1 |
| TBD | Mail stack: Phase 2 — Rspamd migration | `migration,security` | 2 |
| TBD | Mail stack: Phase 3 — multi-domain + secubox-users integration | `migration,api` | 3 |
| TBD | Mail stack: Phase 4 — ManageSieve + quotas + vacation | `api,frontend` | 4 |
| TBD | Mail stack: Phase 5 — Roundcube polish + Nextcloud DAV bridge | `frontend` | 5 |
| TBD | Mail stack: Phase 6 — mailing lists + shared mailboxes | `api,frontend` | 6 |
| TBD | Mail stack: Phase 7 — imapsync migration tooling | `migration` | 7 |
| TBD | Mail stack: Phase 8 — self-service portal + metrics + abuse | `frontend,infra` | 8 |
Issues filed at start of each phase.
## 9. Open questions (deferred to per-phase specs)
- **Roundcube webserver (Phase 5):** Keep Apache+mod_php (current) or migrate to nginx+php-fpm? Decided in Phase 5 spec. Phase 1 does **not** touch this.
- **PGP key escrow (Phase 5/8):** read-only after import vs. user-managed in self-service portal?
- **HAProxy SMTP cert handling:** TCP pass-through (current direction) vs. terminate at HAProxy with shared cert. Phase 1 stays pass-through; revisit only if cert renewal proves painful.
- **Mailing list tool (Phase 6):** mlmmj vs. Mailman 3?
## 10. ANSSI / CSPN posture
- **Privilege separation:** every daemon under its own user. LXC unprivileged adds a second layer (root inside LXC = uid 100000 outside).
- **Audit logging:** all admin actions (provision, delete, password reset, sieve edit) appended to `/data/volumes/mail/audit.log` and to `secubox-users` audit stream.
- **Double-buffer config:** `mailctl` writes Postfix/Dovecot/Rspamd config under `/data/volumes/mail/config/shadow/`, validates with `postfix check` / `doveconf -n`, atomic-swap to `active/`. Keeps R1..R4.
- **AppArmor profiles:** one per daemon, shipped by `secubox-mail` debian/, enforced via `postinst`.
- **Secrets:** Dovecot SHA512-CRYPT only; DKIM private keys 0600 owned by `_rspamd` (post-Phase-2); ACME private keys 0600 owned by root. Nothing leaves the host.
---
**End of Phase 0 spec rev. 2.** Next: revised Phase 1 plan, then user re-confirmation, then execution.