Files
felhom.eu/documentation/audits/SPIKE-facebook-page-api-2026-10-08.md
T
admin e9e6300cfc
gates / gates (push) Successful in 4m35s
Spike: Facebook Page reached after the operator's Page click; scheduled post + photo created, read back byte-equal, deleted
Read phase passes (Page 1360018983863273, CREATE_CONTENT/MODERATE/ANALYZE, page token PAGE expires_at 0, three insights
metrics alive on v26.0). Write test: removal check accepts Meta's code-10 'Object does not exist' (fixed without a row,
4 tests); run 1 evidence kept. R-914 READY, R-915 narrowed to Live mode.
2026-10-08 18:38:25 +02:00

76 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPIKE — can Claude Code run the Felhom.eu Facebook Page? (2026-10-08)
**Verdict: yes.** The system-user key is valid and never expires; it reaches the Felhom.eu Page with every task the
skill needs; a scheduled text post and a scheduled photo were created, read back byte-equal (Hungarian accents) and
deleted, each removal proven by a failed GET. **Open:** posts made while the app is in development mode are seen
only by people with a role on the app (read, not measured) — public posting needs the app switched to Live (R-915).
Baseline `felhom.eu` @ `fe0dc0d03f`. Probe: `scripts/facebook/fb_probe.py` (stdlib; tests `test_fb_probe.py`).
Evidence: `facebook-page-api-2026-10-08/` — `read` run at the top level, `write-test/` the passing write run,
`write-test-run1-strict-check/` the first write run (see „Removal proof"). Graph API **v26.0** accepted, no fallback.
Grades: **measured** = this run; **read** = Meta's documentation, not run.
**Architecture: no document in `documentation/architecture/` covers marketing or social media** (checked `00`–`12`).
Correct — the Page is a business tool, not part of the product. Decision home: `CONTEXT.md` (2026-10-08,
„Facebook Page"); rows R-914, R-915.
**Timeline.** First `read` (twice): `/me/accounts` empty — the app was assigned to the robot, the Page was not.
The operator assigned the Page (Business settings → Pages → Felhom.eu → assign `felhom-cc`); the **same key**,
not regenerated, then saw the Page. A system-user token is not pinned to a Page list (measured).
## Findings
| Question | Answer | Grade | Evidence |
|---|---|---|---|
| Key valid? type? | `is_valid: true`, `SYSTEM_USER`, app `2273465403490709` (`felhom.eu`) | measured | `A1-debug-token.json` |
| Does it expire? | `expires_at: 0`, `data_access_expires_at: 0` — never | measured | `A1` |
| Scopes | `read_insights, pages_show_list, business_management, pages_read_engagement, pages_read_user_content, pages_manage_posts, pages_manage_engagement, public_profile`; granular scopes carry no `target_ids` | measured | `A1` |
| Robot | id `122094150717513084` (app-scoped), name `felhom-cc`; Business Suite shows it as `61595392523486` (business-scoped) — the same robot | measured | `B1-me.json` |
| Page reached | exactly one: `Felhom.eu`, **Graph ID `1360018983863273`** (the browser profile address shows `61595336666018`) | measured | `B2-me-accounts.json` |
| Page tasks | `CREATE_CONTENT, MODERATE, MESSAGING, ADVERTISE, ANALYZE, MANAGE_LEADS, VIEW_MONETIZATION_INSIGHTS` | measured | `B2` |
| Page token | derived from `/me/accounts?fields=…,access_token`; `type: PAGE`, `expires_at: 0`; a fresh value on each derivation (199 / 201 chars) | measured | `B3-debug-page-token.json` |
| Page fields | name, link, category `Information Technology Company`, about, website, `followers_count 0`, `fan_count 0` | measured | `C1-page.json` |
| Feed | 1 post (profile picture update) | measured | `C2-feed.json` |
| Insights (v26.0, `period=day`) | `page_post_engagements`, `page_follows`, `page_media_view` all answer (values 0) — none deprecated | measured | `C3-insights-*.json` |
| Scheduled text post — D | `POST /{page}/feed` `message`, `published=false`, `scheduled_publish_time=now+7d` → id `{page}_{post}`; read back `is_published: false`, `scheduled_publish_time` equal, **message hex equal** | measured | `write-test/D1`, `D2`, `D9-verdict.json` |
| Scheduled photo — E | `POST /{page}/photos` multipart `source` (`website/assets/logo.png`), `caption`, `published=false`, `scheduled_publish_time` → **only `id` (a photo id), no `post_id`**; photo `name` read back **hex equal**; DELETE on the photo id | measured | `write-test/E1`, `E3`, `E9-verdict.json` |
| Is App Review / Live needed to post? | **No refusal in development mode**: both writes answered HTTP 200 — no (#200), (#10) or (#3) | measured | `D1`, `E1` |
| Dev-mode posts visible to the public? | **No**: „Any data generated while an app is in Development mode, such as test posts, can only be seen by role users"; visible to all once the app is Live | read | [app modes](https://developers.facebook.com/docs/development/build-and-test/app-modes) |
| App Review for our own Page? | Standard Access is automatic and covers users/assets with a role on the app; Advanced Access (review + business verification) is for others' assets | read | [access levels](https://developers.facebook.com/docs/graph-api/overview/access-levels) |
| What does Live need? | display name, contact e-mail, **Terms of Service URL**, app icon, category, app purpose (each „required to switch your app to Live mode"); Privacy Policy URL and data-deletion URL listed beside them | read | [basic settings](https://developers.facebook.com/docs/development/create-an-app/app-dashboard/basic-settings) |
| Headers | `facebook-api-version: v26.0`; `x-business-use-case-usage` keyed by business `4713349378884589`, type `business_integration_system_user_platform_endpoints`; `x-app-usage` | measured | `F1-headers.json` |
## Removal proof — a deleted post answers code 10, not 100
| Object | DELETE | GET after DELETE (quoted) |
|---|---|---|
| text post `1360018983863273_122096071749511222` (run 1) | `{"success": true}` | (#10) „Object does not exist, cannot be loaded due to missing permission or reviewable feature, or does not support this operation…" `fbtrace_id AVitleYH5GQggv9QstbpU4Q` |
| text post `1360018983863273_122096072277511222` (run 2) | `{"success": true}` | same (#10) text, `fbtrace_id Aiogseplh0f5BKjQ8tqEzpR` |
| photo `122096072325511222` (run 2) | `{"success": true}` | (#100, subcode 33) „Unsupported get request. Object with ID '122096072325511222' does not exist…" `fbtrace_id AREMCG8p12jhITdpE8zqsPp` |
Run 1 stopped (exit 7) because the probe expected code 100. Code 10 also means a missing permission, so it is
counted as removal only because the **same Page token read the same post with HTTP 200 seconds before the DELETE**
(`D2-readback.json`). `gone_error()` now accepts code 100, or code 10 with „Object does not exist" (tests in
`test_fb_probe.py`). A different-channel control — Meta Business Suite → Planner showing no scheduled post — is the
operator's look, not run here.
## Gaps (for the skill)
- **The photo's publish state was not read**: the photo object has no `is_published`, and no `post_id` came back, so
„scheduled, not public" is unproven for photos. Its `created_time` was the create time (16:36 UTC), while the text
post's `created_time` was its scheduled time. The skill should read scheduled photos back through the Page's
scheduled-post listing before trusting the photo path.
- Is the system user a „role user" for the dev-mode visibility rule? Not stated in the docs read; measured only after Live.
## What the skill needs
- Base `https://graph.facebook.com/v26.0/`; token `FACEBOOK_API` via `load_key()` (R-453 strip + asserts), sent as
`Authorization: Bearer`; never in a logged URL.
- Page token: `GET /me/accounts?fields=id,name,tasks,access_token`, pick `name == "Felhom.eu"` (id `1360018983863273`),
memory only, re-derive per run.
- Text: `POST /{page}/feed` form `message`, `published=false`, `scheduled_publish_time=<unix>` → `id`.
- Photo: `POST /{page}/photos` multipart `source`, `caption`, `published=false`, `scheduled_publish_time` → `id` (photo).
- Delete: `DELETE /{id}` → `{"success": true}`; prove with `GET /{id}` → error per `gone_error()`.
- Stats: `GET /{page}/insights?metric=<m>&period=day` for the three metrics above.
- Every response through `redact()`; HTTP 200 with an `error` body counts as failure.