Commit Graph

2750 Commits

Author SHA1 Message Date
f9302df075 feat(billets): emoji hashtags, quick views, and feed resumes
Authors already typed #hashtags in their bodies (37 of the 46 billets on gk2
carry one), so tags are EXTRACTED from the body rather than added via a picker:
tagging costs the author nothing and applies retroactively.

- migration 0004: tag(slug,emoji,label) + billet_tag, indexed on tag_slug.
  Extracted into rows rather than re-parsed per request, so filtering hits an
  index instead of LIKE-scanning every body. slug is accent-folded + lowercased
  (#Réseau == #reseau); emoji is STORED so a later curation change never
  silently restyles an already-published billet.
- services/tags.py: curated map + extraction. The lookbehind keeps a URL anchor
  (page#anchor) from becoming a tag; unknown tags fall back to a neutral badge
  rather than being rejected. Map covers this box's real content (podcast/media)
  as well as the security/box vocabulary; genuinely ambiguous topics are left on
  the default rather than editorialised.
- quick views: ?tag=slug on / and /feed.json, plus /tags.json and
  /admin/api/tags for the chip bar. Uses EXISTS, not a JOIN, so keyset paging
  cannot duplicate a row per matching tag; the pager carries the active tag.
- feed resumes: long billets show a short excerpt + "Lire la suite"; short ones
  render whole. `is_long` is measured on the markdown-collapsed text, so a short
  billet padded with link syntax is not "resumed" into a copy of itself.
- feed.json gains `summary` + `tags` (both JSON Feed 1.1 standard) with the
  emoji in the spec-legal `_secubox` extension; optional keys are omitted, not
  nulled.
- GET /admin/api/billets: the authoring list. The public feed cannot serve it —
  its item id is a permalink URL (so edit/delete could not address a row, which
  is why they 404'd) and it hides drafts, which an authoring client must see.
- manage.py backfill-tags: idempotent; also re-applies the curated map to
  existing tags as an explicit operator step.

Verified on gk2: backfill tagged 37/46 billets; ?tag=podcast→37, jfk→4, nope→0;
a draft's tag correctly stays out of the public chip bar; delete cascades.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:35:24 +02:00
b50387a06b fix(auth): record client IP + user-agent on the MFA login paths
/login/mfa and /totp/confirm both hardcoded "ip": "" and omitted user_agent
entirely, while only the password path captured them. admin is forced-TOTP,
so EVERY real admin login took the MFA path — every session row landed
unauditable (who logged in, from where, with what), and the users webui
sessions tab rendered blanks for data it was already asking for.

Factor the extraction into _client_meta(request) (X-Forwarded-For first: nginx
and HAProxy front every login, so request.client.host is only ever the proxy)
and use it on all three paths.

Verified by driving the real /login/mfa handler in-process with
SECUBOX_AUTH_SESSIONS pointed at a temp file: the session row now records
ip=192.168.1.77 (the first XFF hop, not the proxy) and the full user-agent.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 10:35:07 +02:00
bf97dc94c7 fix(companion): SW network-first bypasses HTTP cache (cache: reload)
Some checks are pending
License Headers / check (push) Waiting to run
A plain fetch(req) in the network-first handler still consults the browser
HTTP cache, which heuristically caches module JS (no Cache-Control on .js).
That let an old view.js survive a deploy — the recurring 'still broken after
hard refresh'. Refetch with cache:'reload' so every online load pulls fresh
app code; offline still falls back to the Cache API. Bump v7 -> v8.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:56:04 +02:00
ce31b59915 Merge fix/companion-billets-feed: billets JWT admin + Companion authoring/upload
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:51:29 +02:00
9792e0ad81 docs: track billets JWT admin + Companion authoring/upload
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:49:15 +02:00
db7fc2449e feat(billets): JWT admin surface + Companion authoring & image upload
Add packages/secubox-billets/api/routes/jwt_admin.py — a JWT-authed JSON
admin surface (secubox_core.auth.require_jwt: Bearer OR secubox_session
cookie) that reuses the same repo/media layer as the session HTML admin.
Endpoints: POST/PUT/DELETE /admin/api/billets, POST
/admin/api/billets/{id}/media (multipart, EXIF-strip via services.media),
GET /admin/api/comments, approve/delete comment moderation. No-op if
secubox_core is absent (isolated unit tests, session admin unaffected).

Wire it into api/main.py (register_jwt_admin after register_admin).

postinst: drop a sorted-last zz-secubox-system.pth into the venv so the
isolated billets venv can import the .deb-managed secubox_core without
shadowing its own fastapi/pydantic wheels.

Companion billets module: point EP map at /feed.json + /admin/api/*, add
an image file input to the editor and upload it after the billet is
created (POST /admin/api/billets/{id}/media, multipart). Fixes the "New
billet missing upload" gap. Bump service worker v6 -> v7.

Verified live on gk2: create -> feed, valid PNG upload -> {url,thumb},
delete — all 200.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:48:12 +02:00
b928ab71f7 fix(companion): bump SW to v6 so the billets feed.json fix reaches stale clients
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:27:26 +02:00
6afd155d69 fix(companion): billets module reads the real feed (/feed.json), not /feed
The billets module called `/feed` (404) — billets serves a JSON Feed at
/feed.json — and read `published_at` where JSON Feed uses `date_published`;
status_endpoint was /health (billets serves /healthz). Fixed all three, so the
#/m/billets list loads (30 items) instead of {"detail":"Not Found"}. Writes
(new billet, comment moderation) remain unavailable — billets exposes only GET
routes (its admin is the session-based web UI); those tabs already degrade
gracefully. A JWT write API for billets is a separate feature.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 07:24:20 +02:00
60f059ac62 docs: WIP/HISTORY for 2026-07-17 WAF (webui/autoban/analysis) + Nextcloud rework (PR #866, #867)
Some checks are pending
License Headers / check (push) Waiting to run
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 06:30:27 +02:00
CyberMind
116aca49ab
Merge pull request #867 from CyberMind-FR/fix/nextcloud-webui-status
fix(nextcloud): working status probe + cyan webui reskin
2026-07-17 06:29:01 +02:00
CyberMind
1c9171a4f0
Merge pull request #866 from CyberMind-FR/fix/waf-autoban-cscli-bridge
fix(waf): sbxwaf auto-bans reach the firewall (cscli bridge + dedup)
2026-07-17 06:28:56 +02:00
7e926eeb7f fix(nextcloud): working status probe + cyan webui reskin
The API runs unprivileged (secubox) but probed the container via lxc-info/occ
WITHOUT sudo and with lxc_path=/srv/lxc (container is /data/lxc) → status always
'Stopped', cloud.local domain, empty version/users/disk (and a 500 once the path
was corrected, from a PermissionError on /data/lxc). Now: privilege-free TCP port
probe for liveness, all container ops via 'sudo nextcloudctl' (the sole sudoers
surface, incl. its occ passthrough), a daemon-thread 60s cache for the slow occ
fields (version/users/storage) so /status stays fast, real data_path, and a
public web_url from the domain. Webui restyled to the /certs/ cyan hybrid-skin
(361 lines, emoji cards, live pulse, 30s refresh); every endpoint preserved.

Needs (board drift): NoNewPrivileges=false drop-in (sudo was blocked) and
[nextcloud] domain=nc.gk2.secubox.in in secubox.conf.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-17 05:00:37 +02:00
a9ab57b5e7 fix(waf): make sbxwaf auto-bans reach the firewall (cscli bridge + dedup)
The CrowdSec bridge was silently OFF: the unit passes --crowdsec-url but no
--crowdsec-jwt-file, so the wiring hit the 'disabled' branch and srv.crowdsec
stayed nil → Report() was never called. 198K threats detected, ~0 auto-bans
enforced (the 36k-hit offender was never nft-dropped). The LAPI /v1/alerts path
was doubly broken anyway: JWTs expire hourly and our alert schema 500s on this
build.

Add CscliReporter — the proven path the dashboard's manual ban uses
('cscli decisions add', which creates a real bouncer-enforced nft drop). It
engages when --crowdsec-url is set without a JWT file (new --crowdsec-cscli
flag, default 'cscli'). Per-IP 5-min dedup collapses the storm from the
graduated ban firing Report on every banned request (a rapid attacker otherwise
spawned dozens of concurrent cscli procs that contended until killed); on cscli
failure the IP is un-recorded so the next hit retries. Verified live: 5 honeypot
hits from a test IP → exactly 1 decision, no storm.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 16:28:34 +02:00
8617ba2083 feat(waf): rework sbxwaf dashboard to cyan hybrid-skin look
Some checks are pending
License Headers / check (push) Waiting to run
Restyle-only rework of the WAF webui to the guidelined /certs/ cyan hybrid-skin
(shared hybrid-skin.css, body.hybrid-dark, emoji cards, live pulse dot, reactive
30s/10s refresh), dropping the old crt-light/crt-engine/sidebar-light includes.
Every loader/action + its endpoint/method/payload preserved verbatim
(alerts, bans, categories, stats, status, category toggle, bans/history, visits,
mode, bans/clear, export, ban, unban). 1354 → 1028 lines.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 15:42:15 +02:00
3e2a359410 docs: WIP/HISTORY for 2026-07-16 mail/auth/dpi session (PR #865, #862-#864)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 15:36:20 +02:00
CyberMind
1269fd303c
Merge pull request #865 from CyberMind-FR/fix/mail-dpi-auth-webui-20260716
fix: mail (auth/domain/status/reskin/password) + DPI (exfil/regen) + shared auth cookie-fallback + openclaw CT
2026-07-16 15:34:43 +02:00
a96c3b1c4a fix(mail): /user/password writes the real dovecot passwd-file (admin reset was a no-op)
The endpoint shelled to 'mailctl user passwd', which writes a host-side copy the
LXC never reads, so admin password resets silently didn't take (login kept
failing). Now generates the SHA512-CRYPT hash via the container's doveadm and
updates DATA_PATH/config/users directly (the bind-mounted file dovecot reads),
normalising any legacy 2-field line to the full 8-field record.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 12:00:41 +02:00
0ebce730d6 fix(openclaw): certSpotter fallback for CT lookup + whois registrable domain
crt.sh is frequently down (502/404 HTML) and its raw error leaked into scan
results. Now queries certSpotter first (reliable JSON), falls back to crt.sh,
then a clean message. whois now resolves the registrable domain for sub-domains.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00
8a01d5f631 feat(dpi): /exfil serves cumulative R3 devices + engine liveness; webui regen on real engine
/exfil read the idle live-window state.json (empty when the wg-toolbox tunnel is
idle) so the dashboard showed no devices; now serves the cumulative rollup (real
devices) + overlays live active_flows + an engine-liveness block. Service control
repointed from dormant netifyd to the real Go collector (secubox-dpi-flowcap) via
a scoped sudoers grant (postinst). Webui regenerated in the cyan hybrid-skin look,
framed on the R3 engine, netifyd/mirred/block-rules cruft removed; the XSS-safe
media-buffer rendering is preserved verbatim.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00
83c8094141 fix(mail): live TCP-probe status + certs-cyan webui reskin
Status/components/ports now use direct TCP probes instead of lxc-info (which
read the wrong lxc_path and reported everything Stopped); storage reads vmail.
Webui rebuilt in the /certs/ hybrid-skin look: emoji stat-cards, 15s live
pulse, External-mailboxes tab, scanline engine dropped; all endpoints preserved.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00
5c46ad864a fix(auth): require_jwt tries Bearer then cookie so a stale token can't shadow a live session
A present-but-stale localStorage Bearer was used exclusively, ignoring a valid
secubox_session cookie, so every webui 401'd into a /login.html loop. Now both
sources are tried (Bearer first, then cookie); 401 only if neither validates.
Strictly more permissive — no regression for working Bearer clients.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-16 10:12:32 +02:00
c967c6ba65 fix(companion): network-first service worker — deploys take effect immediately
Some checks are pending
License Headers / check (push) Waiting to run
The stale-while-revalidate SW served cached core/*.js first, so every fix needed
2+ reloads (and left the old registry.js resolving module imports to /core/…).
Switch to network-first (fresh when online, cache only offline). SW→v5.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 18:25:34 +02:00
84514dcaca fix(companion): full password+TOTP login via canonical auth.sock — definitively working
Some checks are pending
License Headers / check (push) Waiting to run
Root cause of the module 401s: the companion proxied /api/v1/auth to the
AGGREGATOR, whose minted token's jti is never written to sessions.json, so
require_jwt rejected it. Route /api/v1/auth to auth.sock (canonical, session-
recorded). Implement the real two-step login like login.html: password →
access_token, or mfa_required → prompt TOTP code → /auth/login/mfa → access_token.
Capture the token as Bearer (+ the SSO cookie login sets). Ship the nginx vhost
in deploy/. SW→v4.

Proven end-to-end (temp viewer user): login returns access_token + sets
secubox_session; podcaster/wireguard/system → 200 via both Bearer and cookie.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 16:40:45 +02:00
ecd32dde65 fix(companion): resolve module view.js import against the page, not /core/ (fixes 'invalid MIME')
Dynamic import() resolves relative to registry.js (/core/), so import('./modules/x/view.js')
hit /core/modules/… → 404 → index.html fallback (HTML) → browser rejected the module
('not a valid JavaScript MIME'). Make m._path an absolute URL via new URL(path, document.baseURI).
SW→v3 to ship past the cached shell.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 16:26:38 +02:00
451270c1ee fix(companion): cookie/SSO auth — box uses parent-domain secubox_session cookie, not a body token
secubox-auth login is cookie-based (#400 SSO-lite: require_jwt accepts the
parent-domain secubox_session cookie OR a Bearer token). Switch the client to
credentials:'include' (send the cookie) and stop treating 'no token in body' as
an error; keep an optional bearer if a build returns one. Pre-fill the box URL
with the same-origin (the companion vhost proxies /api/v1 to the box, so no
CORS). Bump SW cache to v2 to ship the fix past the cached shell.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 16:20:49 +02:00
99feea1057 Merge: vhost webui reskin to certs look + read certs from HAProxy PEM store off-loop (#858)
Some checks are pending
License Headers / check (push) Waiting to run
2026-07-15 16:01:45 +02:00
11b1d71f90 Merge: podcaster downloads/upload/SSD/socket + webui reskin + cover + apple resolver + add-from-URL (#853 #855 #857) 2026-07-15 16:01:26 +02:00
129c482b9b Merge: SecuBox Companion — remote modular PWA/Capacitor app (#859, PR #860) 2026-07-15 16:01:26 +02:00
c4e4b784b6 feat(companion): self-host charte fonts (Space Grotesk + JetBrains Mono woff2, latin, offline)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 12:15:29 +02:00
597446ed29 feat(companion): add auth, waf, system, exposure, wireguard modules — full AUTH→MESH spectrum
Proves the drop-a-folder flow (core untouched) and gives the dashboard one card
per canonical group. Reads use known box routes; a few write/detail routes are
marked TODO(api).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 12:04:21 +02:00
c6457d8009 feat(companion): capacitor config + build scripts + new-module doc
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 11:59:07 +02:00
43af973732 feat(companion): billets + podcasteur reference modules + _template
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 11:59:07 +02:00
a440336165 feat(companion): buildless PWA core — shell, API client, registry, auth, store, SW, charte
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 11:59:07 +02:00
77dc21933a feat(vhost): remove the Actions tab from the webui (ref #858)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 09:01:40 +02:00
3d8207816e fix(vhost): parse certs in-process (cryptography) off-loop + cache — no openssl storm
The /certificates handler shelled out to openssl once per PEM (84 blocking
subprocesses) inside an async handler on the shared aggregator loop → froze the
board when the certs tab was viewed. Now: parse in-process via cryptography
(84 certs in 0.1s vs 12s), run via asyncio.to_thread, and cache 60s. Reads the
real HAProxy PEM store (/data/haproxy/certs), matching the certs module.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 07:33:07 +02:00
e66d3e5066 fix(vhost): read certs from /data/haproxy/certs PEM store, not empty /etc/acme (ref #858)
The cert list + cert_count read the legacy acme.sh /etc/acme layout (empty on
this platform) → 0 certs shown. Point them at the combined HAProxy PEM store
(/data/haproxy/certs/*.pem) — the same source the certs module uses. Wildcard
filenames (_wildcard_.x.pem) map back to *.x.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 07:15:47 +02:00
83d2da96ea fix(vhost): reskin webui to certs/WebUI-Panel-Guidelines look + enrich certificate metrics (closes #858)
- single clean stylesheet on /shared/hybrid-skin.css + Courier Prime + certs
  palette/component vocab (removed crt-light/sidebar-light/hybrid-dark + the
  duplicate conflicting .btn/.card/.stat-card blocks)
- Certificates tab: parse notAfter -> days-left, colour rows by status
  (expired/critical/warning/healthy), status-count stat grid; add esc() helper
- all tabs/modals/actions + JS element IDs preserved

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-15 07:13:48 +02:00
1e3772b6ec feat(podcaster): 'Add from YouTube/URL' button — full mirror (podcaster + PeerTube + billets) (closes #857)
Productises the one-off importer into the module:
- api/importer.py: yt-dlp URL importer (playlist or single video, or any yt-dlp
  site). Per item: download video<=720p + extract audio, optional PeerTube upload
  (unlisted, creds from root-only /etc/secubox/secrets/peertube-import.json),
  podcaster episode (local audio, done), optional published billet (description +
  source + both stream links). Off-loop via asyncio.to_thread; single job with
  live JOB progress. Optional yt-cookies.txt to defeat YouTube's bot throttle;
  yt-dlp errors surfaced in the progress log.
- endpoints: POST /import/url (guarded), GET /import/status.
- webui: ' Add from URL' button + modal (URL + PeerTube/billets toggles) +
  live progress (current, done/total, bar, errors, log tail).
- Recommends: yt-dlp, ffmpeg.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 11:41:11 +02:00
dae1991309 feat(podcaster): resolve Apple Podcasts links to their RSS feed (ref #856)
Detect podcasts.apple.com / itunes.apple.com links, extract the numeric id, and
resolve the underlying feedUrl via the iTunes Lookup API before ingesting as a
normal RSS feed. Best-effort: falls back to the original URL on any error.
Declare python3-mutagen + python3-pil (used by the #855 cover extractor).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 09:57:28 +02:00
e818f13cf2 feat(podcaster): extract embedded cover art + display cover (now-playing on portal) (ref #855)
- backend: _extract_cover pulls ID3 APIC / MP4 covr / FLAC picture from the first
  audiobook track via mutagen, downscales with Pillow, stores <feed>/cover.jpg,
  sets feed image to /feeds/<fid>/cover; new public cover endpoint. Best-effort
  (never breaks an upload).
- webui: cover thumbnails on episode cards/rows with graceful emoji fallback on
  missing/404; portal gets a 'Now Playing' cover bar driven by a capture-phase
  play listener reading data-* (does NOT rebuild the list → playback guard intact).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 09:55:29 +02:00
35a125fc1f Merge branch 'fix/853-podcaster-downloads-wedge-with-no-timeou' into fix/855-podcaster-webui-episode-ordering-by-type
# Conflicts:
#	packages/secubox-podcaster/www/podcaster/index.html
2026-07-14 09:47:25 +02:00
ed9d679aba feat(podcaster): order episodes by type + feed-first admin + reskin admin/portal + portal playback fix (ref #855)
- store: audiobook feeds order ASC (chapter 1→last), podcasts DESC (latest first)
- admin webui: require selecting a container before showing episodes; reskin to
  WebUI Panel Guidelines (Courier Prime, hybrid-skin.css, certs .btn/.card/.stat-card)
- portal: fix audio stopping every 15s (guard the innerHTML re-render vs playing
  audio + unchanged signature); funky/emoji/modern reskin

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 09:47:10 +02:00
56d3962309 fix(podcaster): stage uploads on the media filesystem + reject oversized early (ref #853)
The audiobook upload wrote its temp ZIP to a hardcoded /var/lib/secubox/podcaster
(the 15GB eMMC) regardless of where media_path points. On a box whose media
store is a large SSD, a big upload still filled the tiny eMMC and failed with a
confusing 502.

- temp ZIP is now created under MEDIA (media_path), same filesystem as the
  extraction target — so it lands wherever the store lives (the SSD).
- before streaming, if Content-Length * 2 + margin exceeds the media
  filesystem's free space, return a clear 413 ("needs ~X MB, only Y free")
  instead of failing mid-write. (No amount of code makes a 700GB ZIP fit — but
  the user gets an instant honest reason.)

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 09:15:32 +02:00
de5c2f2ebb fix(podcaster): socket-ownership race left podcaster.sock root:root → nginx 502
Some checks failed
License Headers / check (push) Has been cancelled
The unit chowned the socket in an ExecStartPost gated only by `sleep 1`.
uvicorn can take several seconds to bind the UDS, so the chown ran before the
socket existed (silently, via the `-` prefix) and it stayed root:root — which
nginx (not root) cannot open, so every podcaster request 502'd after a restart.
A naive poll didn't help either: it found the STALE socket from the previous
run, chowned that, and uvicorn then re-bound a fresh root:root socket.

Fix: ExecStartPre removes the stale socket first, and ExecStartPost polls up to
~15s for the genuinely-new socket before chmod/chown. Socket is now reliably
secubox:secubox on start. Verified live: owner settles secubox:secubox and
nginx serves /api/v1/podcaster/status 200.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 09:04:52 +02:00
bfce737b1d fix(core): raise shared nginx proxy_read_timeout 30s → 300s
30s was too short for slow module operations behind the reverse proxy — a
podcaster audiobook upload (server-side ZIP extraction of ~120MB) exceeded it,
so nginx returned 502 on an upload that actually succeeded. 300s matches the
per-vhost timeouts already used elsewhere (lyrion, yacy). Applies to every
module served through secubox.d.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:59:01 +02:00
f554ec2f9a docs(webui): message boxes persist by severity — transient progress/success, persistent dismissable errors
Fatal messages (API/HTTP error, failed action, missing) must stay until the
user dismisses them, never a 3s flash. Codify the toast()/errorToast() split
and the "report the true server outcome" rule (a proxy timeout must not turn a
real success into a permanent-looking failure).

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:58:25 +02:00
66cf9f4089 fix(podcaster): off-loop audiobook extraction + persistent error toasts (ref #853)
The audiobook ZIP handler extracted synchronously on the single-worker event
loop. A ~120MB/20-track audiobook took longer than nginx's 30s proxy_read_timeout,
so nginx returned 502 WHILE the extraction actually completed server-side — a
false-negative "Upload failed" on a successful upload (and it froze the service
meanwhile).

- extraction moved to asyncio.to_thread (_publish_audiobook_zip); the event
  loop stays responsive and the response is sent as soon as it finishes.
- ClientDisconnect during body streaming is caught → clean 400, not a 500.
- any extraction failure now deletes the partial feed (no orphaned feed; the
  prior code only cleaned up the "no audio found" case).

Webui (per operator guideline): fatal messages (API/HTTP/missing) now persist
until the user dismisses them (red toast with ✕); progress + success stay
transient. Routed all 7 error sites to errorToast.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:57:13 +02:00
ae98a76fe5 fix(billets): communiqué always-light + CSP-safe health-banner opt-out on public blog (ref #851)
Backport of two board-live fixes so a package reinstall can't revert them:

- billets-communique.css: the communiqué permalink extends the blog base
  (billets.css flips dark under prefers-color-scheme:dark), which leaked a dark
  ground into the always-light press-release. Pin color-scheme:light + html/body
  light. Bump billet_communique.html css ref to ?v=2.
- health-banner.js: public pages can now opt out of the operator banner with
  <meta name="sbx-no-health-banner"> (reliable — the WAF injects the loader
  after the page head; and CSP-safe, unlike an inline window flag). base.html
  emits it, so the public blog no longer shows the banner while admin pages
  (standalone templates) keep it. Replaces a board-only hostname hardcode.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:42:00 +02:00
0e41813e57 refactor(podcaster): use asyncio.wait to await cancelled download (review #853)
Replace `try: await cur except BaseException: pass` with `await asyncio.wait({cur})`
so awaiting the cancelled download's unwind no longer swallows cancellation of
the request handler itself (client disconnect). Pure idiom cleanup; behaviour
for the cancel-and-restart path is unchanged.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:35:03 +02:00
e0ec3ab417 fix(podcaster): dedup in-flight downloads + cancel-on-restart (review #853)
Adversarial review found a corruption ship-blocker: nothing prevented two
_download_one tasks for the same episode (manual ↻ button, auto-retry re-queue
and restart-recovery all feed the same queue). Two tasks share deterministic
tmp/dest paths → interleaved writes corrupt the audio and thrash state.

- _inflight registry keyed by episode id; the worker skips a queue entry whose
  download is still running (dedup).
- manual /episodes/{id}/download now cancels any live task and awaits its
  unwind before re-queuing, so ↻ genuinely restarts a wedged download instead
  of colliding or being deduped away.
- _download_one treats CancelledError as a clean abort (drop .part, no attempt
  charged), distinct from a real failure.

Test: worker runs one download for two same-id puts, and a cancel+re-put
starts a fresh attempt.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:29:36 +02:00