Compare commits

..

12 Commits

Author SHA1 Message Date
7fafdd9d7c 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 13:24:40 +02:00
8e3b62efc3 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 13:24:40 +02:00
92c5b1f90f 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 13:24:40 +02:00
c39743726a docs(wip): expand #135/PR #140 with this session's fixups + hardware bench
- List the three fixup commits landed on feature/135-* today (pod_size,
  icon assets, gadget composer) so the WIP captures what's actually in
  the PR head right now (89968477, 35 commits ahead, net +35 / -2086).
- Add hardware-bench notes for both round (Pi Zero W) and square (Pi 4B
  + DSI) covering the converged work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:22:04 +02:00
5c72d869a4 docs(wip): housekeeping — 3 stale 127-* worktrees cleaned, master pushed
- Removed feature/127-add-remote-ui-square-variant-for-pi-4b-7 (PR #130 merged)
- Removed feature/127-phase2-square-variant (PR #131 closed, superseded)
- Force-removed feature/127-phase3-python-kiosk (PR #132 merged) — stray Signal
  Desktop apt-key files in the worktree were unrelated to the project, safe to
  discard
- Master synced with origin at a313816e (pushed 839bab94..a313816e)
- Updated section with the table of still-active worktrees for at-a-glance view

Note: agent-worktree.sh clean resolves by issue number which collides for
multi-worktree issues like #127 — used direct git worktree remove + branch -D
instead. Possible enhancement for the script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:19:44 +02:00
a313816edc docs(wip): hardware bench results for #138/#139 + rescope of #139 (closes nothing)
- #138 (PR #142, radar_concentric): mark hardware-validated on Pi 4B+DSI
  (rotating radar + icons + right panel) on 2026-05-15.
- #139 (PR #143, round image cleanup): rescope from "OTG dead" to
  dead-ifupdown / sudo / misleading-comment cleanup; OTG was always
  working on usb1 via secubox-otg-gadget.sh. Bench validated all 3 fixes
  live (Pi Zero W ARMv6 + HyperPixel) including host ping 3/3 at 0.3 ms,
  SSH OPEN, secubox in sudo group.
- Note the orthogonal pre-existing Pi Zero W dwc2 panic under xHCI reset
  hammering, deferred for a separate investigation.

Conflict between local stash and divergent master resolved by taking the
HEAD side; stashed simpler version dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
9c6e11d54d docs: WIP snapshot 2026-05-15 — mail #136 PR #141, dashboard #135 PR #140, #138 PR #142, #139 PR #143
- Phase 3 (#127): 3 of 4 hardware gates closed (Pi 4B / Pi 400 / Pi Zero W);
  only Task 18 diffoscope regression remaining. 5 bench bugs fixed in PR #134.
- Mail stack Phase 1 (#136): mailctl/mailser + 62-route pytest + acceptance
  smoke landed; PR #141 opened today.
- Converged dashboard (#135): PR #137 merged (839bab94); PR #140 follow-on
  opened today.
- New: #138 radar_concentric → secubox_common (PR #142), #139 round usb0 OTG
  networking bug (PR #143).
- CMSD SPDX (#81): worktree at aa1f7481, no PR yet.
- Note 3 stale 127-* worktrees safe to clean; master 4↑/1↓ vs origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
3770586a31 docs: Phase 1 rollback recipe (pre-issue) 2026-05-15 13:16:16 +02:00
bd4ef55a98 docs: revise mail Phase 0 spec + Phase 1 plan to match board reality
Live board inspection on 2026-05-15 found that the single-mail-LXC layout
has already been hand-built under /data/lxc/mail with /data/volumes/mail
bind-mounts and 10.100.0.10/24 unprivileged-veth networking on br-lxc.
The repo source code was out of date (still referenced /srv/lxc, /srv/mail,
192.168.255.x, mail_container/webmail_container).

Spec rev. 2:
- Invariants now reflect /var/lib/lxc/mail, /data/volumes/mail, 10.100.0.10
- New invariant I12: persistent data lives on host bind-mounts only
- New invariant I13: existing 5-user secubox.in mailboxes MUST be preserved
- Postgrey dropped entirely; ClamAV deferred to Phase 2 with Rspamd

Plan rev. 2:
- Reduced from ~30 tasks to ~15 — Phase 1 is now "catch source up to board"
- Migration script becomes defensive scanner (no destructive ops)
- guard_data_path bats test enforces the data-preservation invariant
- secubox-mail bumps 2.1.0 → 2.2.0 (was incorrectly targeting 2.0.0 in rev. 1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
b59d053c54 docs: Phase 1 implementation plan (mail stack LXC consolidation)
Bite-sized TDD plan covering 8 milestones (~30 tasks) to collapse the
existing mailserver + roundcube two-LXC layout into a single 'mail' LXC.
Resolves spec open questions: Roundcube via nginx+php-fpm; HAProxy
TCP-pass-through to LXC for 25/587/993/4190. Includes data migration
helper, deprecation of 3 packages to transitional, and end-to-end
acceptance smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
2494b55a18 docs: Phase 0 architecture spec for full-featured mail+webmail stack
8-phase plan to deliver multi-domain Postfix+Dovecot+Rspamd+ClamAV+Roundcube
inside a single 'mail' LXC, integrated with secubox-users (identity) and
secubox-nextcloud (CardDAV/CalDAV). Locks 10 architectural invariants,
deprecates secubox-mail-lxc + secubox-webmail-lxc + secubox-webmail, and
records the deferred per-phase design questions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
CyberMind
839bab94a8
Converge round/ + square/ dashboards into secubox_common + pointer input on Pi 4B/400 (#137)
Some checks failed
License Headers / check (push) Has been cancelled
* feat(common): seed secubox_common package skeleton (ref #135)

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

* feat(common): theme.py — palette constants + DEFAULT_FONT loader (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

* feat(common): modules.py — canonical Hamiltonian module table (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

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

* feat(common): canvas.py — DashboardCanvas base + paint_background (ref #135)

* feat(common): canvas — paint_rainbow_ring (HSV hue rotation) (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

* feat(common): canvas — paint_concentric_arcs (per-module ring fill) (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

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

* feat(common): canvas — paint_pod_cluster (icon or letter on coloured disc) (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

* feat(common): canvas — paint_central_button + paint_alert_ribbon (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

* feat(square): SquareDashboard via secubox_common; drop ring_dashboard.py (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

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

* feat(square): pointer_input — mouse + touchpad via python-evdev (ref #135)

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

* feat(square): cursor.py — 12×16 arrow sprite for pointer overlay (ref #135)

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

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

* feat(square): __main__ wires SquareDashboard + PointerInput + cursor overlay (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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

* test(square): cross-form-factor secubox_common API stability test (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(round): RoundDashboard via secubox_common (ref #135)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(round): fb_dashboard.draw_dashboard delegates to RoundDashboard (ref #135)

* build(images): ship secubox_common to /var/www/common/python/ on both images (ref #135)

* build(systemd): PYTHONPATH=/var/www/common/python for both kiosks (ref #135)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:49:24 +02:00
4 changed files with 2110 additions and 5 deletions

View File

@ -1,5 +1,117 @@
# WIP — Work In Progress
*Mis à jour : 2026-05-14*
*Mis à jour : 2026-05-15*
---
## 🔄 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) |
---
@ -20,11 +132,24 @@ Replace Phase 2's Chromium+PySide6 dual-window stack with a single-process Pytho
- Phase 2 PR #131 closed (superseded by Phase 3)
- **Squash-merged 2026-05-14 as `dee8bf8b`** on master (after Phase 1 `7c37415f`)
### Followups
### Hardware gates — 3 of 4 closed (2026-05-15)
- **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).
- Issue #127 stays open until both Pi 4B and Pi 400 benches pass.
| Task | Hardware | Status |
|------|----------|--------|
| **Task 23** — Pi 4B square/ manual bench | Pi 4B + official 7" DSI 800×480 | ✅ kiosk renders correctly post-#134 fixes |
| **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.
---

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
# 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

@ -0,0 +1,316 @@
# 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.