mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 21:38:35 +00:00
Compare commits
4 Commits
a1ec2601c8
...
8f46bcb93b
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f46bcb93b | |||
| 58f1f1a2c8 | |||
| 66301f4307 | |||
| a9f349a57d |
|
|
@ -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 4–5 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).
|
||||
0
packages/secubox-appstore/api/__init__.py
Normal file
0
packages/secubox-appstore/api/__init__.py
Normal file
167
packages/secubox-appstore/api/main.py
Normal file
167
packages/secubox-appstore/api/main.py
Normal 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])
|
||||
20
packages/secubox-appstore/debian/changelog
Normal file
20
packages/secubox-appstore/debian/changelog
Normal 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
|
||||
15
packages/secubox-appstore/debian/control
Normal file
15
packages/secubox-appstore/debian/control
Normal 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.
|
||||
14
packages/secubox-appstore/debian/postinst
Executable file
14
packages/secubox-appstore/debian/postinst
Executable 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
|
||||
9
packages/secubox-appstore/debian/prerm
Executable file
9
packages/secubox-appstore/debian/prerm
Executable 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
|
||||
37
packages/secubox-appstore/debian/rules
Executable file
37
packages/secubox-appstore/debian/rules
Executable 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
|
||||
25
packages/secubox-appstore/debian/secubox-appstore.service
Normal file
25
packages/secubox-appstore/debian/secubox-appstore.service
Normal 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
|
||||
15
packages/secubox-appstore/debian/secubox.yaml
Normal file
15
packages/secubox-appstore/debian/secubox.yaml
Normal 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/
|
||||
10
packages/secubox-appstore/menu.d/580-appstore.json
Normal file
10
packages/secubox-appstore/menu.d/580-appstore.json
Normal 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"]
|
||||
}
|
||||
9
packages/secubox-appstore/nginx/appstore-routes.conf
Normal file
9
packages/secubox-appstore/nginx/appstore-routes.conf
Normal 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;
|
||||
}
|
||||
9
packages/secubox-appstore/nginx/appstore.conf
Normal file
9
packages/secubox-appstore/nginx/appstore.conf
Normal 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;
|
||||
}
|
||||
63
packages/secubox-appstore/scripts/gen-appstore-catalog.py
Executable file
63
packages/secubox-appstore/scripts/gen-appstore-catalog.py
Executable 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")
|
||||
136
packages/secubox-appstore/www/appstore/index.html
Normal file
136
packages/secubox-appstore/www/appstore/index.html
Normal 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>
|
||||
15
packages/secubox-lyrion/debian/secubox.yaml
Normal file
15
packages/secubox-lyrion/debian/secubox.yaml
Normal 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/
|
||||
Loading…
Reference in New Issue
Block a user