mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
Merge branch 'feature/appstore-phase-a' — App Store Phase A (read-only catalog)
Some checks are pending
License Headers / check (push) Waiting to run
Some checks are pending
License Headers / check (push) Waiting to run
This commit is contained in:
commit
8f46bcb93b
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