The acampada26s.net mark as is (two tents), with a cable coming out of
the bottom tent's door to a radio antenna. Colour (blue/orange) and
white versions, SVG and 1024px PNG.
- produceVideo checks every logo overlay key against the workspace's
media_assets, group_assets or branding logo before queueing; a
well-formed content address alone no longer suffices.
- Remove MEDIA_DEBUG_STDERR=1 from the production compose: it wrote raw
ffmpeg stderr to an unrotated file, bypassing redaction.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Approved posts may carry mediaAssetIds and altTexts since the repository
gate was lifted, but the scheduler built connectors without an asset
loader, so Mastodon and Telegram rejected every post with media as
INVALID_POST and alt texts never reached them.
- media.ts: loadAsset reads media_assets inside the execution's tenant
(app.workspace_id, RLS forced); openAsset fetches the sealed object,
opens the SPENC1 frame with S3_KEK and checks the content hash.
Read-only; bytes go worker to provider and never through the API.
- preparePostForDispatch maps positional altTexts onto
providerMetadata.altText keyed by asset id.
- Instagram joins the scheduler registry so its posts fail with an
explicit connector error instead of UnknownProviderError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous pass kept the dark base and the red icons, which did not
read as the acampada. Now:
- Light theme: white page, black ink and borders, black buttons with a
hard neon offset shadow, neon for focus and the active nav item;
h1/h2 in Anton with the stepped neon shadow of the site headings.
- Nav, empty-state, spinner and pattern assets in black; platform icons
keep their dark disc with a neon stroke.
- New mark: three dome tents wired from their doors into a switch with
an antenna, drawn in the inked style of the acampada logo (curved
poles, ball ends, hatching) over a neon offset copy. LogoMark renders
it inline; visible product name is T-HUB.
- docs/brand: t-hub.svg and a lockup with the Anton wordmark converted
to outlines so it renders without the font.
- Public submission page moved to the light palette.
Checked with headless Chromium screenshots (offline sample data) on
desktop and mobile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: architecture diagrams (single account, shared accounts,
components), status per platform, outreach, hardware and bandwidth
requirements, ports, security practices and what to monitor.
docs/T-HUB-VALIDACION.txt: per-commit test plan for reviewers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Black/white/neon (#00e5ff) on the existing dark base, h1/h2 in Anton
uppercase with the neon offset used on acampada26s.net. Neon fills get
dark text. The public submission page and the control-room focus ring
follow the same accent. Anton ships under the SIL OFL (license copied
next to the font). New logo in docs/brand/t-hub.svg, also the favicon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration 042 enables and forces the tenant_isolation policy on
media_assets with an explicit WITH CHECK; media and publishing queries
also scope by app.workspace_id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A list of public accounts a collective wants to write to (artists,
collectives, public figures), with a drafted message per contact and the
state of the conversation: todo, sent, replied, declined.
The Hub does not send these messages. "Copy & open profile" puts the
draft on the clipboard and opens the profile; a person sends it from the
platform's own DM box and marks it sent. Unsolicited bulk DMs break the
terms of every commercial platform and get accounts restricted, and
Instagram's API cannot open a conversation at all.
API (OutreachModule):
- migration 041: outreach_contacts, RLS enabled + forced with WITH CHECK,
unique per (workspace, platform, lower(handle)); permissions
outreach.view (Owner/Admin/Editor/Moderator) and outreach.manage
(Owner/Admin/Editor).
- GET/POST/PATCH/DELETE /outreach/contacts; bulk import up to 500 rows,
known accounts skipped. Audit records list changes and status moves,
never the message text.
Web: /outreach page (sidebar + More), CSV import, {name}/{handle}
template for pending contacts, status filters, a daily counter that
warns past 20 first messages.
Tests: service and request parsing with the DB faked; the migration and
every service query were also run against PGlite as the non-superuser
app role (tenant isolation, conflict skip, status timestamps).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Instagram connector existed but was never registered, so Channels showed
it as "provider unavailable" and publishing could not resolve it.
- Register createInstagramConnector in the connections registry and the
publishing registry; add the connector-runtime publishing policy
(idempotency "none": media_publish has no key, never replay).
- The Facebook app credentials identify the Hub, not a collective: read them
only from INSTAGRAM_CLIENT_ID / INSTAGRAM_CLIENT_SECRET. Drop the
clientId/clientSecret connect fields and stop copying the app secret into
each connection's vault entry. The provider is connectable only when both
are set (connectEnabled + connectNote).
- Fail the connect when several Instagram accounts are granted instead of
silently publishing as the first Page's account.
- Fix the connector package's pre-existing lint errors (it now ships in api).
- Tests: contract cases for env-only credentials and the multi-account
refusal; connections integration case for provider gating and the
Facebook dialog redirect.
- docs/oauth-connect.md: Instagram setup, callback URI and limits.
Not in this change: publishing images needs a public plaintext JPEG URL that
Meta downloads, and storage is client-side encrypted, so presigned S3 URLs
cannot serve it. Until a short-lived media endpoint exists, publishing to
Instagram fails with an explicit error. Token refresh via ig_refresh_token
on graph.facebook.com also needs checking against Facebook Login tokens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>