The live tuwunel does not offer m.login.jwt (the jwt.* flat config keys were
inert), so the JWT broker flow could never work. Pivot: appservice.hub owns
the exclusive hub_* namespace on the homeserver; POST /matrix/session now
registers @hub_<username> with the as_token and mints a single-use
m.login.token via login/get_token (?user_id= impersonation). Optional
MATRIX_SSO_HS_TOKEN authenticates inbound appservice transactions, acked by
the new MatrixAppserviceController sink. Response shape unchanged.
- group-files: origin filter facet; GET /files/:id returns 410 for unavailable
rows after the ACL recheck; fix latent group_members.status filter and unbound
workspace parameter that would 500 listFiles
- archive-bot: markRedacted revokes live group_file_shares rows in-transaction
- new live-DB integration suite for the archive index surface
- archive_sync_state keyed by room per migration 029 (drop synthetic id cursor)
- putStaging buffers the stream and sends content-length (oCIS rejects chunked)
- mimeClass passthrough from sniffed payload
- edge-firewall: RETURN docker-bridge ingress before the CF-range catch-all so
container egress (Matrix/R2/platform) survives the 80/443 source filter
- files scaffold: keep the ocdav data-gateway hop on-loopback
The docker build (fresh install, container @types resolution) fails on
http.test.ts's enhanced-Response mock (bytes/textStream) — tests are vitest
territory, never part of the dist build.
page.tsx now calls the real backend: GET /files with groupId/name/mimeClass/
cursor/limit (origin filter dropped — the api has no origin param), shares
tracked as ShareCreated (revoke by id), retention mapped to the orthogonal
legalHold model, downloads by item.name, samples reshaped to the wire
contract.
GroupFile gains mimeClass (the indexed coarse class the row icon and the
filter depend on): core contract, service row mapper, parseGroupFile guard,
test fixture, UI types — one vocabulary end to end. parseGroupFile's roomId
becomes null-tolerant to match direct-upload rows (mapping input stays
strict).
Verified: core build clean, api typecheck 0 errors + group-files 22/22,
web typecheck 0 errors.
Wires the three parallel group-cloud tracks to one surface and fixes the
seams between them:
- chat_media_archive.room_id becomes nullable (direct web uploads have no
Matrix room yet — the contract's null-event pattern extends to the room);
core GroupFile.roomId and ArchiveEntryInput.roomId follow.
- mime_class gains 'archive' (zip) across the 029 CHECK, validation
MIME_CLASSES and the bot classifier — one vocabulary everywhere.
- registerArchiveEntry claim keys are null-safe for room-less uploads.
- New: POST /files/groups/:groupId/upload — FileInterceptor (20 MiB) streams
through magic-byte sniffing, library-safe naming, oCIS upload and the
metadata index, behind group-membership ACL (Owner/Admin bypass).
- safeFileName/sniffMime promoted to packages/core (file-names.ts,
file-mime.ts) so the bot and the api share one implementation; the bot's
local modules become re-export shims; @types/node added to core.
Verified: api typecheck clean, group-files 22/22, bot 74/74, web typecheck
clean; 029 re-applied clean on a fresh scratch DB (room_id nullable,
archive class, claim_key unique all confirmed in the catalog).
The group-files api and the archive bot authenticate to oCIS WebDAV with
basic credentials (admin until a dedicated service user exists). The proxy
is LAN-published only; no WAN forward exists.
281-line version with the person-identity DDL sketch, per-model license
audit table, VRAM/cost budget, GDPR Art. 9 analysis, and all primary-source
citations — replaces the 53-line executive summary.
Research: use PeerTube
native transcription (core since 6.2, whisper-ctranslate2, remote runners on
GPU nodes; large-v3-turbo MIT via model_path); pyannote community-1
diarization (CC-BY-4.0, clean); WhisperX fusion.
Face recognition license audit: NO accurate open FR model has clean weights
(InsightFace/SCRFD non-commercial, yolo-face GPL, AdaFace unstated) —
posture is detection+tracking+blur by default; identity matching opt-in,
human-review-gated, collective decision + DPIA required (Art. 9 biometric
data; automated FR over non-consenting third parties effectively unlawful).
DB DDL sketch: persons/person_face_embeddings/video_face_detections/
transcript_segments/video_speaker_person/identity_audit with review gating,
visibility tiers, erasure workflow.
infra/occupymedia/files/ was gitignored in phase one ('nothing deployed');
the oCIS deployment is now live, and the storage doc + DEPLOY.md link to its
runbook. Track the secret-free scaffold (compose.yml, csp.yaml, README,
tuwunel example, .env.example); keep only .env + secrets/ ignored.
compose.yml carries the deployment-proven fixes: ocis runs as user 1000 with
OCIS_CONFIG_DIR=/etc/ocis and HOME=/var/lib/ocis (source-built images do not
inherit the official image's conventions), and 9200/9980 publish on the LAN
for the CT150 tunnel connector.
Research: Tika full-text
via 3 oCIS env vars (phase 1); bge-m3 on TEI + pgvector halfvec/HNSW in the
existing Postgres with Postgres FTS + RRF hybrid (phase 2); Docling tier for
scanned PDFs; NATS event subscription (UploadReady et al) + nightly PROPFIND
reconciliation for change propagation. jina-v3 excluded (non-commercial).
Bilingual EN+ES eval set flagged as mandatory before freezing models.
The hub-* buckets were provisioned 2026-10-08 (files-blobs, matrix-media,
media-library, pads); the doc's occupymedia-files-blobs name never existed.
hub-files-blobs is now live as the oCIS S3NG bucket (occ-files CT 320,
deployed 2026-10-10) with a bucket-scoped S3 key.
One-shot watcher for the workstation: probes ping.occupymedia.cc through a
reachable edge IP, flips the four hostnames after the port-forward verifies,
checks the public path for 6 minutes and auto-reverts to TUNNEL if the new
pool assignment is broken too. Retires itself if the tunnel heals first.
README updated to match.
Cloudflare's tunnel edge-pool assignment can blackhole the site for whole
regions (2026-10-10: tunnel hostnames served from an unreachable anycast
range for hours). This adds an independent path and the automation that
keeps occupymedia.cc reachable when either path is sick:
- nginx edge mirroring the tunnel ingress (app/api/tube/chat/ping), LE
wildcard via dns-01, twice-daily renewal
- DOCKER-USER firewall: 80/443 restricted to Cloudflare ranges (CF mode)
or OPEN for grey-cloud mode via firewall.mode
- edge-watcher (60s systemd timer): dynamic public-IP sync, dual-path
health checks, DNS failover TUNNEL<->ORIGIN/GREY with hysteresis,
gated by EDGE_ENABLED
- ping.occupymedia.cc sacrificial probe host for activation
- deployment runbook in README.md; credential files shipped as .example
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.
MIT licensed. See README.md and docs/.