Commit Graph

182 Commits

Author SHA1 Message Date
CyberMind
2072c0eae5
fix(eye-square): relocate firstboot.sh out of /usr/local/sbin/ (closes #207) (#208)
Debian policy reserves /usr/local/ for the local administrator;
packages may not install files there. dh_usrlocal was correctly
rejecting the staged tree built from remote-ui/square/files/.

Move:
  remote-ui/square/files/usr/local/sbin/firstboot.sh
  → remote-ui/square/files/usr/lib/secubox/firstboot.sh

Update ExecStart in secubox-firstboot.service to match. The new
location is namespaced under /usr/lib/secubox/ consistent with
the sister helpers find-usb-serial and leasewatch.sh shipped by
secubox-system.

Bump 1.0.2 → 1.0.3. Local dpkg-buildpackage now produces a valid
.deb; `dpkg-deb -c` confirms:
  - /usr/lib/secubox/firstboot.sh (0755)
  - /etc/systemd/system/secubox-firstboot.service (with new ExecStart)
  - no entries under /usr/local/

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 06:53:31 +02:00
CyberMind
5879dce63a
fix(round): real glyphs for the 6 root-menu icons (#172)
* feat(round): real glyphs for the 6 root-menu icons

The radial dashboard's ROOT menu (DEVICES, SECUBOX, LOCAL, NETWORK,
SECURITY, EXIT) was rendering with the letter-in-circle placeholders
emitted by assets/icons/generate_menu_icons.py — never replaced with
actual designs, so each slice on the Pi screen showed a coloured disk
with a single capital letter (D/S/L/N/S/E) over the slice label.

Replace those 12 PNGs (22 + 48 each) with proper PIL-drawn vector
glyphs (white #e8e6d9, transparent background, 4x supersampled AA):

  devices   — tablet + phone overlay with home dot
  secubox   — isometric cube outline (brand cube)
  local     — house silhouette with door
  network   — globe with meridians
  security  — shield with embedded checkmark
  exit      — door frame + arrow pointing out

New generator script assets/icons/generate_root_icons.py is committed
alongside the PNGs so the design can be tweaked and re-emitted; the
existing generate_menu_icons.py still owns the remaining ~40
sub-menu icons (which remain placeholders for now — a separate
follow-up will design those properly).

* fix(round): use common/ brand-icon glyphs (recolored to text-primary) for the 6 root menu slices

Replaces the letter-in-circle placeholders shipped for DEVICES, SECUBOX,
LOCAL, NETWORK, SECURITY, EXIT with the corresponding SecuBox brand
icons from remote-ui/common/assets/icons/ — the same glyphs the fallback
connected dashboard already uses on its rotating cube. To keep contrast
on every slice color, the brand-icon alpha mask is preserved but the
fill is normalized to text-primary (#e8e6d9), matching the existing
label color in radial_renderer.

Mapping mirrors the fallback dashboard's cube order so the menu reads
as a logical continuation of the connected state:

  DEVICES  → auth   (CPU)
  SECUBOX  → wall   (Memory / Firewall)
  LOCAL    → boot   (Disk)
  NETWORK  → mind   (Load / Brain)
  SECURITY → root   (Temp / Root)
  EXIT     → mesh   (Network)

Drops the hand-drawn glyph generator (generate_root_icons.py) from the
previous commit — superseded by this approach.

* fix(round): fallback_manager finds the 6 brand icons (auth/wall/boot/mind/root/mesh)

The build script installs remote-ui/common/ at /var/www/common/ so the
real brand icons (auth-48.png … mesh-48.png) land at
/var/www/common/assets/icons/ on the image. fallback_manager.py however
only searched /usr/lib/secubox-eye/assets/icons/ (which holds the
round/-side menu placeholders) and so couldn't load any of the six
canonical module icons — the fallback radar's central icon stack came
out either blank or substituted with a placeholder.

Two-pronged fix:

1. fallback_manager.ICON_PATHS gains /var/www/common/assets/icons as the
   third lookup path, before the local round/assets/icons fallback. First
   existing file wins per icon name; the round-side dir keeps owning any
   icon name not present in common/.

2. build-eye-remote-image.sh now also copies common/assets/icons/*.png
   into /usr/lib/secubox-eye/assets/icons/ (with cp -n so the round/
   placeholders aren't overwritten when names collide — none do today
   anyway). Pure defense-in-depth so any consumer resolving icons via
   either of the two canonical paths finds them.

Issue reported: on the rpiz boot, the offline fallback radar showed
five of the six module icons correctly but one slot was wrong/empty.

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-18 12:14:37 +02:00
8905228cbd fix(round): restore ICON_PATHS /var/www/common + defense-in-depth icon copy
Some checks failed
License Headers / check (push) Failing after 6s
These two edits were on fix/round-real-root-icons (commit a5b312f7) but
got wiped by my earlier force-push to fix/round-armv6-boot-services.
Re-applying identically so v4 picks them up.

fallback_manager.py: add /var/www/common/assets/icons as the third
lookup path so the 6 brand icons (auth/wall/boot/mind/root/mesh) ship
via common/ are found. Without this, fallback_manager only sees the
round/-side placeholder set and can't render the canonical module
icons on the fallback radar.

build-eye-remote-image.sh: also copy common/assets/icons/*.png into
/usr/lib/secubox-eye/assets/icons/ (cp -n so placeholders aren't
overwritten when names collide — none do). Pure defense-in-depth.
2026-05-18 06:25:41 +02:00
33f6c1f68e fix(round): re-disable agent + storage-gadget enables wiped by force-push
PR #175 commented out four ln -sf lines (two for secubox-eye-gadget.service
storage-only mode, two for secubox-eye-agent.service Pydantic-v2/SIGILL
crash loop). A force-push for the dwc2 fix earlier today inadvertently
overwrote that branch HEAD, dropping the four edits. Re-applying them
identically so the branch's intent — only secubox-otg-gadget.service
active at boot — is restored.

Combined with the dwc2 modprobe fix already on the branch, the rpiz now
boots into:
  - secubox-otg-gadget.service modprobes dwc2, libcomposite, usb_f_*
  - waits for /sys/class/udc to populate (poll, 5s)
  - creates composite ECM+ACM gadget on the UDC
  - MOCHAbin enumerates "Linux Foundation Multifunction Composite Gadget"
2026-05-17 12:00:03 +02:00
d58460ebaf fix(round): modprobe dwc2 in secubox-otg-gadget chain (rpiz UDC was implicit on eye-gadget)
The previous build had two gadget services enabled at boot:
  - secubox-eye-gadget.service: ExecStartPre=modprobe dwc2 + libcomposite
  - secubox-otg-gadget.service: ExecStartPre=modprobe libcomposite + usb_f_*
                                (NO dwc2 — relied on eye-gadget loading it first)

When PR #175 disabled secubox-eye-gadget at boot (to stop the UDC conflict),
nobody loaded dwc2 anymore. systemd-modules-load reads /etc/modules but the
order of dwc2 load vs the otg-gadget service start window was tight — and
on the rpiz the journal showed no successful gadget enumeration after the
PR #175 image deployed.

Defensive fix in two places:

1. secubox-otg-gadget.service: prepend ExecStartPre=/sbin/modprobe dwc2.
   Drop the ConditionPathIsDirectory=/sys/class/udc gate so the service
   actually runs and can load dwc2 if it isn't loaded yet (the path is a
   sysfs dir that exists when its parent /sys/class is mounted — it was
   harmless, but removing it makes the chain self-bootstrapping).

2. secubox-otg-gadget.sh check_prerequisites(): also modprobe dwc2 (in
   case the service is invoked manually by an operator), then poll
   /sys/class/udc for up to 5s waiting for the BCM USB controller to
   bind asynchronously. Diagnostic message updated to point at the two
   places to check (dtoverlay + /etc/modules) if no UDC ever shows up.
2026-05-17 12:00:03 +02:00
CyberMind
e91b9e9ea4
feat(eye-remote): multi-gadget DHCP on eye-br0 — Phase 1 (#161)
Phase 1 of issue #158: dnsmasq scoped to eye-br0 with per-MAC stable leases in 10.55.0.10–.250. FastAPI /leases + /lease-events endpoints, Round image switched to DHCP client with usb0→eye0 rename. 65 unit/integration tests pass + 1 netns acceptance test (requires root). Ref #158.
2026-05-17 06:04:54 +02:00
CyberMind
e2a905ca91
fix(eye-remote): own the OTG bridge for multi-gadget MOCHAbin USB (#157)
Replaces the brittle .link rename approach with a package-owned `eye-br0` bridge so multiple Pi RNDIS gadgets coexist at L2. Verified live on 192.168.1.200 in #155 / PR thread. Ref #155.
2026-05-17 06:02:25 +02:00
CyberMind
d50aa52d6d
feat(common): radar_concentric painter + phase-aware dashboards (ref #138) (#142)
Port the radar concentric layout from remote-ui/round/agent/display/fallback/
into a stateless painter under secubox_common.painters.radar_concentric.
Both round and square dashboards now consume it; the square's main loop
drives a monotonic-clock phase so its left half rotates the same way the
round's deployed fallback_manager does.

What landed
-----------
- `secubox_common/painters/radar_concentric.py`
  - `paint(img, center, modules, metrics, radii=None, phase=0.0, ...)`
  - phase × 2π = sweep angle radians, clockwise from 12 o'clock.
  - Module → wheel-angle map decoupled from list order via
    DEFAULT_NAME_TO_ANGLE so AUTH/WALL/ROOT/MESH/MIND/BOOT sit at the
    rainbow position matching their colour regardless of MODULES order.
  - DEFAULT_RADII = [214, 188, 162, 136, 110, 84] matches the deployed
    fallback_manager.py geometry.
  - draw_hub=False lets the converged dashboard composite its own
    central button + pod cluster on top.

- `DashboardCanvas.paint_radar_concentric` — thin wrapper, single source
  of truth verified by a test that asserts canvas-vs-painter byte-equal.

- `RoundDashboard.layout(metrics, phase=0.0)` and
  `SquareDashboard.layout(metrics, phase=0.0)` — phase=0 preserves the
  static still-frame contract for tests / callers that don't drive
  animation. RING_RADII bumped to the deployed [214..84] geometry on
  both classes so the converged left-half look matches the round.

- `secubox_eye_square_kiosk.__main__` drives
  `phase = (time.monotonic() * RADAR_RPM / 60) % 1` at 12 RPM, matching
  fallback_manager._sweep_speed.

Tests: 8 new in secubox_common/tests/test_radar_concentric.py — phase
differentiation, period-1 wrap, default-radii fallback, length-mismatch
guard, hub toggle, sweep_accent range, canvas-helper byte-equal.

Total green: 118 / 118 (36 secubox_common + 78 square kiosk + 4 round).

Out of scope for this commit (deferred)
---------------------------------------
- `fallback_manager.py` migration to the painter (#138 acceptance lists it).
  fallback_manager uses its own brighter MODULES list and stateful
  ConnectionState machinery; converting it requires a colour-palette
  decision the user has not yet signed off on. Will land in a follow-up
  fixup after hardware confirms the square radar visual is what we want.

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:29:06 +02:00
CyberMind
b0b42e81de
Feature/135 converge round square dashboards into re (#140)
* 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>

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

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

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:26:41 +02:00
CyberMind
355767935c
Round image: drop dead ifupdown config, give secubox sudo, fix misleading OTG comment (closes #139) (#143)
* fix(round): remove dead ifupdown config + secubox sudo + clarify gadget IP comment (closes #139)

Three small cleanups against the round image, all triggered by the
follow-up to PR #137's OTG bench:

1. Drop `remote-ui/round/files/etc/network/interfaces.d/usb0` — the
   stanza configured `usb0` via `ifupdown`, but `ifupdown` is not in
   the apt list, `networking.service` is inactive, and the actual IP
   binding happens in `secubox-otg-gadget.sh` (which configures `usb1`
   programmatically with `ip addr add`). Keeping the file on disk
   misleads diagnostics — it cost a round trip during the 2026-05-15
   bench when I assumed `usb0`/10.55.0.2 must be where the IP would
   land. Deleting it.

2. Add `sudo` to the `secubox` user's group list in
   `build-eye-remote-image.sh`. Without it, ACM serial console
   recovery is impossible — the user can log in but can't fix
   anything. The round previously needed `pi/raspberry` (which the
   build script does not provision) as a fallback; now `secubox` can
   recover networking from the only path that survives a broken IP
   link.

3. Rewrite the misleading comment block in `secubox-otg-gadget.sh`
   about which kernel interface name belongs to which gadget function.
   The legacy comment claimed `usb1 = ECM` and tied the binding choice
   to "Linux uses cdc_ether"; in reality both RNDIS and ECM share the
   same host_addr so the host can reach 10.55.0.2 via either function
   (verified on Pop!_OS 22.04 host: ping succeeded with 10.55.0.1
   bound on the host's RNDIS leg). Comment now states the actual
   contract: bind on `usb1` (the second-registered network function),
   independent of which transport carries the packets.

Test plan
---------

- [ ] Rebuild the round image and flash to Pi Zero W 1st gen.
- [ ] Verify boot still reaches OFFLINE-mode radar on the HyperPixel.
- [ ] `id secubox` on the booted device shows `sudo` in groups.
- [ ] `ls /etc/network/interfaces.d/usb0` returns "no such file".
- [ ] `systemctl is-active secubox-otg-gadget` returns `active`.
- [ ] Host ping 10.55.0.2 succeeds (regression check — the comment
      rewrite is documentation-only, must not change behaviour).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(round): also drop inline heredoc that recreates dead ifupdown stanza (ref #139)

The previous commit deleted remote-ui/round/files/etc/network/interfaces.d/usb0
but missed that build-eye-remote-image.sh writes the same file inline at
build time via heredoc. Without removing that block too, the dead config
keeps shipping to the rootfs.

Verified on the just-rebuilt image: file still present after first
attempt at the fix. With both source-tree deletion AND the heredoc gone,
the config never lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:26:32 +02:00
CyberMind
839bab94a8
Converge round/ + square/ dashboards into secubox_common + pointer input on Pi 4B/400 (#137)
* 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
CyberMind
a3a918ed6f
remote-ui/square: 4 bugs caught at Pi 4B hardware bench (Phase 3 followup, ref #127) (#134)
* fix(remote-ui/square): 4 bugs caught at Pi 4B hardware bench (closes #133, ref #127)

Phase 3 (#132) merged with 82/82 pytest green and two-stage subagent
review on every task — but the kiosk crashed at hardware boot because
the review loop had no real /dev/fb0. All four fixes validated live
on a Pi 4B + official 7" DSI panel this session by hand-patching the
uSD, then ported here.

(1) /run/secubox not recreated on reboot
    Add remote-ui/square/files/etc/tmpfiles.d/secubox-eye-square.conf
    creating /run/secubox + /var/log/secubox at boot under the
    secubox-eye-square user. /run is tmpfs and the build-time mkdir
    didn't persist, so secubox-eye-square-helper.service crashed on
    Path.mkdir() at startup, which cascaded into the kiosk's
    HelperClient timeout-loop.

(2) fonts-dejavu-core missing from chroot apt-install
(3) draw.text() calls lacked font= argument
    Pillow on Bookworm (9.4) falls back to a latin-1 bitmap font when
    no font is passed. ring_dashboard.py draws "○ NOMINAL" (U+25CB)
    which raised UnicodeEncodeError. Fix on both ends: install
    fonts-dejavu-core in the chroot, expose theme.DEFAULT_FONT loaded
    from /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf, and pass
    font=theme.DEFAULT_FONT to every draw.text() in the kiosk
    (25 call sites across 6 files — confirmed by AST walk).

(4) framebuffer.py hardcoded 32bpp BGRA but vc4drmfb is 16bpp RGB565
    The Pi 4B 7" DSI exposes /dev/fb0 as DRM_FORMAT_RGB565
    (16bpp, R in top 5 bits). framebuffer.py wrote 800*480*4=1.5MB
    of BGRA bytes into a 768KB fb. mmap silently truncated, dd ...
    of=/dev/fb0 confirmed smem_len=768KB by erroring "No space left
    on device" at exactly that offset. Pillow's RGB→RGB565 raw
    packers were removed in Pillow >=9.4 (tested 9.4 and 10.2,
    same behaviour), so we pack via numpy:
        pixels = ((R & 0xF8) << 8) | ((G & 0xFC) << 3) | (B >> 3)
        pixels.astype("<u2").tobytes()
    Auto-detect bpp from /sys/class/graphics/<dev>/bits_per_pixel;
    for 32bpp paths, FBIOGET_VSCREENINFO ioctl picks the right
    Pillow raw mode (BGRA / RGBA / ARGB / ABGR). EYE_SQUARE_FB_MODE
    env var overrides for diagnostics. Adds python3-numpy to the
    chroot apt-install list.

Tests
- test_framebuffer.py rewritten: drops bpp= kwarg (now auto-detected),
  adds tests for env override, sysfs bpp=16 detection picking RGB565,
  numpy pack of pure red → 0xF800 (R in top 5 bits, LE bytes), and
  pure black → 0x0000.
- New test_theme.py: palette tuple shape + DEFAULT_FONT loads +
  Unicode glyph (U+25CB) rendering smoke.
- 68/68 kiosk tests green (was 61 + 7 new). Helper untouched.

Hardware validation (Task 23 / #127)
Same uSD with these patches renders correctly on Pi 4B + 7" DSI:
black background, six distinct ring colors (AUTH orange, WALL gold,
BOOT brown, MIND blue, ROOT teal, MESH blue), Unicode dot in tab
labels, frame-budget headroom (numpy pack ~5-10ms on Pi 4B).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(remote-ui/square): also auto-detect fb size, center-pad for HDMI on Pi 400 (ref #133)

Caught at Pi 400 + HDMI bench. The Pi 400 doesn't ship with the official
7" DSI panel, so /dev/fb0 is exposed at the HDMI monitor's native
resolution (e.g. 1920x1080) instead of 800x480. The kiosk hardcodes
800x480 and writes 800-wide rows; against a 1920-wide fb the writes
get sliced and tiled, producing rainbow stripes across the top.

Fix: in addition to the bpp + byte-order detection from the first
commit, read /sys/class/graphics/<dev>/virtual_size at __init__.
Mmap to actual fb size (not logical). Center-pad the kiosk's 800x480
canvas into a black canvas of the real fb size inside blit() before
encoding. Kiosk drawing code stays unchanged — same 800x480 design,
just letterboxed on larger displays.

API change
- FrameBuffer signature is now (path, logical_width=800, logical_height=480)
  instead of (path, width=800, height=480). width/height still exist as
  attributes but now hold the ACTUAL fb dimensions, with logical_width /
  logical_height holding the kiosk's design canvas.

Tests
- New autouse fixture _isolate_sysfs monkeypatches _read_sysfs_size to
  return the fallback. Otherwise tests on a host with a real /dev/fb0
  would pick up the laptop's display dimensions and mismatch the tmp
  fb file size.
- New tests:
  * test_fb_size_detection_uses_sysfs_when_available (1920x1080 fake fb)
  * test_blit_pads_into_larger_fb (red square center-painted into 1920x1080)
  * test_blit_no_pad_when_fb_equals_logical (fast path)
- 71/71 kiosk tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:21:49 +02:00
CyberMind
dee8bf8b81
feat(remote-ui): Phase 3 — Pillow+framebuffer kiosk for Pi 4B/400 (ref #127)
* feat(remote-ui/square): carry forward Phase 2 helper + debian + firstboot (ref #127)

Snapshot of Phase 2 PR #131's validated infrastructure, brought into Phase 3
unmodified. The Chromium+PySide6 dual-window kiosk path is dropped (replaced
by Pillow+framebuffer per the Phase 3 design spec). What carries forward:

packages/secubox-eye-square/
├── helper/                  21 pytest cases green
│   ├── eye_square_helper/   FastAPI app + SO_PEERCRED auth + 4 route modules
│   │   ├── app.py           middleware (JSONResponse fix preserved)
│   │   ├── auth.py          ALLOWED_UIDS frozenset
│   │   ├── __main__.py      uvicorn UDS bind
│   │   └── routes/          usb_gadget, service, lockdown, console
│   └── tests/               6 test files (test_e2e.py dropped — it depended
│                            on the right_panel package which is gone)
└── debian/                  arm64 package shell; control will be edited by
                             Phase 3 plan to drop Chromium/Qt/X deps

remote-ui/square/
├── files/etc/systemd/system/
│   ├── secubox-eye-square-helper.service    Helper FastAPI as secubox-eye-square user
│   ├── secubox-otg-gadget.service           configfs composite gadget (square variant)
│   └── secubox-firstboot.service            oneshot trigger for firstboot.sh
├── files/etc/udev/rules.d/
│   └── 90-secubox-otg-square.rules          host-side interface rename rule
├── files/etc/apparmor.d/
│   └── secubox-eye-square-helper            CAP_NET_ADMIN + CAP_SYS_ADMIN scope
├── files/etc/secubox/eye-square.toml.example
├── files/usr/local/sbin/firstboot.sh        GPIO 5V check + hostname + SSH + toml
├── build-eye-square-image.sh                will be heavily modified — drop Chromium/Qt/X
├── install_pi4.sh                           SD flash with safety guards
└── deploy.sh                                SSH hot-update — kiosk service list will change

Phase 1's remote-ui/common/ (PR #130) is NOT brought in — Phase 3 doesn't
consume it. round/fb_dashboard.py stays on master untouched.

What's DROPPED from Phase 2 (not brought in):
- right_panel/ entire PySide6 widget tree + ipc_bridge + WebSocket server
- square-bridge.js Chromium-side TM hook override
- secubox-kiosk-x.service, secubox-square-chromium.service,
  secubox-square-right-panel.service systemd units
- etc/openbox/{autostart,rc.xml}, etc/nginx/sites-available/secubox-square
- home/secubox/.xinitrc

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(remote-ui): Phase 3 implementation plan — Pillow+framebuffer kiosk (ref #127)

25 tasks covering kiosk skeleton, theme + modules table, sim, framebuffer +
touch input, transport manager + helper client, 4 right-pane tabs (Alerts /
Module Detail / Console / Mode Controls), right_panel composer, ring_dashboard
(Pillow port of Phase 2's QPainter ring renderer — visually faithful to
round/'s dashboard but independently authored, since round/fb_dashboard.py
must remain unchanged), __main__ event loop, systemd unit, fb udev rule,
debian/control simplification (drop Qt/X/Chromium, add python3-pil/evdev),
build script + firstboot + deploy.sh edits, README + CLAUDE.md docs,
regression test, Pi 4B + Pi 400 hardware bench tests, open PR.

Plan supersedes Phase 2 PR #131 (closed). Phase 2 helper FastAPI carries
forward unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(remote-ui/square): scaffold kiosk/ package skeleton (ref #127)

* feat(remote-ui/square/kiosk): theme.py + modules_table.py with TDD (ref #127)

* feat(remote-ui/square/kiosk): sim.py drift generator with TDD (ref #127)

* feat(remote-ui/square/kiosk): framebuffer.py mmap helper with TDD (ref #127)

* feat(remote-ui/square/kiosk): touch_input.py evdev reader + classify (ref #127)

* feat(remote-ui/square/kiosk): transport_manager.py with TDD (ref #127)

* feat(remote-ui/square/kiosk): helper_client.py sync httpx UDS with TDD (ref #127)

* feat(remote-ui/square/kiosk): tabs/alerts.py with TDD (ref #127)

* feat(remote-ui/square/kiosk): tabs/module_detail.py with TDD (ref #127)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(remote-ui/square/kiosk): tabs/console.py with TDD (ref #127)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(remote-ui/square/kiosk): tabs/mode_controls.py with TDD (ref #127)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(remote-ui/square/kiosk): right_panel.py tab manager with TDD (ref #127)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(remote-ui/square/kiosk): ring_dashboard.py with easing + alerts ribbon (ref #127)

The implementer adapted update_metrics() to store raw metric values in
_target/_current (e.g. cpu_percent=80.0) rather than the 0..1 extracted
ratio from the plan's code block — this matches the test's assertion
(_target["cpu_percent"] == 80.0). Module.extract() is now applied at
draw() time. Behaviour is identical from the renderer's perspective.

Also added 'if m.metric in metrics' guard so partial-metric updates
preserve existing targets for unspecified keys (kiosk-friendly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(remote-ui/square/kiosk): __main__.py event loop + smoke tests (ref #127)

Wires HelperClient + TransportManager + SimState + RingDashboard +
RightPanel + FrameBuffer into a 30 FPS event loop. Probes transport
every 30s, fetches metrics every 2s (falls back to SIM), composes
800x480 frame (dashboard + panel), blits to /dev/fb0.

Touch input integration deferred; modules from Task 6 are available
for the bench iteration to wire when the hardware ships.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(remote-ui/square): secubox-square-kiosk.service unit + fb0 udev rule (ref #127)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(secubox-eye-square): debian/control drops Qt/X/Chromium deps for Phase 3 (ref #127)

Phase 3 replaces the Phase 2 Chromium+PySide6 stack with a single-process
Pillow+/dev/fb0 kiosk. Dependency cleanup:

 dropped: chromium, openbox, xserver-xorg, xinit, unclutter, nginx-light,
          python3-pip, python3-pyside6 (Recommends), python3-qasync
 added:   python3-pil, python3-evdev

Helper FastAPI deps unchanged (carried forward from Phase 2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(remote-ui/square): build-eye-square-image.sh slimmed for Phase 3 (ref #127)

Apt install list trimmed: dropped chromium, openbox, xserver-xorg, xinit,
unclutter, 12 libxcb-* libraries, nginx-light, python3-pip; added
python3-pil + python3-evdev. The PySide6 pip install is gone.

Removed Phase 2 vestiges:
- nginx + round/ html copy + square-bridge.js injection
- openbox autostart, xinitrc chmod
- render group (no /dev/dri access needed without Chromium)

Systemd enable list updated:
- Dropped: nginx, secubox-kiosk-x, secubox-square-chromium,
           secubox-square-right-panel
- Added:   secubox-square-kiosk
- Default target now multi-user.target (was graphical.target)

Python install copies kiosk package (was right_panel package).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(remote-ui/square): firstboot.sh enables kiosk service (drops nginx + 3 Phase 2 units) (ref #127)

Phase 3 firstboot only enables secubox-otg-gadget, secubox-eye-square-helper,
and secubox-square-kiosk. The X / Chromium / right-panel / nginx units no
longer exist on the image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(remote-ui/square): deploy.sh targets kiosk service (drops chromium+right-panel+nginx) (ref #127)

Hot-update flow simplified for Phase 3:
- Rsync helper + kiosk Python packages only (no /var/www/, no
  square-bridge.js, no nginx content).
- Restart secubox-eye-square-helper + secubox-square-kiosk
  (was chromium + right-panel + nginx reload).
- Health check via systemctl is-active (was curl http://localhost/).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(remote-ui/square): README + CLAUDE.md for Phase 3 (ref #127)

README rewritten for the Phase 3 architecture (Pillow+/dev/fb0 kiosk, no
Chromium/Qt/X/nginx). CLAUDE.md added with file map, stack inventory, run +
debug recipes, and the round/ untouched constraint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 05:59:42 +02:00
CyberMind
7c37415f88
feat(remote-ui): Phase 1 — extract common/ shared core (ref #127)
* feat(remote-ui/common): scaffold shared-core directory (ref #127)

* feat(remote-ui/common): extract palette.css from round/ (ref #127)

* fix(remote-ui/common): trim palette.css to spec (6 module + 8 C3BOX tokens) (ref #127)

* feat(remote-ui/common): extract base.css verbatim from round/ (ref #127)

* feat(remote-ui/common): extract ICONS to icons.js (ref #127)

* docs(remote-ui/common): correct icons.js header comment — sizes are {22,48,96} not 128 (ref #127)

* feat(remote-ui/common): extract RINGS + CX/CY/SA to modules-table.js (ref #127)

* feat(remote-ui/common): extract CFG to config.js (replaces jwt-helper.js per #127 plan revision)

* feat(remote-ui/common): extract TransportManager with onModuleTap/onTransportChange hooks (ref #127)

* feat(remote-ui/common): extract SIM + simStep to sim.js (ref #127)

* feat(remote-ui/common): move 24 SecuBox module PNG icons to common/assets/icons/ (ref #127)

* feat(remote-ui/common): move secubox-otg-gadget.sh, add GADGET_NAME env override + arm64 serial fallback (ref #127)

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

* fix(remote-ui/common): trim whitespace from device-tree serial-number read (ref #127)

* feat(remote-ui/common): move secubox-otg-host-up.sh + variant-aware comment (ref #127)

* refactor(remote-ui/round): consume common/ via <link>/<script> tags (ref #127)

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

* feat(secubox-system): add form_factor to RemoteUIConnectedRequest with TDD (ref #127)

* feat(remote-ui/round): deploy.sh bundles common/ alongside round/ (ref #127)

* fix(remote-ui/round): deploy.sh COMMON_SRC path resolution + rsync --delete (ref #127)

* feat(remote-ui/round): build-eye-remote-image.sh embeds common/ + nginx /common/ alias (ref #127)

* docs(remote-ui): document common/ dependency in round/ docs (ref #127)

* docs(remote-ui/round): clarify palette.css is forward-looking (ref #127)

* docs(remote-ui): Task 18 regression-gate report — structural verification (ref #127)

Full diffoscope gate blocked by missing hyperpixel2r.dtbo prerequisite.
Structural equivalence verified instead: Phase 1 changes are purely
additive (common/ embed + nginx /common/ alias + extracted JS/CSS/icons),
no behavioural change to round/'s existing logic.

User must run the full image diffoscope manually after sourcing the
hyperpixel2r.dtbo blob, before final merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(eye-remote): bump workflow VERSION env 2.2.0 → 2.2.1 (ref #127)

Pre-existing drift: build-eye-remote-image.sh writes
secubox-eye-remote-2.2.1.img but the workflow's Compress/Checksum/
Upload steps reference ${{ env.VERSION }} = 2.2.0, so the compress
step fails with "No such file or directory" after a successful build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:59:40 +02:00
462d271278 feat(eye-remote): Add Mode Control API with metrics endpoint
- Add mode_api.py: HTTP API for manual gadget mode switching (port 8081)
  - GET /api/status: Current gadget mode and functions
  - POST /api/switch/<mode>: Switch to composite/network/storage/silent/hid
  - GET /health: Health check for dashboard
  - GET /metrics: System metrics (CPU, RAM, disk, temp, load, uptime)
- Web UI control panel with mode buttons
- Integrated into admin dashboard at /eye-remote/
- Uses port 8081 to avoid conflict with CrowdSec on 8080

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-12 08:15:10 +02:00
b859817ec9 fix(eye-remote): Improve auto-mode stability and single-instance display
- Auto-mode controller now detects current gadget mode from configfs
- Probes network connectivity BEFORE attempting mode switches
- Stays in COMPOSITE mode when network already works (no unnecessary switching)
- Display manager uses PID file locking to prevent multiple instances
- Gadget-setup.sh uses lock file and cooldown to prevent switch race conditions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 15:55:01 +02:00
3e2fc27e50 fix(eye-remote): Use CDC-ECM instead of RNDIS for USB gadget network
- RNDIS was not responding to incoming connections
- CDC-ECM interface works correctly for bidirectional traffic
- Updated udev rules to use CDC-ECM as primary, disable RNDIS
- Fixed subnet mask from /24 to /30 for point-to-point link
- Added deploy-auto-mode.sh for deploying auto-mode system
- Added test-auto-mode.sh for testing all gadget modes

The Pi Zero W composite gadget exposes both RNDIS and CDC-ECM,
but CDC-ECM provides better compatibility with Linux hosts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 12:29:43 +02:00
8f49ec1996 feat(eye-remote): Add intelligent auto-mode OTG switching system
Implement auto-mode controller for Eye Remote that handles:
- Automatic host type detection (U-Boot vs Linux OS)
- Mode priority: network > hid > storage
- Silent storage fallback after 60s disconnected
- Multiple wake-up triggers (serial, magic file, GPIO, scheduled)

New files:
- agent/auto_mode_controller.py: State machine controller
- agent/wakeup_manager.py: Wake-up trigger handlers
- files/etc/secubox/eye-remote/auto-mode.toml: Configuration
- files/etc/systemd/system/secubox-auto-mode.service: Systemd unit
- scripts/setup-hid-gadget.sh: HID keyboard gadget setup

Modified:
- gadget-setup.sh: Add hid, network, silent modes
- gadget_switcher.py: Add HID mode, transfer protection
- failover.py: Add SILENT_STORAGE state
- gadget_status.py: Add storage mode display

Features:
- U-Boot detection (powered >3s without config)
- Network probing with configurable endpoints
- Transfer protection before mode switch
- Silent fallback with wake monitoring
- Storage mount status detection from Pi

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 12:09:34 +02:00
e71209f523 fix(eye-agent): Resolve import errors and add missing modules (ref #78)
- Add robust import handling with try/except blocks in main.py
- Create stub implementations for missing modules:
  - system.py: WifiManager, BluetoothManager, DisplayController
  - secubox.py: DeviceManager, FleetAggregator
  - web.py: WebServer
- Add mode-specific renderers in display/renderers.py:
  - DashboardRenderer: 3D cube + metric rings
  - LocalRenderer: Local mode display
  - FlashRenderer: Flash messages
  - GatewayRenderer: Fleet overview
  - RenderContext: Data container for renderers
- Update display/__init__.py with graceful fallbacks
- Imports now succeed even when optional dependencies (aiohttp) are missing

The agent can now be imported without crashing when some components
are unavailable. This allows the service to start and provide
basic functionality while degraded.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 11:43:14 +02:00
13d7bceab6 docs(eye-remote): add product status image AI prompt
Detailed prompt for GPT/Gemini/DALL-E to generate
v2.2.1 product status infographic with hardware,
software, features, and connectivity checklists.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 11:32:12 +02:00
71959401da docs(eye-remote): add AI infographic roadmap prompt
Detailed prompt for GPT/Gemini/DALL-E to generate
colored cyberpunk-style roadmap infographic with:
- SecuBox color palette
- Version timeline (v2.2.1 → v3.0.0)
- Feature breakdown per version

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 11:16:29 +02:00
5137b26d17 docs(eye-remote): add AI assistant issue template
Prompt for GPT/Gemini/Claude to create well-formatted
Eye Remote GitHub issues with proper context.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 11:14:38 +02:00
cef2d25aba docs(eye-remote): add roadmap section with issues and refs
- ASCII sketch of version progression (v2.2.1 → v3.0.0)
- v2.3.0: Agent revival tasks (#78)
- v2.4.0: Performance/Buildroot (#79)
- v3.0.0: Multi-device mesh concept
- Ideas & proposals section
- Wiki and internal doc references

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 11:13:45 +02:00
897dc339a6 docs: update WIP, HISTORY, README for Eye Remote v2.2.1
- Session 146: build script fix, image validation
- README: v2.2.1 features and known issues
- GitHub issues #78, #79 referenced

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 10:51:33 +02:00
b2f046c146 fix(eye-remote): use fallback-display service instead of broken eye-agent
- Add secubox-fallback-display.service (3D cube + rainbow rings)
- Enable fallback-display instead of broken eye-agent
- Add libopenjp2-7, libtiff6 PIL dependencies
- Bump version to 2.2.1

The fallback_manager.py provides a stable working dashboard with:
- Connection states: OFFLINE/CONNECTING/ONLINE/COMMUNICATING
- Real metrics from MOCHAbin via direct API calls
- 3D rotating cube and rainbow ring visualizations

The eye-agent has import errors (relative imports, missing classes)
and needs further work to be functional.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 10:26:12 +02:00
66860c8022 fix(eye-remote): Fix HyperPixel 2r LCD init causing kernel panic
Problem:
- hyperpixel2r-init.service had Restart=on-failure which caused
  repeated restart attempts during boot
- This triggered a kernel panic in bcm2835_handle_irq due to
  GPIO/DPI timing conflicts

Solution:
- Remove Restart=on-failure from hyperpixel2r-init.service
- LCD init is a one-shot operation that should not retry
- Add Before= ordering to ensure init completes before dashboard
- Configure pigpiod with -l flag (local socket only)

The fix allows the HyperPixel 2.1 Round display to initialize
correctly on Pi Zero W with Bookworm.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-11 07:51:39 +02:00
45c2b7c020 feat: Add metoblizer, streamlit power management, replace espressobin with mochabin
- Add secubox-metoblizer package (centralized log aggregator)
- Streamlit: double-buffer caching, power management UI, autostart
- Streamlit UI: consolidate Components into Status tab
- GitHub Actions: replace espressobin-v7 with mochabin for live USB builds
- Various UI improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-10 18:37:58 +02:00
7af2ac641c feat(eye-remote): Add virtual round display radar animation
- vr-radar.js: Full radar sweep animation for virtual round display
  - True hexagonal icon layout (60° spacing)
  - Rainbow colored rings (red→orange→yellow→green→blue→violet)
  - Rainbow radar sweep line with blue trail
  - Colored circles around each icon matching module color
  - Icons illuminate when radar passes
  - Real-time metrics from hub API

- vr-styles.css: Pod highlighting styles for radar animation

Matches the real HyperPixel 2.1 Round gadget display exactly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-08 19:28:44 +02:00
47372e8601 feat(eye-remote): Add radar sweep animation dashboard
- New radar.html for Eye Remote HyperPixel Round display
- Classic radar sweep animation with green phosphor aesthetic
- 6 module blips (AUTH/WALL/BOOT/MIND/ROOT/MESH) at 60° intervals
- Blips illuminate when radar sweep passes with fade effect
- Color-coded status: green=ok, yellow=warn, red=critical
- Concentric grid rings and 12 radial lines
- Center: time/date/hostname/status with glow effects
- Mini metrics bar: CPU/MEM/DSK
- Transport auto-detection: OTG/WiFi/SIM
- Alert ticker at bottom
- Simulation mode for offline demo
- Threshold alerts per module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-08 19:06:58 +02:00
0bed6f0f04 feat(metrics): Add connections tracking for Round Eye MIND metric
- Add connections, peak_connections, connections_percent to /api/v1/system/metrics
- Track current TCP connections via /proc/net/tcp and /proc/net/tcp6
- Persist peak_connections to file for resilience across restarts
- Pre-calculate connections_percent (current/peak * 100) for MIND ring
- Update README.md and WIKI.md with new API response documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-07 15:42:34 +02:00
eab3688561 fix(eye-remote): Prevent duplicate IP on USB composite gadget
Pi Zero composite gadget creates both RNDIS (usb0) and CDC-ECM (usb1)
interfaces with same MAC prefix. This caused duplicate routes for
10.55.0.0/24 network, breaking connectivity to Eye Remote.

Fix:
- RNDIS (usb0): Configure with IP 10.55.0.1/24, bring up
- CDC-ECM (usb1): Disable (ip link down) to prevent route conflict
- Add serial console symlink /dev/eye-remote-console

Ref #52

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-07 04:54:38 +02:00
a32a452442 feat(eye-remote): Add /api/v1/system/metrics alias for Pi Zero compatibility
- Added /api/v1/system/metrics endpoint to eye-remote API
- Returns host metrics (CPU, mem, disk, load) for remote-ui dashboard
- Pi Zero can now seamlessly fetch MOCHAbin metrics
- Reverted secubox_client.py to use original /api/v1/system/metrics path

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-06 17:16:33 +02:00
3f0b549a13 feat(infra): Add vhost-matrix-sync.sh + fix Eye Remote metrics endpoint
VHost Matrix Sync (scripts/vhost-matrix-sync.sh):
- Extracts vhost→backend mappings from HAProxy config
- Syncs to mitmproxy routes.json (using 10.100.0.1 for LXC bridge)
- Syncs to health prober vhost-matrix.json
- Commands: sync, status, reload

Eye Remote Round UI fix:
- Changed METRICS_ENDPOINT from /api/v1/system/metrics
  to /api/v1/eye-remote/metrics/public
- Pi Zero will now fetch MOCHAbin host metrics, not local metrics

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-06 17:14:48 +02:00
01f2bf119d fix(eye-remote): Resolve rp_filter and dual-interface routing issues
Root causes identified and fixed:
- "martian source" packet drops due to rp_filter=1 on USB interfaces
- Dual interface conflict: usb0 (RNDIS) and usb1 (CDC-ECM) both getting
  same IP 10.55.0.2, causing routing loops on Pi Zero

MOCHAbin fixes:
- Add sysctl.d/99-secubox-usb.conf to disable rp_filter globally
- Update udev rules to disable rp_filter per-interface on add
- Add bind/change events for USB re-plug detection
- Update secubox-eye-network.sh with find_interface() and rp_filter disable

Pi Zero fixes:
- Add usb1-disable config to install_zerow.sh to prevent routing conflict

Tested: Ping/TCP connectivity restored, metrics flowing via API.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-05 11:22:31 +02:00
30b8773390 feat(eye-remote): Add auto-pair and paired-devices endpoints
- Add /api/v1/eye-remote/auto-pair POST endpoint for device pairing
- Add /api/v1/eye-remote/paired-devices GET endpoint for listing devices
- Add /api/v1/eye-remote/pizero/metrics public endpoint for dashboard
- Integrate pizero-metrics-api into install_zerow.sh builder (v1.9.0)
- Metrics API auto-installed on new Pi Zero SD cards

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-05 10:54:06 +02:00
772562c4e6 feat(eye-remote): Add auto-pairing and Pi Zero metrics API
Eye Remote Dashboard:
- Add Device Pairing card with auto-pair button
- Add paired devices count display
- Fix metrics endpoint to use /pizero/metrics
- Add autoPair(), listPaired(), loadPairedCount() functions

Pi Zero Metrics API:
- Add pizero-metrics-api.py - minimal HTTP server for system metrics
- Add pizero-metrics.service systemd unit
- Provides /api/v1/status, /metrics, /health endpoints
- Returns CPU%, memory%, disk%, temperature, uptime, load averages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-05 10:45:54 +02:00
8a0aa57f70 fix(net-detect): Separate VirtualBox VM handling from baremetal x64
VMs with multiple interfaces (NAT + host-only) now correctly configure
all interfaces with DHCP instead of creating a br-lan bridge that
conflicts with VBox's host-only DHCP server.

Changes:
- Add dedicated x64-vm case in get_interface_config()
- Set profile="vm" for VMs to trigger single mode (no bridge)
- In generate_netplan(), detect x64-vm board and configure ALL
  physical interfaces with DHCP instead of WAN-only
- VM profile forces mode="single" in main() to skip bridge creation

This fixes host-only network access for VirtualBox VMs while
preserving router/bridge functionality for baremetal deployments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 17:33:31 +02:00
f8b006f9be feat(eye-remote): Change disk icon to floppy diskette
Replace hard drive icon with classic 3.5" floppy disk icon for the
DISK metric on Eye Remote display. More recognizable and nostalgic.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-30 09:12:57 +02:00
6a1434c590 fix(eye-remote): Resize metric symbols to fit targeting circle
Compact all metric symbols to fit within the 30px targeting glow:
- CPU: 20px chip with shorter pins
- MEM: 36x16px RAM stick
- DISK: 24px disk with spindle
- LOAD: 28px gauge arc
- TEMP: 30px thermometer
- WiFi: 22px signal arcs

Symbols only appear when ONLINE and radar targets module.
Offline mode (local metrics) unchanged - shows module icons.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 20:57:58 +02:00
892a1a23e1 feat(eye-remote): Show metric symbols when radar targets module
When the radar sweep targets a module icon, temporarily replace
the module icon with a metric symbol representing its function:
- AUTH → CPU chip with pins
- WALL → RAM stick with chips
- BOOT → Hard drive cylinder
- MIND → Gauge with dynamic needle
- ROOT → Thermometer with level
- MESH → WiFi signal arcs

Symbols are drawn using PIL primitives for crisp rendering.
Original module icon returns when radar moves away.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 20:55:33 +02:00
663b403e5c fix(eye-remote): Remove center metrics text from display
Clean up display by removing module label/value text overlays.
Only icons and rings are shown now for a cleaner look.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 20:46:20 +02:00
0be4d999a8 fix(eye-remote): Fix FallbackManager API fetch for ESPRESSObin
- Switch from HTTP:8000 to HTTPS:443 (nginx reverse proxy)
- Add SSL context for self-signed certificate bypass
- Handle ESPRESSObin field names (memory_used_percent, disk_used_percent)
- Add synchronous initial fetch on OFFLINE→ONLINE transition
- Fix race condition: don't overwrite API values with local metrics
- Add /api/v1/eye-remote/api/system/info as primary endpoint
- Add ESPRESSObin main network IP (192.168.255.250) as fallback
- Network: Pi Zero W (10.55.0.2) ← OTG USB → ESPRESSObin (10.55.0.1)

This fixes the issue where Pi Zero W display showed local metrics
instead of remote ESPRESSObin metrics when connected via OTG.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 20:39:05 +02:00
a84f3a8828 fix(eye-remote): Rename 'validate' field to avoid Pydantic conflict
The 'validate' field name in ImportRequest shadows Pydantic's BaseModel
validate method, causing NameError on Pydantic 1.x. Renamed to 'do_validate'.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 20:05:22 +02:00
55eff8fee9 feat(eye-remote): Add recovery boot protocols + unified design charter
Recovery Boot System:
- Add mvebu64boot protocol for Armada 7040/8040 (MOCHAbin 64-bit boot)
- Add kwboot protocol for Armada 3720 (ESPRESSObin serial boot)
- Add XMODEM-CRC file transfer protocol for BootROM
- Add RecoveryController with auto-detection and multi-protocol support
- Port mvebu64boot.c (Pali Rohár) to async Python implementation

Design Charter v2.0:
- Sync graphic charter with Eye Remote dashboard metrics
- Add module → metric mapping (AUTH=CPU, WALL=MEM, BOOT=DISK, etc.)
- Add unified alert thresholds across Eye Remote and Smart-Strip
- Document RGB values for SK6812 LEDs
- Add pod layout diagram for round display
- Add transport badge colors (OTG=ROOT, WiFi=MESH)

Smart-Strip v1.2:
- Sync with graphic charter module colors and metrics
- Add alert threshold table matching Eye Remote
- Document automatic metric mode behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 13:01:43 +02:00
c4b7ac0f7f feat(eye-remote): Add multi-mode display system v1.9.0
Eye Remote Interactive UI enhancements:
- TTY mode: Serial terminal display from /dev/ttyGS0
- Flash mode: Progress bar with speed/ETA for USB transfers
- Auth mode: QR code generation for backup authentication
- Mode detection via /etc/secubox/gadget-mode

Hub service VM compatibility fix:
- Changed from Unix socket to TCP port 8001
- Updated nginx configs for TCP proxy
- Fixes 502 errors in VirtualBox VMs

Also includes:
- FAQ/Troubleshooting wiki page with GitHub issue links
- Kiosk launcher --no-sandbox fix for VMs
- Profile Generator GUI mockup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 10:24:02 +02:00
33304e7b6f feat(build): Add kiosk mode + VirtualBox setup + Profile Generator architecture
VM-x64 Build Improvements:
- Enable kiosk mode by default with GRUB menu entries
- Increase image size to 8GB (5.5GB root) for full package set
- Add proper EFI boot configuration with 3 boot modes
- Pre-generate SSL certificates during build
- Add TTY2 autologin for emergency console access

VirtualBox Support:
- Add scripts/vbox-setup.sh for automated VM setup
- Add wiki/VirtualBox-Setup.md with comprehensive guide
- Support bridged, NAT, and host-only network modes
- Auto-detect network adapter for bridged mode

Architecture Documentation:
- Add docs/architecture/profile-generator.md (v0.2)
- Define tier × board × flavors resolution model
- Document 3 CLI tools: secubox-gen, secubox-build, secubox-fetch
- Add Phase 12-14 roadmap alignment for Ulule Campaign 1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 09:16:36 +02:00
1b7669124d feat(eye-remote): Implement OTG features 3-5 (gadget control, storage sync, setup wizard)
Complete Eye Remote OTG features implementation:

Feature 3 - Gadget Mode Control:
- Add gadget_config.py for TOML-based USB gadget configuration
- Add gadget_switcher.py for mode switching via configfs
- Add gadget_gesture.py for touch-based mode selection
- Add eye-gadget-switch.sh shell script for actual mode changes
- Add gadget.toml default configuration

Feature 4 - Storage Sync:
- Add storage_manager.py for USB mass storage partition management
- Add backup_manager.py with AES-256 encrypted backups
- Add config_sync.py for bidirectional SecuBox config sync
- Add storage.py API routes for backup/restore/sync operations

Feature 5 - Self-Setup Portal:
- Add setup.py wizard controller with 7-step state machine
- Add setup_wizard.py display renderer for HyperPixel round screen
- Add setup.py API routes for wizard navigation and configuration
- Steps: Welcome, Network, Security, Services, Mesh, Verify, Complete

API endpoints added:
- /gadget: status, mode, modes, config
- /storage: info, mount, files, backup, export, import
- /setup: status, start, next, back, detect, verify, complete

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-29 06:41:23 +02:00
a697925a3f fix(build): Upgrade Python deps + CORS + login endpoint fixes
- Add CORS headers to nginx secubox-proxy.conf for cross-origin API requests
- Fix login.html endpoints: /auth/login -> /login
- Upgrade Python deps in build scripts: pydantic>=2.0, fastapi>=0.100, uvicorn>=0.25
- Add pip upgrade in secubox-core postinst for Debian bookworm compatibility
- Fix display/__init__.py to import existing modules only

Fixes authentication and API issues in VBox and ebin builds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-28 20:53:40 +02:00
d34b07db33 feat(eye-remote): Reorder rings + fix arc-icon color alignment
- Rings order (outer→inner): rouge, orange, jaune, vert, bleu, violet
- MODULES array reordered: BOOT, AUTH, WALL, ROOT, MESH, MIND
- Fixed PIL angles for arc bases to face matching colored icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-28 18:06:30 +02:00
bb73adfede feat(eye-remote): Align metric arcs with icon directions
- Each colored arc now points toward its corresponding icon
- Fixed green/purple and red/yellow position mapping

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-28 17:47:54 +02:00