# 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

<!-- toc -->
- [Validation snapshot](#validation-snapshot)
- [Hub landing](#hub-landing)
- [Console home](#console-home)
- [Issues (crash reports)](#issues-crash-reports)
- [Tickets](#tickets)
- [Graphs](#graphs)
- [Remote access](#remote-access)
- [RBAC / access control](#rbac--access-control)
- [APK builder](#apk-builder)
- [Git (Gitea)](#git-gitea)
- [OTA updates](#ota-updates)
- [Heartbeat (device)](#heartbeat-device)
- [Auth & account security](#auth--account-security)
- [Non-HTTP services](#non-http-services)
- [Validation scripts](#validation-scripts)
<!-- /toc -->

---

## 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)

---

## 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`
