Compare commits

..

4 Commits

Author SHA1 Message Date
8f46bcb93b Merge branch 'feature/appstore-phase-a' — App Store Phase A (read-only catalog)
Some checks are pending
License Headers / check (push) Waiting to run
2026-06-29 13:20:49 +02:00
58f1f1a2c8 fix(appstore): serve catalog API from secubox-routes.d (beats /api catch-all)
The /api/v1/appstore/ route must live in secubox-routes.d/ (the hub's
authoritative API include) or the generic /api/ -> aggregator catch-all
swallows it (404). Split: API route -> secubox-routes.d/appstore-routes.conf
(standalone appstore.sock), UI -> secubox.d/appstore.conf. Bumps 0.1.1.
2026-06-29 13:20:17 +02:00
66301f4307 feat(appstore): SecuBox App Store Phase A — read-only module catalog
New secubox-appstore module: api/main.py serves /catalog (category/tier/
state/q), /module/{name}, /categories, /health — merging a build-time
catalog (generated from every module's debian/secubox.yaml) with live dpkg
+ systemctl state (available/installed/running/tier-locked). Categorized,
tiered, searchable grid UI at /appstore/. Standalone service on
/run/secubox/appstore.sock, nginx route + static (no aggregator dependency).
Also backfills secubox-lyrion's missing manifest.
2026-06-29 13:16:37 +02:00
a9f349a57d docs(appstore): SecuBox App Store + Module Composer architecture sketch + GPT prompt
Catalog over the 128 secubox.yaml manifests; granular control
(module/component/navbar/appearance); profiles (4R atomic apply); P2P-mirrored
apt repo + federated catalog; multi-service mesh agents. Includes a complete
GPT architectural-research-notes prompt (§10).
2026-06-29 13:09:15 +02:00
16 changed files with 709 additions and 0 deletions

View File

@ -0,0 +1,165 @@
# SecuBox App Store / Module Manager — Architecture Sketch
**Date:** 2026-06-29 · **Status:** brainstorm sketch (pre-spec) · **Author intent:** "appstore categorized + live prefs/components care; lyrion/peertube/nextcloud/webmail/kbin are modules of this toolbox system; implement shortly."
---
## 1. Core idea
A single web UI to **discover → install → enable/disable → configure → monitor** every SecuBox module, like an app store but fused with a live "components care" panel (prefs + health + restart/repair). It is a *front-end + lifecycle layer over manifests that already exist* — not a new packaging system.
**Foundation already present (don't reinvent):**
- **128 modules** each ship `debian/secubox.yaml` = `{name, category, tier, description, depends, api:{socket,health}, ui:{path}}` — this IS the catalog entry.
- Categories in use: `media, email, ai, iot, communication, publishing, network, security, system, vpn, dashboard, misc`. Tiers: `lite | standard | pro | all`.
- Module anatomy: `api/main.py` (FastAPI control-plane), `nginx/*.conf`, `sbin/<mod>ctl` (29), `lib/<mod>/install-lxc.sh` (9 LXC-backed: lyrion, peertube, photoprism, jellyfin-ish, grafana, mqtt, yacy, rustdesk, zigbee), `conf/<mod>.toml.example`, `menu.d/NNN-<mod>.json` (129), `www/<mod>/` (150).
- The **aggregator** mounts each module API at `/api/v1/<name>/`; the **hub** is the central dashboard; **menu.d** drives the menu.
- apt repo `apt.secubox.in` is the package source; `dpkg` is installed-state truth.
## 2. State model (per module)
`available` (in apt, not installed) → `installed``enabled` (service on + menu/nginx wired) → `running` (healthy). Plus `update-available`, `error`, and `tier-locked` (board tier < module tier). Health = {service active?, LXC state (for LXC apps), socket up?, last /health, mem/disk}.
## 3. Backend — `secubox-appstore` module (FastAPI, on the aggregator, runs as `secubox`)
Read/aggregate endpoints (safe, no privilege):
- `GET /catalog?category=&tier=&q=` — merged list: apt-available dpkg-installed, each with manifest + state + version + update flag.
- `GET /module/{name}` — manifest + live state + health + prefs schema + screenshots.
- `GET /module/{name}/health` — proxy module `/health` + LXC/service/socket/resources.
- `GET /jobs/{id}` — async job status (install/uninstall progress).
Mutating endpoints (delegate to a root helper — see §4):
- `POST /module/{name}/install` · `/uninstall`
- `POST /module/{name}/enable` · `/disable`
- `POST /module/{name}/restart`
- `GET/PUT /module/{name}/prefs` — read/write the module TOML (schema-validated; sensitive configs go through the CSPN double-buffer/4R: shadow → validate → atomic swap → rollback).
- `POST /module/{name}/expose` — toggle public exposure (HAProxy ACL + mitmproxy route, **never waf_bypass**) — later phase.
## 4. Privilege & async model (the hard part)
The FastAPI runs as `secubox` and **cannot** apt-install, run `install-lxc.sh`, `lxc-*`, `systemctl`, or edit `/etc/nginx`. Mirror the proven `sbx-mesh-up`/`<mod>ctl` pattern:
- A **root helper `secubox-appstorectl`** does the privileged verbs (`install <name>` = apt-get install + run the module's `install-lxc.sh` if LXC-backed; `enable/disable` = systemctl + menu.d toggle + nginx reload; `uninstall` = apt purge [+ optional LXC destroy]).
- FastAPI invokes it via a **narrow sudoers rule** (only `secubox-appstorectl <verb> <name>`), or hands jobs to a **privileged oneshot/queue** (a root `secubox-appstore-worker` consuming a job dir).
- Installs take **minutes** (apt + debootstrap LXC) → **async job model**: enqueue → return job id → UI polls `/jobs/{id}` with a progress stream (stage: download → install → provision-lxc → enable → health-check).
- Every action → append-only **audit log** (`/var/log/secubox/audit.log`), CSPN requirement.
## 5. Manifest schema extension (`secubox.yaml` v2 — additive, back-compatible)
Add optional appstore fields, defaulted so the 128 existing manifests still parse:
```yaml
appstore:
icon: "lyrion.svg" # or emoji/glyph
summary: "Squeezebox / LMS music server"
long_description: | # markdown
screenshots: ["1.png","2.png"]
lxc: true # LXC-backed (drives install-lxc.sh step)
exposure: optional # none | optional | required (HAProxy/mitmproxy)
default_ports: [9000, 3483]
resources: { ram_mb: 512, disk_mb: 2048 }
conflicts: [] # mutually-exclusive modules
homepage: "https://lyrion.org"
```
## 6. Web UI — components
- **CatalogGrid** + **AppCard** (icon, name, summary, category/tier badge, state pill, primary action toggle).
- **CategoryFilter** (chips from `category`), **TierFilter**, **SearchBox** (name/desc), **State filter** (installed/enabled/available/updates).
- **AppDetailDrawer**: description + screenshots; **ActionBar** (Install/Enable/Disable/Restart/Uninstall, tier-locked → upsell); **PrefsForm** (schema-driven from `conf/*.toml.example`); **HealthPanel** (service/LXC/socket/resources, live); **LogViewer** (journalctl tail); **JobProgress** (install spinner with stages).
- **Live Care view**: cross-module health board (what's down / degraded), bulk restart/repair, update-all.
- Style: SecuBox C3BOX palette (cosmos-black/gold-hermetic/cyber-cyan, Cinzel/JetBrains Mono).
## 6b. Granular control + Profile system (requested 2026-06-29)
The store doesn't just install/remove whole modules — it **composes the appliance** at four granularity levels, and **Profiles** bundle a whole composition you can switch atomically.
**Granularity levels (each independently toggleable):**
- **L0 — Module:** install / enable / disable the whole `secubox-<name>` (service + LXC).
- **L1 — Component:** sub-features *within* a running module (e.g. lyrion: streaming on / plugins off; nextcloud: files on / calendar off; a security module: detection on / active-response off). Driven by a `components:` list the module declares in its manifest, mapped to the module's own config flags / sub-services.
- **L2 — Navbar / menu:** show/hide and reorder individual `menu.d` entries — UI surface independent of whether the module runs (hide an app from the menu without disabling it, or vice-versa).
- **L3 — Appearance:** theme / palette / layout / branding (the C3BOX themes), per-board or per-profile.
**Profiles** = a named, versioned bundle of `{enabled modules, per-module component flags, navbar layout+order, appearance/theme, exposure settings}`.
- **Built-in presets:** *Minimal*, *Media Center* (lyrion/peertube/jellyfin/photoprism + lean navbar), *Security Ops* (waf/crowdsec/dpi/soc forward), *Home/Family* (nextcloud/mail/homeassistant), *Headless/Kiosk*. Presets can align with the board **tier**.
- **Custom profiles:** create / clone / edit / export / import.
- **Switch = one atomic transaction** via the CSPN double-buffer/4R (shadow → validate → swap → rollback) — flipping a profile reconfigures modules+navbar+appearance together, with one-click rollback if it breaks.
- **Scope:** a per-board *active profile* (the appliance's persona) and optionally **per-user** profiles (master users `gk2`/`admin` see different navbars/apps) — ties into identity.
- **Storage:** `/etc/secubox/profiles/<name>.toml` + an `active` pointer + 4R snapshots; API: `GET /profiles`, `GET/PUT /profiles/{name}`, `POST /profiles/{name}/apply` (atomic), `POST /profiles/{name}/rollback`, `GET /profiles/active`.
## 6c. P2P distribution + multi-service agents (requested 2026-06-29)
The app store rides the **gondwana mesh** — both its *package source* and its *running services* are federated across nodes, so the fleet is redundant and clients are served from the nearest node.
**P2P-mirrored apt repo + federated catalog:**
- `apt.secubox.in` (today only on gk2) becomes **P2P-mirrored**: each mesh node holds a signed mirror, synced over the wg-mesh (`10.10.0.0/24`) with rsync/content-addressed deltas. A node/client installs from the **nearest reachable mirror** (local → mesh peer → upstream gk2/public), so installs are fast and **survive gk2 being offline** (access redundancy). Mirrors stay trustworthy via GPG (verify `InRelease`); never trust an unsigned peer mirror.
- The **catalog is federated**: each node advertises which package versions it holds and which services it runs; the store shows a **mesh-wide view** ("install here" vs "already running on c3box"). This is exactly the gondwana **distributed directory** (peers/services/name records) — the app store is its first big consumer.
**Multi-service agents (serve all network clients):**
- Each node runs its module services as **agents** that serve its LAN clients; the mesh makes any service reachable from any node (hub-routed today, direct later), so a client on c3box's LAN can use a service hosted on gk2 and vice-versa — **service mirroring + redundancy of access** (gondwana Phase 4).
- A thin **agent/orchestration layer** gossips health + "who-runs-what", does **failover** (a dead service → route clients to a mesh replica), and advises **placement** (install a module on the best-suited node). Clients reach services by the per-node name `<service>.<boxname>.secubox.in`, routed over the mesh.
- This makes the app store the *control plane* and the mesh agents the *data plane* of one distributed appliance, not 3 separate boxes.
## 7. Implementation plan (incremental, each shippable)
- **A — Catalog (read-only, safe):** manifest aggregator (parse 128 `secubox.yaml` + dpkg + apt state) → `GET /catalog` + `/module/{name}`. UI: CatalogGrid + filters + AppDetailDrawer (read-only). No privilege. *Delivers the store view immediately.*
- **B — Health & prefs (read + careful write):** `/health` aggregation + `/prefs` GET; PUT prefs via the CSPN double-buffer path. UI: HealthPanel + PrefsForm + Live Care board.
- **C — Lifecycle (privileged):** `secubox-appstorectl` root helper + sudoers + async job model; enable/disable first (low risk), then install/uninstall (LXC). UI: ActionBar + JobProgress + audit.
- **D — Exposure & mesh (later):** per-app public exposure toggle (HAProxy+mitmproxy, no waf_bypass); gondwana mesh mirroring/redundancy (install once, run-anywhere) tying into the Phase-2/4 mesh work.
- **E — Manifest v2 rollout:** add `appstore:` block to the headline apps first (lyrion, peertube, nextcloud, webmail/mail, kbin/gotosocial, jellyfin, photoprism), generators backfill the rest.
## 8. Open questions (to resolve in the spec / by GPT)
1. New `secubox-appstore` package vs. extend `secubox-hub`?
2. Privilege bridge: narrow sudoers vs. a root job-queue worker? (CSPN favors the auditable queue.)
3. Job/progress transport: poll `/jobs/{id}` vs. SSE/WebSocket.
4. Prefs schema source: parse `*.toml.example` comments vs. a declared JSON-schema per module.
5. Tier enforcement: hard block vs. show-and-upsell.
6. Uninstall semantics for LXC apps: keep data volume vs. purge.
---
## 9. GPT architecture-design prompt (copy-paste)
> You are a senior systems architect. Design the **"SecuBox App Store / Module Manager"** for an existing Debian-based security-appliance platform. Output a concrete architecture: data model, REST API surface, UI component tree, the manifest schema, and the install/enable/prefs/health/async-job flows — with a privilege/security model. Be specific and implementable; prefer extending what exists over inventing new systems.
>
> **Platform context (ground truth — design WITH this, not around it):**
> - SecuBox-DEB: Debian bookworm appliance. ~128 "modules", each a Debian package `secubox-<name>` from a signed apt repo (`apt.secubox.in`). `dpkg` = installed-state truth.
> - Every module already ships a manifest `debian/secubox.yaml`: `{name, category, tier, description, depends, api:{socket,health}, ui:{path}}`. Categories: media, email, ai, iot, communication, publishing, network, security, system, vpn, dashboard, misc. Tiers: lite|standard|pro|all (the board has a tier; modules above it are locked).
> - Module anatomy: a FastAPI control-plane `api/main.py` (mounted by a central **aggregator** at `/api/v1/<name>/`, served over a unix socket, running as unprivileged user `secubox`), an nginx vhost fragment, an optional `sbin/<name>ctl` CLI, an optional `lib/<name>/install-lxc.sh` that provisions the app inside an **LXC container** (9 apps today: lyrion, peertube, photoprism, jellyfin, grafana, mqtt, yacy, rustdesk, zigbee), a `conf/<name>.toml.example`, a hub menu entry `menu.d/NNN-<name>.json`, and a static dashboard `www/<name>/`. A central **hub** dashboard aggregates everything.
> - Hard constraints: the API process is unprivileged (`secubox` user, NoNewPrivileges) — it CANNOT apt-install, run lxc/systemctl, or edit /etc/nginx; privileged actions must go through a separate root path. Security model is CSPN/ANSSI-oriented: append-only audit log, double-buffer/4R for sensitive config writes (shadow→validate→atomic-swap→rollback), AppArmor, no `waf_bypass` (all public exposure routes through an HAProxy→mitmproxy WAF chain). Installs take minutes (apt + LXC debootstrap) so lifecycle ops must be asynchronous with progress.
>
> **Design deliverables:**
> 1. **Catalog/data model:** how to merge apt-available dpkg-installed into a categorized, tiered, searchable catalog; the per-module state machine (available→installed→enabled→running, plus update-available/error/tier-locked) and health model (service/LXC/socket/resources).
> 2. **Manifest schema v2:** an additive `appstore:` block (icon, summary, long_description, screenshots, lxc:bool, exposure: none|optional|required, default_ports, resources, conflicts, homepage) that keeps the 128 existing manifests valid.
> 3. **REST API:** read endpoints (`/catalog`, `/module/{name}`, `/health`, `/jobs/{id}`) and mutating endpoints (install, uninstall, enable, disable, restart, prefs GET/PUT, expose) — request/response shapes.
> 4. **Privilege bridge + async jobs:** compare (a) a narrow sudoers rule to a root `secubox-appstorectl <verb> <name>` vs (b) a root job-queue worker consuming a spool dir; pick one for CSPN auditability; define the job/progress model and the install pipeline stages (download→install→provision-lxc→enable→health-check).
> 5. **Web UI component tree:** CatalogGrid/AppCard, Category/Tier/Search/State filters, AppDetailDrawer (description, screenshots, ActionBar, schema-driven PrefsForm, HealthPanel, LogViewer, JobProgress), and a cross-module "Live Care" health board with bulk restart/repair/update-all.
> 6. **Prefs editing:** how to render a schema-driven form from `conf/<name>.toml.example` (or a declared JSON-schema), validate, and write via the double-buffer/4R path.
> 7. **Decisions:** new `secubox-appstore` package vs extend the hub; poll vs SSE for job progress; tier hard-block vs upsell; LXC uninstall data-retention.
>
> Produce: an architecture diagram (ASCII), the manifest v2 schema, the full API table, the privilege/async design, the UI component tree, and a 45 step incremental build plan where each step ships something usable. Headline apps to use as worked examples: lyrion (LXC music), peertube (LXC video), nextcloud (cloud), webmail/mail, kbin/gotosocial (fediverse).
---
## 10. GPT prompt — COMPLETE (architectural research notes) · copy-paste
> **Role.** You are a distributed-systems architect. Write **architectural research notes / a design sketch** (not production code) for the **"SecuBox App Store + Module Composer"** — a web UI and control plane to discover, install, enable/disable (at multiple granularities), configure, profile, monitor, and **federate over a P2P mesh** every module of a Debian security appliance. Prefer extending what already exists over inventing new systems. Be concrete, opinionated, and implementable; call out trade-offs and pick.
>
> **Platform ground truth (design WITH this):**
> - SecuBox-DEB: Debian bookworm appliance. ~128 "modules"; each is a Debian package `secubox-<name>` from a signed apt repo (`apt.secubox.in`); `dpkg` = installed truth.
> - Each module already ships a manifest `debian/secubox.yaml` = `{name, category, tier, description, depends, api:{socket,health}, ui:{path}}`. Categories: media, email, ai, iot, communication, publishing, network, security, system, vpn, dashboard, misc. Tiers: lite|standard|pro|all (the board has a tier; higher-tier modules are locked).
> - Module anatomy: a FastAPI control-plane `api/main.py` (mounted by a central **aggregator** at `/api/v1/<name>/` over a unix socket, running as **unprivileged user `secubox`, NoNewPrivileges**), an nginx vhost fragment, an optional `sbin/<name>ctl` CLI, an optional `lib/<name>/install-lxc.sh` that provisions the app in an **LXC** (9 LXC apps today: lyrion, peertube, photoprism, jellyfin, grafana, mqtt, yacy, rustdesk, zigbee), a `conf/<name>.toml.example`, a hub menu entry `menu.d/NNN-<name>.json`, and a static dashboard `www/<name>/`. A central **hub** aggregates everything.
> - **Hard constraints:** the API process is unprivileged — it CANNOT apt-install, run lxc/systemctl, or edit /etc/nginx; privileged actions go through a separate root path. CSPN/ANSSI security: append-only audit log; **double-buffer/4R** for sensitive config writes (shadow→validate→atomic-swap→rollback); AppArmor; **no `waf_bypass`** (all public exposure routes through an HAProxy→mitmproxy WAF chain). Installs take minutes (apt + LXC debootstrap) → lifecycle ops must be **asynchronous with progress**.
> - **Mesh ("gondwana"):** the appliance is one of several nodes (today: gk2=master/`10.10.0.1`, c3box=`.2`, amd64=`.3`) on a WireGuard mesh `10.10.0.0/24:51822` owned by module `secubox-p2p`. The mesh has node identity (wg pubkey + node-id + DDNS name `<boxname>.secubox.in`) and a planned **distributed directory** (replicated peers/services/name records, DNS-like) plus per-node service naming `<service>.<boxname>.secubox.in` routed via the hub.
>
> **Design the following (produce research notes for each):**
> 1. **Catalog & state.** Merge apt-available dpkg-installed into a categorized, tiered, searchable catalog. Per-module state machine: available→installed→enabled→running, plus update-available/error/tier-locked. Health model: service active / LXC state / socket up / last `/health` / mem-disk.
> 2. **Manifest schema v2 (additive, keeps 128 manifests valid):** an `appstore:` block (icon, summary, long_description, screenshots, `lxc:bool`, `exposure: none|optional|required`, default_ports, resources, conflicts, homepage) **and a `components:` list** declaring toggleable sub-features (mapped to the module's own config flags / sub-services).
> 3. **Granular control — four levels, each independently toggleable:** L0 module (install/enable/disable whole package+LXC); L1 component (sub-features within a running module); L2 navbar/menu (show/hide/reorder `menu.d` entries, independent of whether the module runs); L3 appearance (theme/palette/layout/branding). Define the data model + API for each.
> 4. **Profiles.** A named, versioned bundle of `{enabled modules, per-module component flags, navbar layout+order, appearance, exposure}`. Built-in presets (Minimal, Media Center, Security Ops, Home/Family, Headless/Kiosk) that can align to tier; custom create/clone/edit/export/import. **Apply = one atomic transaction via double-buffer/4R** with one-click rollback. Scope: a per-board active profile AND optional per-user profiles (master users see different navbars/apps — ties to identity). Storage + API (`/profiles`, `apply`, `rollback`, `active`).
> 5. **Lifecycle API + privilege bridge + async jobs.** REST read endpoints (`/catalog`, `/module/{name}`, `/health`, `/jobs/{id}`) and mutating ones (install, uninstall, enable, disable, restart, prefs GET/PUT, expose). The unprivileged API must reach root work: compare (a) a narrow sudoers rule to a root `secubox-appstorectl <verb> <name>` vs (b) a root **job-queue worker** consuming a spool dir; pick one for CSPN auditability. Define the job/progress model and install pipeline stages (download→install→provision-lxc→enable→health-check).
> 6. **Prefs editing.** Render a schema-driven form from `conf/<name>.toml.example` (or a declared JSON-schema); validate; write via the 4R double-buffer path.
> 7. **P2P distribution (rides the mesh).** Make `apt.secubox.in` **P2P-mirrored**: each node holds a GPG-signed mirror synced over the wg-mesh (rsync/content-addressed deltas); install source preference local→mesh-peer→upstream; **survives the master being offline**; never trust an unsigned peer mirror (verify `InRelease`). Make the **catalog federated**: each node advertises held package versions + running services (this is the distributed directory's first consumer); the UI shows a mesh-wide view ("install here" vs "running on c3box").
> 8. **Multi-service agents (serve all network clients).** Each node runs its modules as agents serving its LAN clients; the mesh makes any service reachable from any node so clients are served from the nearest node (service mirroring + access redundancy). Design a thin agent/orchestration layer: health/who-runs-what gossip, failover (dead service → route to a mesh replica), placement advice (install on the best-suited node), client access via `<service>.<boxname>.secubox.in`. App store = control plane; mesh agents = data plane of one distributed appliance.
> 9. **Web UI component tree.** CatalogGrid/AppCard; Category/Tier/Search/State filters; AppDetailDrawer (description, screenshots, ActionBar, schema-driven PrefsForm, component toggles, HealthPanel, LogViewer, JobProgress); a Profiles manager; a cross-module **Live Care** board (what's down/degraded, bulk restart/repair/update-all, mesh-wide). Palette: C3BOX (cosmos-black/gold-hermetic/cyber-cyan; Cinzel + JetBrains Mono).
> 10. **Key decisions to resolve:** new `secubox-appstore` package vs extend the hub; job progress transport (poll vs SSE/WebSocket); tier hard-block vs upsell; LXC uninstall data-retention; mesh-sync protocol for the repo and the directory.
>
> **Output format (research notes):** an ASCII architecture diagram (UI ↔ appstore API ↔ root worker ↔ apt/lxc/systemd, plus the mesh: mirror sync + federated catalog + agents); the manifest v2 + `components:` schema; the full REST API table; the profile/4R model; the P2P mirror + federated-catalog + agent protocol; the privilege/async-job design; the UI component tree; and a phased, each-step-shippable build plan. Use these as worked examples throughout: **lyrion** (LXC music), **peertube** (LXC video), **nextcloud** (cloud), **webmail/mail**, **kbin/gotosocial** (fediverse).

View File

@ -0,0 +1,167 @@
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""
SecuBox-Deb :: secubox-appstore :: catalog API (Phase A read-only)
Serves a categorized, tiered, searchable catalog of SecuBox modules by
merging the baked manifest catalog (generated at build from every module's
debian/secubox.yaml) with live runtime state (dpkg installed/version +
systemctl active). Runs unprivileged (user `secubox`): all state queries are
read-only. Install/enable/prefs/profiles are later phases (a root worker).
"""
import os
import json
import time
import subprocess
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, HTTPException
CATALOG_FILE = Path(os.environ.get(
"APPSTORE_CATALOG", "/usr/share/secubox/appstore/catalog.json"))
TIER_RANK = {"all": 0, "lite": 1, "standard": 2, "pro": 3}
_STATE_TTL = 30.0
_state_cache = {"ts": 0.0, "data": {}}
app = FastAPI(title="secubox-appstore", version="0.1.0",
root_path="/api/v1/appstore")
def board_tier() -> str:
"""Best-effort board tier; defaults to 'pro' (unlock all) when unset."""
t = os.environ.get("SECUBOX_TIER")
if t:
return t.strip()
try:
for line in open("/etc/secubox/secubox.conf", encoding="utf-8"):
s = line.strip()
if s.lower().startswith("tier") and "=" in s:
return s.split("=", 1)[1].strip().strip('"').strip("'")
except Exception:
pass
return "pro"
def load_catalog() -> list:
try:
return json.loads(CATALOG_FILE.read_text(encoding="utf-8")).get("modules", [])
except Exception:
return []
def _dpkg_state() -> dict:
out = {}
try:
r = subprocess.run(
["dpkg-query", "-W", "-f=${Package}\t${db:Status-Abbrev}\t${Version}\n", "secubox-*"],
capture_output=True, text=True, timeout=10)
for line in r.stdout.splitlines():
p = line.split("\t")
if len(p) >= 3:
out[p[0]] = {"installed": p[1].strip().startswith("ii"), "version": p[2].strip()}
except Exception:
pass
return out
def _svc_active(names: list) -> dict:
out = {}
if not names:
return out
try:
units = [f"{n}.service" for n in names]
r = subprocess.run(["systemctl", "is-active", *units],
capture_output=True, text=True, timeout=10)
for n, st in zip(names, r.stdout.splitlines()):
out[n] = (st.strip() == "active")
except Exception:
pass
return out
def compute_state(force: bool = False) -> dict:
now = time.time()
if not force and _state_cache["data"] and (now - _state_cache["ts"] < _STATE_TTL):
return _state_cache["data"]
catalog = load_catalog()
dpkg = _dpkg_state()
installed_names = [m["name"] for m in catalog if dpkg.get(m["name"], {}).get("installed")]
active = _svc_active(installed_names)
brank = TIER_RANK.get(board_tier(), 2)
result = {}
for m in catalog:
name = m["name"]
d = dpkg.get(name, {})
installed = bool(d.get("installed"))
running = bool(active.get(name))
tier = m.get("tier", "lite")
tier_locked = (tier != "all") and (TIER_RANK.get(tier, 1) > brank)
if not installed:
state = "tier-locked" if tier_locked else "available"
elif running:
state = "running"
else:
state = "installed"
result[name] = {
**m,
"installed": installed,
"running": running,
"version": d.get("version"),
"tier_locked": tier_locked,
"state": state,
}
_state_cache["ts"] = now
_state_cache["data"] = result
return result
@app.get("/health")
async def health():
return {"ok": True, "module": "appstore",
"catalog_count": len(load_catalog()), "board_tier": board_tier()}
@app.get("/categories")
async def categories():
st = compute_state()
cats: dict = {}
for m in st.values():
cats[m["category"]] = cats.get(m["category"], 0) + 1
return {
"categories": [{"name": k, "count": v} for k, v in sorted(cats.items())],
"tiers": ["lite", "standard", "pro", "all"],
"states": ["available", "installed", "running", "tier-locked"],
"board_tier": board_tier(),
}
@app.get("/catalog")
async def catalog(category: Optional[str] = None, tier: Optional[str] = None,
state: Optional[str] = None, q: Optional[str] = None):
st = compute_state()
items = list(st.values())
if category:
items = [m for m in items if m["category"] == category]
if tier:
items = [m for m in items if m["tier"] == tier]
if state:
items = [m for m in items if m["state"] == state]
if q:
ql = q.lower()
items = [m for m in items
if ql in m["name"].lower() or ql in (m.get("description") or "").lower()]
items.sort(key=lambda m: (m["category"], m["name"]))
return {"modules": items, "count": len(items), "total": len(st), "board_tier": board_tier()}
@app.get("/module/{name}")
async def module(name: str):
st = compute_state()
if name not in st:
alt = f"secubox-{name}"
if alt in st:
name = alt
else:
raise HTTPException(status_code=404, detail=f"unknown module {name!r}")
return dict(st[name])

View File

@ -0,0 +1,20 @@
secubox-appstore (0.1.1-1~bookworm1) bookworm; urgency=medium
* fix(nginx): serve the catalog API from secubox-routes.d/ (the
authoritative /api/v1/ include) instead of secubox.d/, so it wins over
the generic /api/ -> aggregator catch-all. UI stays in secubox.d/.
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 18:00:00 +0200
secubox-appstore (0.1.0-1~bookworm1) bookworm; urgency=medium
* Initial release — App Store Phase A (read-only catalog).
- api/main.py: GET /catalog (category/tier/state/q filters), /module/{name},
/categories, /health; merges the baked manifest catalog with live dpkg +
systemctl state (available / installed / running / tier-locked).
- catalog.json generated at build from every module's debian/secubox.yaml.
- www/appstore: categorized, tiered, searchable grid UI (read-only).
- Served by secubox-appstore.service on /run/secubox/appstore.sock; nginx
routes /api/v1/appstore/ + static /appstore/. No aggregator dependency.
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 17:30:00 +0200

View File

@ -0,0 +1,15 @@
Source: secubox-appstore
Section: admin
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13), python3
Standards-Version: 4.6.2
Package: secubox-appstore
Architecture: all
Depends: ${misc:Depends}, secubox-core (>= 1.0), python3, python3-fastapi | python3-pip, python3-uvicorn | python3-pip
Description: SecuBox App Store — module catalog & lifecycle (Phase A)
Categorized, tiered, searchable catalog of SecuBox modules with live
install/run state, served as a hub web UI. Phase A is read-only; install,
enable/disable, preferences and profiles arrive in later phases via a
privileged worker.

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
configure)
install -d -m 0755 /usr/share/secubox/appstore /var/log/secubox /var/lib/secubox 2>/dev/null || true
systemctl daemon-reload 2>/dev/null || true
systemctl enable --now secubox-appstore.service 2>/dev/null || true
if systemctl is-active --quiet nginx 2>/dev/null; then
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
fi
;;
esac
#DEBHELPER#
exit 0

View File

@ -0,0 +1,9 @@
#!/bin/sh
set -e
case "$1" in
remove|deconfigure)
systemctl stop secubox-appstore.service 2>/dev/null || true
;;
esac
#DEBHELPER#
exit 0

View File

@ -0,0 +1,37 @@
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_install:
# API module (uvicorn api.main:app)
install -d $(CURDIR)/debian/secubox-appstore/usr/lib/secubox/appstore/api
cp -r $(CURDIR)/api/* $(CURDIR)/debian/secubox-appstore/usr/lib/secubox/appstore/api/
# Web UI
install -d $(CURDIR)/debian/secubox-appstore/usr/share/secubox/www/appstore
cp -r $(CURDIR)/www/appstore/* $(CURDIR)/debian/secubox-appstore/usr/share/secubox/www/appstore/
# nginx route + static
install -d $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox.d
install -m 644 $(CURDIR)/nginx/appstore.conf $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox.d/
# Hub menu entry
install -d $(CURDIR)/debian/secubox-appstore/etc/secubox/menu.d
install -m 644 $(CURDIR)/menu.d/580-appstore.json $(CURDIR)/debian/secubox-appstore/etc/secubox/menu.d/
# systemd service
install -d $(CURDIR)/debian/secubox-appstore/usr/lib/systemd/system
install -m 644 $(CURDIR)/debian/secubox-appstore.service $(CURDIR)/debian/secubox-appstore/usr/lib/systemd/system/
# Authoritative API route (secubox-routes.d)
install -d $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox-routes.d
install -m 644 $(CURDIR)/nginx/appstore-routes.conf $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox-routes.d/
# Generate the catalog index from every sibling module's debian/secubox.yaml
install -d $(CURDIR)/debian/secubox-appstore/usr/share/secubox/appstore
python3 $(CURDIR)/scripts/gen-appstore-catalog.py \
$(CURDIR)/debian/secubox-appstore/usr/share/secubox/appstore/catalog.json
# Runtime dir
install -d $(CURDIR)/debian/secubox-appstore/run/secubox

View File

@ -0,0 +1,25 @@
[Unit]
Description=SecuBox App Store — module catalog API
After=network.target secubox-core.service
Requires=secubox-core.service
[Service]
Type=simple
User=secubox
Group=secubox
RuntimeDirectory=secubox
RuntimeDirectoryPreserve=yes
ExecStart=/usr/bin/python3 -m uvicorn api.main:app --uds /run/secubox/appstore.sock --workers 1
WorkingDirectory=/usr/lib/secubox/appstore
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
NoNewPrivileges=yes
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/run/secubox /var/log/secubox /var/lib/secubox
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,15 @@
# debian/secubox.yaml
name: secubox-appstore
category: system
tier: all
description: "SecuBox App Store — module catalog, install/enable, profiles"
depends:
- secubox-core
api:
socket: /run/secubox/appstore.sock
health: /api/v1/appstore/health
ui:
path: /appstore/

View File

@ -0,0 +1,10 @@
{
"id": "appstore",
"name": "App Store",
"icon": "🛍️",
"path": "/appstore/",
"category": "system",
"order": 580,
"description": "Install, enable & configure SecuBox modules",
"requires": ["secubox-appstore"]
}

View File

@ -0,0 +1,9 @@
# /etc/nginx/secubox-routes.d/appstore.conf — Installed by secubox-appstore.
# App Store catalog API, served by the standalone secubox-appstore.service
# over /run/secubox/appstore.sock (no aggregator dependency).
location /api/v1/appstore/ {
rewrite ^/api/v1/appstore/(.*)$ /$1 break;
proxy_pass http://unix:/run/secubox/appstore.sock;
include /etc/nginx/snippets/secubox-proxy.conf;
proxy_intercept_errors on;
}

View File

@ -0,0 +1,9 @@
# /etc/nginx/secubox.d/appstore.conf — Installed by secubox-appstore.
# Static App Store UI. The /api/v1/appstore/ route lives in
# secubox-routes.d/appstore.conf (the authoritative API include).
location /appstore/ {
alias /usr/share/secubox/www/appstore/;
index index.html;
try_files $uri $uri/ /appstore/index.html;
}

View File

@ -0,0 +1,63 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
"""SecuBox-Deb :: secubox-appstore :: catalog generator.
Scan every module's debian/secubox.yaml in the monorepo and emit a flat
catalog.json (the App Store's "available" set). Run at package build time
from packages/secubox-appstore (siblings live at ../*/debian/secubox.yaml).
Dependency-free: parses the small flat manifests without PyYAML.
"""
import json, sys, glob, os, re
def parse_manifest(path):
m = {"depends": []}
in_depends = False
for raw in open(path, encoding="utf-8", errors="replace"):
line = raw.rstrip("\n")
if not line.strip() or line.lstrip().startswith("#"):
continue
# top-level key: value (no leading space)
mt = re.match(r"^([a-z_]+):\s*(.*)$", line)
if mt:
key, val = mt.group(1), mt.group(2).strip()
in_depends = (key == "depends")
if key in ("name", "category", "tier", "description") and val:
m[key] = val.strip().strip('"').strip("'")
continue
# list item under depends:
li = re.match(r"^\s+-\s+(.*)$", line)
if li and in_depends:
m["depends"].append(li.group(1).strip().strip('"').strip("'"))
return m
def main(out):
base = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..")
catalog = []
for path in sorted(glob.glob(os.path.join(base, "*", "debian", "secubox.yaml"))):
# skip generated build-tree copies (debian/<pkg>/...)
if "/debian/" in path and path.count("/debian/") > 1:
continue
m = parse_manifest(path)
if not m.get("name"):
continue
catalog.append({
"name": m["name"],
"category": m.get("category", "misc"),
"tier": m.get("tier", "lite"),
"description": m.get("description", ""),
"depends": m.get("depends", []),
})
# de-dup by name (keep first)
seen, uniq = set(), []
for c in catalog:
if c["name"] in seen:
continue
seen.add(c["name"]); uniq.append(c)
data = {"version": 1, "modules": uniq, "count": len(uniq)}
with open(out, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"wrote {out}: {len(uniq)} modules")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "catalog.json")

View File

@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SecuBox · App Store</title>
<link rel="stylesheet" href="/shared/design-tokens.css">
<link rel="stylesheet" href="/shared/crt-light.css">
<style>
:root{
--bg:#0a0a0f; --panel:#13131c; --line:#2a2a3a; --gold:#c9a84c;
--cyan:#00d4ff; --green:#00ff41; --muted:#6b6b7a; --text:#e8e6d9; --cinnabar:#e63946;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);
font-family:'JetBrains Mono',ui-monospace,monospace;}
header{padding:18px 24px;border-bottom:1px solid var(--line);
display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;}
header h1{font-family:'Cinzel',serif;color:var(--gold);margin:0;font-size:22px;letter-spacing:1px;}
header .sub{color:var(--muted);font-size:13px}
.bar{display:flex;gap:10px;flex-wrap:wrap;padding:14px 24px;border-bottom:1px solid var(--line);align-items:center}
.bar input,.bar select{background:var(--panel);border:1px solid var(--line);color:var(--text);
padding:8px 10px;border-radius:6px;font-family:inherit;font-size:13px}
.bar input{min-width:220px}
.stat{color:var(--muted);font-size:12px;margin-left:auto}
.stat b{color:var(--cyan)}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;padding:24px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px;
display:flex;flex-direction:column;gap:8px;transition:border-color .15s}
.card:hover{border-color:var(--gold)}
.card .top{display:flex;align-items:center;gap:10px}
.card .icon{font-size:26px;line-height:1}
.card .name{font-weight:700;color:var(--text);font-size:14px;word-break:break-word}
.card .desc{color:var(--muted);font-size:12px;min-height:32px;line-height:1.4}
.badges{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
.pill{font-size:10px;padding:2px 8px;border-radius:20px;border:1px solid var(--line);text-transform:uppercase;letter-spacing:.5px}
.pill.cat{color:var(--cyan);border-color:#1d4a55}
.pill.tier{color:var(--gold);border-color:#5a4a1d}
.pill.state-running{color:var(--green);border-color:#1d5a2a;background:rgba(0,255,65,.08)}
.pill.state-installed{color:var(--cyan);border-color:#1d4a55}
.pill.state-available{color:var(--muted)}
.pill.state-tier-locked{color:var(--cinnabar);border-color:#5a1d23}
.row{display:flex;justify-content:space-between;align-items:center;margin-top:auto}
.btn{background:transparent;border:1px solid var(--line);color:var(--muted);
padding:6px 12px;border-radius:6px;font-family:inherit;font-size:12px;cursor:not-allowed}
.ver{color:var(--muted);font-size:11px}
.empty{color:var(--muted);padding:40px;text-align:center;grid-column:1/-1}
</style>
</head>
<body>
<header>
<h1>⬢ SecuBox App Store</h1>
<span class="sub">module catalog · install / enable / configure — <em>Phase A: read-only</em></span>
</header>
<div class="bar">
<input id="q" type="search" placeholder="Search modules…" oninput="render()">
<select id="category" onchange="render()"><option value="">All categories</option></select>
<select id="tier" onchange="render()">
<option value="">All tiers</option><option>lite</option><option>standard</option><option>pro</option><option>all</option>
</select>
<select id="state" onchange="render()">
<option value="">All states</option><option value="running">running</option>
<option value="installed">installed</option><option value="available">available</option>
<option value="tier-locked">tier-locked</option>
</select>
<span class="stat" id="stat">loading…</span>
</div>
<div class="grid" id="grid"><div class="empty">Loading catalog…</div></div>
<script>
const API = '/api/v1/appstore';
const ICONS = {media:'🎬',email:'✉️',ai:'🧠',iot:'🛰️',communication:'💬',publishing:'📰',
network:'🌐',security:'🛡️',system:'⚙️',vpn:'🔒',dashboard:'📊',monitoring:'📈',misc:'🧩'};
let ALL = [];
async function getJSON(p){ try{const r=await fetch(API+p);if(!r.ok)throw 0;return await r.json();}catch(e){return null;} }
function esc(s){const d=document.createElement('div');d.textContent=s==null?'':s;return d.innerHTML;}
async function load(){
const cats = await getJSON('/categories');
if(cats && cats.categories){
const sel=document.getElementById('category');
cats.categories.forEach(c=>{const o=document.createElement('option');o.value=c.name;
o.textContent=`${c.name} (${c.count})`;sel.appendChild(o);});
}
const data = await getJSON('/catalog');
ALL = (data && Array.isArray(data.modules)) ? data.modules : [];
render(cats && cats.board_tier);
}
function render(boardTier){
const q=document.getElementById('q').value.toLowerCase();
const cat=document.getElementById('category').value;
const tier=document.getElementById('tier').value;
const state=document.getElementById('state').value;
let items = ALL.filter(m=>{
if(cat && m.category!==cat) return false;
if(tier && m.tier!==tier) return false;
if(state && m.state!==state) return false;
if(q && !(m.name.toLowerCase().includes(q) || (m.description||'').toLowerCase().includes(q))) return false;
return true;
});
const grid=document.getElementById('grid');
const running = ALL.filter(m=>m.state==='running').length;
const installed = ALL.filter(m=>m.installed).length;
document.getElementById('stat').innerHTML =
`showing <b>${items.length}</b> / ${ALL.length} · installed <b>${installed}</b> · running <b>${running}</b>`;
if(!items.length){ grid.innerHTML='<div class="empty">No modules match.</div>'; return; }
grid.innerHTML = items.map(m=>{
const icon = ICONS[m.category]||ICONS.misc;
const label = m.name.replace(/^secubox-/,'');
const st = m.state||'available';
const btn = st==='running'?'Running' : st==='installed'?'Installed' :
st==='tier-locked'?'Tier locked' : 'Install';
return `<div class="card">
<div class="top"><span class="icon">${icon}</span><span class="name">${esc(label)}</span></div>
<div class="desc">${esc(m.description||'')}</div>
<div class="badges">
<span class="pill cat">${esc(m.category)}</span>
<span class="pill tier">${esc(m.tier)}</span>
<span class="pill state-${st}">${st}</span>
</div>
<div class="row">
<span class="ver">${m.version?('v'+esc(m.version)):''}</span>
<button class="btn" title="Lifecycle actions arrive in Phase C" disabled>${btn}</button>
</div>
</div>`;
}).join('');
}
document.addEventListener('DOMContentLoaded', load);
</script>
</body>
</html>

View File

@ -0,0 +1,15 @@
# debian/secubox.yaml
name: secubox-lyrion
category: media
tier: lite
description: "Lyrion Music Server (LMS / Squeezebox) in a Debian LXC"
depends:
- secubox-core
api:
socket: /run/secubox/aggregator.sock
health: /api/v1/lyrion/health
ui:
path: /lyrion/