# Android Cast — exposed services catalog _Validated: 2026-06-10 after FE `apps.conf` consolidation + BE sync. Public base: **https://apps.f0xx.org**._ **Documentation index:** [README.md](README.md) · Companion: [INFRA.md](INFRA.md), [20260608_BE_SERVICES_and_infra.md](20260608_BE_SERVICES_and_infra.md) --- --- --- --- ## Table of contents - [Validation snapshot](#validation-snapshot) - [Hub landing](#hub-landing) - [Console home](#console-home) - [Issues (crash reports)](#issues-crash-reports) - [Related endpoints](#related-endpoints) - [Upload via curl](#upload-via-curl) - [Bulk / synthetic issues](#bulk-synthetic-issues) - [Tickets](#tickets) - [Related endpoints](#related-endpoints-1) - [Create via UI](#create-via-ui) - [Create via curl (session cookie)](#create-via-curl-session-cookie) - [Graphs](#graphs) - [Related endpoints](#related-endpoints-2) - [Fake session data (dev / BE)](#fake-session-data-dev-be) - [Remote access](#remote-access) - [Related endpoints](#related-endpoints-3) - [RBAC / access control](#rbac-access-control) - [Related endpoints](#related-endpoints-4) - [APK builder](#apk-builder) - [Related endpoints](#related-endpoints-5) - [Git (Gitea)](#git-gitea) - [OTA updates](#ota-updates) - [Layout](#layout) - [Publish (operator)](#publish-operator) - [Heartbeat (device)](#heartbeat-device) - [Auth & account security](#auth-account-security) - [URL shortener (preview — not deployed)](#url-shortener-preview-not-deployed) - [Non-HTTP services](#non-http-services) - [Validation scripts](#validation-scripts) **Documentation index:** [README.md](README.md) --- ## Validation snapshot Run: `examples/crash_reporter/backend/scripts/validate_be_services.sh` | Check | URL / target | 2026-06-10 | |-------|----------------|------------| | Hub | `/app/androidcast_project/` | ✅ HTTP 200 | | Crashes console | `…/crashes/` | ✅ HTTP 302 → login | | Graphs | `…/graphs/` | ✅ HTTP 302 → login | | Builder | `…/build/` | ✅ HTTP 302 → login | | Git | `…/git/` | ✅ HTTP 200 | | OTA channel | `/v0/ota/channel/stable.json` | ⚠️ HTTP **404** (FE proxy OK; artifact or BE `location` missing) | | Crash upload | `…/crashes/api/upload.php` GET | ✅ HTTP **400** (expects POST JSON) | | Heartbeat | `…/crashes/api/heartbeat.php` GET | ✅ HTTP **405** (expects POST) | | Graph upload | `…/graphs/api/graph_upload.php` GET | ✅ HTTP **400** (expects POST) | | Two-factor | `…/crashes/two-factor` | ✅ HTTP 302 → login (no pending session) | **Alerts — may need follow-up patch** 1. **OTA:** Public returns **404**; BE direct `curl -H 'Host: apps.f0xx.org' http://artc0…/v0/ota/channel/stable.json` returned HTML lander redirect (not JSON). Confirm BE `listen 80` has `location ^~ /v0/ota/` **before** catch-all, and publish `stable.json` under `ota-artifacts/v0/ota/channel/`. 2. **2FA:** Fixed in repo — `completeTotpLogin()` no longer clears pending session on wrong code; login redirects to `/two-factor` when pending. **Sync BE** (`Auth.php`, `index.php`, `two_factor_challenge.php`, `app.css`). --- ## Hub landing **Web:** [https://apps.f0xx.org/app/androidcast_project/](https://apps.f0xx.org/app/androidcast_project/) Marketing multipage shell; left nav links to Console, Git, Issues, Tickets, Graphs, Remote access, Builder. Shared session cookie `ac_crash_sess` path `/app/androidcast_project`. | Kind | Path | Notes | |------|------|--------| | Static/PHP | `index.php` | Hub landing | | CSS/JS | `hub.css`, `hub-landing.css`, `assets/hub-*.js` | Theme + i18n | | Crashes assets | `…/crashes/assets/css/app.css` | Shared console theme | No device ingest on hub root. --- ## Console home **Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=home](https://apps.f0xx.org/app/androidcast_project/crashes/?view=home) (auth required) Landing cards: Issues, New issue, Tickets, New ticket, Graphs; quick links to all services. --- ## Issues (crash reports) **Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=reports](https://apps.f0xx.org/app/androidcast_project/crashes/?view=reports) Triage anonymous crash JSON; grouped/by-time views; tag editor; tree expand. ### Related endpoints | Method | Endpoint | Auth | Purpose | |--------|----------|------|---------| | `POST` | `…/crashes/api/upload.php` | None | Device crash ingest (JSON or gzip JSON) | | `GET` | `…/crashes/api/reports.php` | Session | List/filter reports (console JS) | | `POST` | `…/crashes/api/report_tags.php` | Session | Update report tags | | `POST` | `…/crashes/api/report_viewed.php` | Session | Mark report viewed | | `GET` | `…/crashes/api/tag_catalog.php` | Session | Tag suggestions | | `GET` | `…/crashes/api/diag.php` | Optional | Diagnostics (may 403 at edge) | ### Upload via curl ```bash BASE=https://apps.f0xx.org/app/androidcast_project/crashes SAMPLE=examples/crash_reporter/sample_crash_java.json RID="manual-$(date +%s)" jq --arg rid "$RID" '.report_id = $rid' "$SAMPLE" | \ curl -sS -X POST "$BASE/api/upload.php" \ -H 'Content-Type: application/json' --data-binary @- ``` Gzip upload smoke: `examples/crash_reporter/backend/scripts/test_gzip_upload.sh` ### Bulk / synthetic issues - Optional payload fields: `ingest.source`, `ingest.client_id`, `ingest.run_id` — see [OPEN_API.md](OPEN_API.md). - Loop `upload.php` with varied `report_id` / fingerprints for load tests. - Tag harness traffic `synthetic` via console or `report_tags.php`. --- ## Tickets **Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=tickets](https://apps.f0xx.org/app/androidcast_project/crashes/?view=tickets) Roadmap / QA tasks; workflow tags (`ticket` vs `issue` meta). ### Related endpoints | Method | Endpoint | Auth | Purpose | |--------|----------|------|---------| | `GET` | `…/crashes/api/tickets.php` | Session | List tickets | | `POST` | `…/crashes/api/ticket_create.php` | Session | Create ticket/issue | | `POST` | `…/crashes/api/ticket_update.php` | Session | Update fields/tags | | `POST` | `…/crashes/api/ticket_tags.php` | Session | Tag catalog ops | | `POST` | `…/crashes/api/ticket_comments.php` | Session | Comments | | `POST` | `…/crashes/api/ticket_upload.php` | Session | Attachment upload | | `GET` | `…/crashes/api/ticket_attachments.php` | Session | List attachments | | `GET` | `…/crashes/api/ticket_attachment.php` | Session | Download attachment | ### Create via UI - Issues view: **New issue** → `ticket_create.js` → `ticket_create.php` with `issue` meta tag. - Tickets view / home: **New ticket** → same API with `ticket` meta tag. ### Create via curl (session cookie) ```bash BASE=https://apps.f0xx.org/app/androidcast_project/crashes JAR=/tmp/ac-cookie.txt curl -sS -c "$JAR" -b "$JAR" -X POST -d 'username=admin&password=admin' "$BASE/login" -o /dev/null curl -sS -b "$JAR" -X POST -H 'Content-Type: application/json' \ -d '{"title":"API test","brief":"from curl","body":"","tags":[{"id":"ticket","label":"ticket","bg":"#6366f1"},{"id":"open","label":"open","bg":"#22c55e"}]}' \ "$BASE/api/ticket_create.php" ``` --- ## Graphs **Web:** [https://apps.f0xx.org/app/androidcast_project/graphs/](https://apps.f0xx.org/app/androidcast_project/graphs/) Session metrics dashboards (user / slug / platform scopes); drag-reorder bricks; empty-session notice when no `graph_upload` data. ### Related endpoints | Method | Endpoint | Auth | Purpose | |--------|----------|------|---------| | `GET` | `…/graphs/api/graphs.php?days=14` | Session | Aggregated metrics JSON | | `POST` | `…/graphs/api/graph_upload.php` | Device/session | Ingest `graph_session` NDJSON/JSON | Graphs PHP routes through crashes `public/index.php` (`?view=graphs`). ### Fake session data (dev / BE) ```bash # Generate NDJSON php examples/crash_reporter/backend/scripts/generate_fake_graph_sessions.php \ --devices=100 --per-device=6 --out=/tmp/sessions.ndjson # SQL import on BE (when HTTP upload times out) php examples/crash_reporter/backend/scripts/ndjson_to_graph_sql.php /tmp/sessions.ndjson | \ ssh alpine-be 'mariadb androidcast_crashes' ``` API smoke: `examples/crash_reporter/backend/scripts/test_graphs_api.sh` --- ## Remote access **Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=remote_access](https://apps.f0xx.org/app/androidcast_project/crashes/?view=remote_access) (RBAC: `remote_access_view`) Operator console: whitelist devices, open WireGuard or RSSH sessions. ### Related endpoints | Method | Endpoint | Auth | Purpose | |--------|----------|------|---------| | `POST` | `…/crashes/api/remote_access.php?action=…` | Session | `whitelist`, `open_session`, `dashboard`, … | | `POST` | `…/crashes/api/heartbeat.php` | None | Device RA poll (`heartbeat.type=ra`) | Device flow: app polls `heartbeat.php` → operator uses console → next poll returns `connect` payload. Smoke: `CRASHES_BASE=https://apps.f0xx.org/app/androidcast_project/crashes examples/crash_reporter/backend/scripts/test_remote_access_api.sh` Prod checklist: `examples/crash_reporter/backend/scripts/verify_remote_access_prod.sh` --- ## RBAC / access control **Web:** [https://apps.f0xx.org/app/androidcast_project/crashes/?view=rbac](https://apps.f0xx.org/app/androidcast_project/crashes/?view=rbac) (admin) ### Related endpoints | Method | Endpoint | Auth | Purpose | |--------|----------|------|---------| | `GET/POST` | `…/crashes/api/rbac.php` | Session | Roles, memberships, lockouts | | `GET/POST` | `…/crashes/api/users.php` | Session | User admin | Smoke: `examples/crash_reporter/backend/scripts/test_rbac_api.sh` --- ## APK builder **Web:** [https://apps.f0xx.org/app/androidcast_project/build/](https://apps.f0xx.org/app/androidcast_project/build/) Docker CI: trigger builds, stream logs, OTA artifact option. ### Related endpoints | Method | Endpoint | Auth | Purpose | |--------|----------|------|---------| | `GET` | `…/build/api/health.php` | Session | Docker/socket health | | `GET` | `…/build/api/builds.php` | Session | Build history | | `POST` | `…/build/api/build_trigger.php` | Session | Start build | | `GET` | `…/build/api/build_log.php?id=` | Session | Log tail | | `POST` | `…/build/api/build_actions.php` | Session | Cancel / rerun | Shared login with crashes (`ac_crash_sess`). See [BUILD_DEPLOY.md](BUILD_DEPLOY.md). --- ## Git (Gitea) **Web:** [https://apps.f0xx.org/app/androidcast_project/git/](https://apps.f0xx.org/app/androidcast_project/git/) FE proxies to `http://artc0.intra.raptor.org:3000/` (Gitea). Canonical git remote: `git://f0xx.org/android_cast`. No Android Cast PHP on this path. --- ## OTA updates **Web channel:** [https://apps.f0xx.org/v0/ota/channel/stable.json](https://apps.f0xx.org/v0/ota/channel/stable.json) Device setting: `ota.base_url` in `examples/settings.json` → `https://apps.f0xx.org/v0/ota/channel/stable.json`. ### Layout See [OTA.md](OTA.md): `v0/ota/channel/{stable,staging,…}.json` → manifest → `.otapkg` / `.otabundle.zip`. ### Publish (operator) ```bash ./scripts/generate-ota-v0.sh app/build/outputs/apk/release/app-release.apk \ https://apps.f0xx.org ./out/ota staging OTA_DEPLOY_TARGET=… ./scripts/deploy-ota-staging.sh ``` BE mount: `…/androidcast_project/ota-artifacts/v0/` — `prepare-be-ota-mount.sh`. **Status 2026-06-10:** FE `/v0/ota/` → BE :80 configured; channel file **not yet served** (404 / lander) — publish artifacts + verify BE nginx `location ^~ /v0/ota/`. --- ## Heartbeat (device) **Web:** none (app service only) **Endpoint:** `POST https://apps.f0xx.org/app/androidcast_project/crashes/api/heartbeat.php` JSON body with `heartbeat` object: session stats, remote-access poll (`type: ra`), etc. ```bash curl -sS -X POST -H 'Content-Type: application/json' \ -d '{"heartbeat":{"type":"ping","device_id":"test-device"}}' \ https://apps.f0xx.org/app/androidcast_project/crashes/api/heartbeat.php ``` --- ## Auth & account security | Page | URL | |------|-----| | Login | […/crashes/login](https://apps.f0xx.org/app/androidcast_project/crashes/login) | | Register | […/crashes/register](https://apps.f0xx.org/app/androidcast_project/crashes/register) | | Two-factor challenge | […/crashes/two-factor](https://apps.f0xx.org/app/androidcast_project/crashes/two-factor) | | Account security (TOTP enroll) | […/crashes/account-security](https://apps.f0xx.org/app/androidcast_project/crashes/account-security) | | Email verify | `…/crashes/verify-email?token=` | | Method | Endpoint | Purpose | |--------|----------|---------| | `POST` | `…/crashes/api/auth_register.php` | Registration API | TOTP verify script: `examples/crash_reporter/backend/scripts/verify_auth_totp.php` Flow doc: [20260607-2FA-email-mobile-auth-flow.md](20260607-2FA-email-mobile-auth-flow.md) --- ## URL shortener **Web:** [https://s.f0xx.org](https://s.f0xx.org) | Kind | Path | Notes | |------|------|--------| | Health | `GET /api/v1/health` | JSON `status: ok`, DB ping | | Shorten | `GET/POST /api/v1/shorten` | Bearer token required | | QR | `GET /api/v1/qr/{slug}.png` | PNG (`libqrencode-tools` on BE) | | Redirect | `GET /{slug}` | `302` → origin; `410` if expired | Spec: [specs/20100611_3_url_shortener.md](specs/20100611_3_url_shortener.md) · code: [backend/url-shortener/](../backend/url-shortener/) ```bash curl -fsS https://s.f0xx.org/api/v1/health # Bearer: php backend/url-shortener/scripts/seed_bearer.php (on BE after deploy) export BEARER=… BASE=https://s.f0xx.org backend/url-shortener/scripts/demo_curl.sh ``` --- ## Non-HTTP services | Service | Access | Used by | |---------|--------|---------| | **WireGuard** | `ra.apps.f0xx.org:45340/udp` | Remote access lab mode | | **RSSH bastion** | `ra.apps.f0xx.org:443/tcp` | Reverse SSH alpha mode | | **Cast LAN** | UDP/TCP `:41235`, discovery `:41234` | Screen mirror (no BE) | | **MQTT OTA** (optional) | `mqtt://host:port/v0/ota/…` | Retained topic payloads | --- ## Validation scripts | Script | Purpose | |--------|---------| | `validate_be_services.sh` | Public HTTP smoke (this doc’s snapshot) | | `test_gzip_upload.sh` | Crash upload plain + gzip | | `test_graphs_api.sh` | Graphs JSON API | | `test_remote_access_api.sh` | RA whitelist + session | | `test_rbac_api.sh` | RBAC API | | `test_rssh_api.sh` | RSSH mode API | | `test_tags_api.sh` | Report tag API | | `verify_remote_access_prod.sh` | Prod RA checklist | FE nginx template: `examples/crash_reporter/backend/nginx/fe/apps.conf` Regenerate FE patch: `scripts/generate-fe-nginx-patch.sh`