--- name: social-console description: Operate Alex's Hermes Social Console — organic social drafts, media, calendar, accounts, approvals, and safe publish/schedule workflows through the self-hosted sister app to Hermes Ads. version: 0.1.0 author: Hermes Agent license: MIT metadata: hermes: tags: [hermes-social, social-media, facebook-page, instagram, tiktok, drafts, approvals, calendar, self-hosted] related_skills: [postiz, paid-ads-agent-platforms, vps-app-deployment] --- # Hermes Social Console Operator Use this skill whenever Alex asks Hermes to manage organic social posts, drafts, media, schedules, approvals, connected accounts, or social publishing through the Hermes Social Console. Hermes Social is a **sister app to Hermes Ads**, not a replacement. It should follow the same internal-dashboard/compliance posture: - owned business/social accounts only - official platform APIs only - draft-first operation - human approval before publishing or scheduling - audit logs for every important action - server-side token storage only - no autonomous spam/bulk posting ## Deployed app target Current planned/default target: - App path: `/home/avalon/apps/hermes-social` - URL: `https://hermes-social.apps.poofc.com` - Local dev URL: `http://127.0.0.1:4029` - PM2 name: `hermes-social` - DB: `/home/avalon/apps/hermes-social/data/hermes-social.sqlite` - Media root: `/home/avalon/apps/hermes-social/uploads` Environment variables for scripts: ```bash export HERMES_SOCIAL_URL="https://hermes-social.apps.poofc.com" export HERMES_SOCIAL_API_KEY="...server-side key from uncommitted .env..." ``` Never print or paste `HERMES_SOCIAL_API_KEY`, OAuth tokens, app secrets, page tokens, or token table contents into chat. ## Safety rules Allowed by default: - list connected accounts - upload media - create local drafts - inspect drafts/calendar/audit - mark a draft as locally approved/rejected/needs changes when Alex clearly asks - generate platform-specific variants Requires explicit approval: - publish now - schedule a draft - reconnect/disconnect an account - request new OAuth scopes - edit an already-approved draft if it will be published Blocked unless Alex gives a very specific batch approval: - bulk autonomous posting - publishing from Telegram without an approval step - browser automation/scraping against social sites - direct DB writes except debugging/repair Important distinction: - **Approve locally** only changes local status to `approved_local`. - **Publish now** or **Schedule** is a separate explicit platform-write action. ## Command surface Operating rule: anything Alex can do in the Hermes Social UI should have a Hermes-agent usable CLI/API path documented here. Prefer the app's scripts or `/api/hermes/...` endpoints over browser UI automation. When new UI features are added, update this skill with the matching CLI command/API call in the same session. When Hermes creates or presents a draft in Telegram, always include a direct Hermes Social UI link to the draft alongside the draft ID. Use `${HERMES_SOCIAL_URL}/?draft=` unless the app exposes a more specific canonical draft route. Run from the app directory unless using absolute paths: ```bash cd /home/avalon/apps/hermes-social ``` ### List connected accounts ```bash HERMES_SOCIAL_URL="https://hermes-social.apps.poofc.com" \ HERMES_SOCIAL_API_KEY="$HERMES_SOCIAL_API_KEY" \ python3 scripts/social_accounts.py ``` Account UX/API rule: the Accounts tab must show platform-specific identities, not just `platform + ID`. For each connected Meta account, expose/render a structured `identities` array with: - Facebook Page: Facebook visual mark, Page avatar (`avatar_url`), display name, Page ID, and direct `https://www.facebook.com/` link. - Instagram: Instagram visual mark, IG avatar (`instagram_avatar_url`), handle when available (`@instagram_username`), Instagram Business ID, and direct `https://www.instagram.com//` link. Persist Instagram profile context from Meta sync (`instagram_business_account{id,username,profile_picture_url}`) into server-side columns such as `instagram_username` and `instagram_avatar_url`; add lightweight SQLite migrations for new columns. If an existing account already has only `instagram_business_account_id`, refresh username/avatar via the server-side Meta/System User token and update the row. Never ask Alex to visually compare raw IDs when names, avatars, and direct links can be shown. ### Create a draft ```bash python3 scripts/social_create_draft.py \ --title "Weekly Astro Mage post" \ --caption "Caption text for Alex to approve." \ --hashtags '["AstroMage", "Astrology"]' \ --targets '["acct_..."]' \ --scheduled-for "2026-05-16T10:00" \ --variants '{"facebook":"Longer Page caption","instagram":"Short IG variant","tiktok":"Video hook"}' ``` This creates a **local draft only**. It does not publish. ### Upload media ```bash python3 scripts/social_upload_media.py /absolute/path/to/image-or-video.mp4 ``` Use the returned `media.id` in draft creation/editing. Platforms such as Instagram/TikTok often require publicly reachable media URLs. In production, uploaded media should go to Hetzner S3 bucket `hermes-social` and return `https://hel1.your-objectstorage.com/hermes-social/...`; if S3 is unavailable, the app should save locally under `/uploads` and return a warning instead of failing the whole upload. ### Get a draft ```bash python3 scripts/social_get_draft.py draft_... ``` ### Edit a draft through the Hermes API Use this for agent-safe draft content/media replacement after uploading assets. This edits a local draft only; it does not publish. ```bash curl -fsS -X PATCH "$HERMES_SOCIAL_URL/api/hermes/drafts/draft_..." \ -H "Authorization: Bearer $HERMES_SOCIAL_API_KEY" \ -H "Content-Type: application/json" \ --data '{ "title": "Updated title", "caption": "Updated caption", "hashtags": ["Astrology"], "media_asset_ids": ["media_..."], "platform_variants": {"publish_target":"instagram","post_type":"carousel"} }' ``` If `/api/hermes/drafts/:id` PATCH returns 404 on an older deployment, update/deploy Hermes Social; commit `9324f49` added the Hermes API alias for the existing browser draft edit route. ### Meta setup / onboarding readiness The UI has a **Setup** tab that guides users through Meta Business, Pages, Instagram Accounts, Developer Apps, System Users, redirect URI, permissions, and safe server-side token storage. Anything in that UI must stay mirrored here as CLI/API capability. Run a redacted readiness report without printing secrets: ```bash cd /home/avalon/apps/hermes-social python3 scripts/social_setup_check.py ``` This reports configured env keys by presence/length only, connected Page/IG account state, and readiness checks for Meta app credentials, Hermes CLI key, Page OAuth sync, Instagram target, and System User token. ### Hermes API / CLI-style publishing Hermes-agent automations and scheduler scripts must use the `/api/hermes/...` route family with `HERMES_SOCIAL_API_KEY`, not the browser-admin `/api/...` routes that require an `hs_session` cookie. For an explicitly approved draft: ```bash curl -fsS -X POST "$HERMES_SOCIAL_URL/api/hermes/drafts/draft_.../publish-now" \ -H "Authorization: Bearer $HERMES_SOCIAL_API_KEY" \ -H "Content-Type: application/json" \ --data '{"platform":"instagram"}' ``` If a scheduler/CLI publish gets `401 Unauthorized`, first check that it is using `/api/hermes/drafts/:id/publish-now`; `/api/drafts/:id/publish-now` is browser-admin and will fail without the login cookie. ## UI workflow 1. Command tab: high-level cockpit and compliance posture. 2. Drafts tab: compact approval cards. 3. Calendar tab: native Date-based month grid + agenda for scheduled drafts/outcomes. Do **not** reintroduce Schedule-X/Temporal-dependent libraries unless iOS support is explicitly handled. 4. Media tab: upload/manage image/video assets. Production uploads should prefer Hetzner S3 public object URLs with graceful local fallback. 5. Accounts tab: connected OAuth accounts and setup checklist. 6. Audit tab: append-only action log. Design rules: - Mobile-first single-column default. - Desktop expands into two-column cockpit/calendar/detail layouts. - Avoid oversized cards/padding; keep queue cards compact. - Draft Queue and Calendar → Selected draft must be **post-first**, not metadata-first: render the attached image/video thumbnail inline, show platform-faithful Instagram/Facebook-style previews with account identity + caption formatting, and collapse IDs/raw JSON/routing/platform variants under a `
` section. - Calendar selected draft UX should be a dismissible overlay/bottom sheet, not a static side/below panel that lengthens the day/week/month list. Provide a visible Close button and tap-outside dismissal. When converting an inline selected-draft panel to an overlay, do **not** hide broad structural selectors such as `.calendar-layout > .panel:last-child`; once the inline panel is removed this can hide the only Calendar panel and make the tab appear blank. Prefer explicit overlay class selectors and verify the built CSS no longer contains accidental `display:none` rules for the main calendar panel. - Calendar selected-draft overlays should keep context while scrolling: split the overlay into a fixed/floating blurred header (`backdrop-filter`) containing the draft title, source/date metadata, status chip, and Close button, plus a separate scroll body for the post preview. Hide duplicate in-card title/status rows inside the overlay so the preview starts with the post/media rather than repeated chrome. - Mobile overlay spacing pitfall: do not implement the selected draft overlay as a bottom sheet with `align-items:flex-end` plus `max-height:86dvh`; this creates a huge blurred gap above the header on iPhone. For mobile, stretch the overlay/panel from the top safe area (`align-items:stretch`, `height:100%`, `grid-template-rows:auto minmax(0,1fr)`) so the blurred header sits near the top and only the preview body scrolls. Desktop can override back to centered with a bounded height. - Media Library cards should show real image/video thumbnails, not only filename/MIME metadata. On mobile, keep the media library as a compact two-column thumbnail grid rather than collapsing to one column. - Calendar Day and Week draft rows should include a small thumbnail from the draft's first attached media asset, with a compact fallback tile when no media exists. - For Meta-hosted images/avatars and externally hosted media thumbnails, use `referrerPolicy="no-referrer"` where applicable. - The calendar is a planning surface, not an automatic publisher. ## Calendar implementation Hermes Social intentionally uses a lightweight native `Date` calendar/agenda instead of Schedule-X. The calendar should be mobile-first with Day / Week / Month modes: default to Week for density, keep Month available as overview, and make tapping a Month day open the Day view so multiple scheduled drafts on that date are inspectable. In Month cells, keep count bubbles visually unobtrusive in the bottom-right; avoid crowding the top-right date number. iOS Safari/WebViews still lack the `Temporal` global, and Schedule-X bundles can crash mobile startup with `Can't find variable: Temporal`. If a calendar crash appears on iPhone: 1. Search source and built assets for `Temporal`, `@schedule-x`, `@preact/signals`, and `preact`. 2. Confirm `dist/` is not stale by rebuilding and checking the public `index-*.js` asset referenced by deployed HTML. 3. Keep the calendar data API stable (`events` with `id`, `title`, `start`, `end/status`) so UI swaps do not disturb drafts/publishing. 4. Verify via public root/API curls and, when possible, a real iPhone/incognito reload because service workers/webviews can cache old bundles. For S3 + calendar fix details, see `references/hermes-social-s3-calendar-temporal.md`. For post-faithful Draft Queue/Calendar selected draft preview patterns, including media attachment shape and iOS Temporal polyfill notes, see `references/hermes-social-post-preview-ui.md`. For Meta/Instagram Page-edge debugging, Creator account handling, preserving configured IG IDs, and PM2 env leakage pitfalls, see `references/hermes-social-meta-instagram-access.md`. For the Postiz-vs-Hermes Meta publishing comparison, provider patterns, permalink fix, and future assurance checklist, see `references/hermes-social-postiz-meta-assurance.md`. ## Platform rollout MVP order/current status: 1. Local drafts, media, approvals, calendar, audit. 2. Facebook Page OAuth: implemented. `/api/oauth/meta/start` redirects through Meta Login, `/api/oauth/meta/callback` validates state, exchanges the code, stores encrypted user/page tokens, and syncs Facebook Page accounts plus linked Instagram Business account IDs. - Current Facebook Login OAuth scopes should stay Page-only: `pages_show_list`, `pages_read_engagement`, `pages_manage_posts`. Do not request `instagram_basic` or `instagram_content_publish` through this flow if Meta returns “Invalid Scopes”; use Meta Business/System User token env for the IG publishing path instead. - Meta Page sync should request `instagram_business_account{id,username,profile_picture_url}` on `/me/accounts` when available and store the returned IG business ID on the Page account row. If Business Settings provides a verified IG Business ID, it may be stored as the server-side target after Alex explicitly provides it. - If Alex says an Instagram account is “connected” in Business Settings but Hermes Social still shows no IG ID, do **not** assume app code is broken first: ask him to reconnect Meta OAuth in Hermes Social so `/me/accounts` is re-synced with the new Page↔Instagram link. - Meta Business “Request Sent / access pending” for an Instagram account is approved in the owning Business portfolio’s Business Settings → Requests/Received/Instagram accounts, not necessarily in Instagram DMs. - Alex’s current IG target: `@druidmaxxing`, Instagram Business ID `17841467108307246`; it should appear as The Magi Page’s `instagram_business_account` after successful reconnect. 3. Facebook Page publish after explicit approval: implemented for text posts, single-image/photo posts, multi-photo posts, and single-video posts via Meta Graph (`/{page_id}/feed`, `/{page_id}/photos`, `/{page_id}/videos`) using the encrypted `meta_page` token. Mixed image+video Facebook posts are intentionally blocked; publish one video or one/more images. 4. Instagram Business publishing: implemented for explicit **Publish now** on approved/draft-local posts. The adapter supports single-image feed posts, Reels/single-video posts, carousel posts, and Stories using the Meta IG media container flow (`/{ig-user-id}/media`, bounded poll of `/{creation-id}?fields=id,status_code,status` until `FINISHED`, then `/{ig-user-id}/media_publish`) and a server-side System User token (`META_SYSTEM_USER_TOKEN`). Keep tokens server-side; do not expose them to the client. Immediate publish after container creation can fail with `Media ID is not available`; use bounded polling before publish. After `media_publish`, fetch `/{media-id}?fields=id,permalink` and store the real permalink instead of constructing a URL from the media ID. For CLI/API publishes, pass `{"platform":"instagram","post_type":"story"}` to force Stories, `post_type:"reel"` for Reels, or `post_type:"carousel"` for carousels; otherwise multiple media defaults to carousel and a single video defaults to Reel. 5. TikTok OAuth/video publishing. ## Postiz relationship Postiz remains a reference implementation for OAuth/media/platform quirks, not the final control plane. Do not create new Postiz drafts unless Alex explicitly asks to use Postiz. ## Session-specific references - See `references/hermes-social-mvp-deployment.md` for the first MVP build/deployment reference: repo/path/PM2/port, SQLite/media paths, Schedule-X dependencies, Hermes operator scripts, nginx/certbot follow-up, and remaining Meta/Instagram/TikTok connector gaps. ## Publish safety / anti-block guardrails Before adding or operating any scheduled/background publisher, implement and verify an app-level Publish Safety Guard. Do **not** rely on Postiz/provider concurrency defaults as the safety layer; Postiz recognizes platform errors but its provider `maxConcurrentJob` values are high for Alex's use case. Recommended conservative defaults for Alex's owned accounts: - Instagram: soft warn at 3 posts/day/account, hard cap around 5/day/account, minimum 45–60 minutes between publishes. - Facebook Page: soft warn at 5 posts/day/page, hard cap around 10/day/page, minimum 20–30 minutes between publishes. - Stop/pause an account after 2 Instagram or 3 Facebook Meta failures in an hour, unless Alex manually reviews and overrides. - Check Instagram `/{ig-user-id}/content_publishing_limit` before publish when available. - Detect duplicates by normalized caption + media hash and block/warn on same media+caption reposts within a cooling window. - Never auto-retry policy/rate/content errors (`spam`, `blocked`, `Page request limit reached`, permission errors, invalid media). Retry only transient network/5xx once or twice with backoff. - Keep schedule execution approval-gated: only `approved_local` drafts may become scheduled/published, and every platform write should create a publish_attempt + audit row. For the detailed Postiz comparison, Meta docs excerpts, observed provider safeguards, and proposed Hermes Social guard schema/policy, see `references/hermes-social-publish-safety-guards.md`. ## Failure handling - If a publish attempt fails, report the attempted draft ID, account ID, platform, and error summary. - Do not retry blindly. - Preserve the draft and publish_attempt row. - If OAuth/token errors occur, guide Alex through reconnect/review scopes rather than asking for raw tokens in chat. - Meta Graph `/me/accounts` on current Graph versions should request Page `tasks`, not deprecated `perms`; `perms` can trigger `(#100) Tried accessing nonexisting field (perms)`. Keep storage tolerant of legacy `page.perms`, but do not include `perms` in the fields query.