Commit Graph

2747 Commits

Author SHA1 Message Date
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
4e18347d7a fix(podcaster): timeout+auto-retry+restart-recovery for wedged downloads; live upload progress (ref #853)
Downloads no longer wedge a queue slot forever:
- real httpx.Timeout (connect/read/write) so a stalled stream aborts to error
  instead of hanging on the previous timeout=None
- bounded auto-retry (3 tries, 10/30/60s backoff) via a new attempts column
- restart-recovery: episodes orphaned in downloading/queued are re-queued when
  the worker (re)starts
- webui: manual "↻" restart button on downloading/queued episodes; a manual
  (re)download resets the retry budget

Audiobook ZIP upload now uses XHR with upload.onprogress: a persistent
"Uploading N%" indicator (was a fetch with a 3s toast that hid mid-upload),
then a persistent published/error result.

Store: idempotent attempts-column migration + requeue_stuck + bump_attempts,
with tests.

Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
2026-07-14 08:23:46 +02:00
CyberMind
37688ebc60
Merge pull request #852 from CyberMind-FR/feature/851-billets-communique-style-embed-snapshot
feat(billets): communiqué style + embed snapshot vignette + portable HTML archive (#851)
2026-07-14 08:07:24 +02:00
965377388f fix(billets): SSRF — validate embed_url public-IP before Chromium screenshot (ref #851)
Review HIGH: the headless-browser snapshot path drove Chromium to embed_url with
no SSRF check (only the og:image fallback was guarded), so an author could
screenshot an internal service (127.0.0.1/RFC1918/::1/169.254.169.254). Now
ssrf.validate_url() gates the navigation (reject → fall back to og:image), plus a
Playwright route aborts any redirect/sub-resource to a non-public host. 12 tests.

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