From 43f802de5d971b8842925b6834dcdd0890fbb9de Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Wed, 27 May 2026 16:44:50 +0200 Subject: [PATCH] snap --- examples/app_hub/hub.css | 151 ++++++++++++++++++ examples/app_hub/index.html | 65 ++++++++ .../backend/public/api/hub_deploy_docs.php | 96 +++++++++++ .../crash_reporter/backend/public/index.php | 5 + orchestration/.env.example | 17 ++ orchestration/.gitignore | 4 + orchestration/README.md | 64 ++++++++ orchestration/cleanup.sh | 67 ++++++++ orchestration/deploy.sh | 86 ++++++++++ orchestration/docker-compose.yml | 96 +++++++++++ orchestration/images/crash-php-fpm/Dockerfile | 7 + orchestration/nginx/crashes-fe.conf | 35 ++++ orchestration/nginx/gitea-fe.conf | 33 ++++ orchestration/nginx/landing.conf | 29 ++++ orchestration/runtime/db-init/.gitkeep | 1 + orchestration/runtime/db-init/010_schema.sql | 127 +++++++++++++++ .../runtime/db-init/040_ticket_workflow.sql | 19 +++ .../db-init/050_ticket_attachments.sql | 21 +++ orchestration/scripts/apply-hosts.sh | 34 ++++ 19 files changed, 957 insertions(+) create mode 100644 examples/crash_reporter/backend/public/api/hub_deploy_docs.php create mode 100644 orchestration/.env.example create mode 100644 orchestration/.gitignore create mode 100644 orchestration/README.md create mode 100755 orchestration/cleanup.sh create mode 100755 orchestration/deploy.sh create mode 100644 orchestration/docker-compose.yml create mode 100644 orchestration/images/crash-php-fpm/Dockerfile create mode 100644 orchestration/nginx/crashes-fe.conf create mode 100644 orchestration/nginx/gitea-fe.conf create mode 100644 orchestration/nginx/landing.conf create mode 100644 orchestration/runtime/db-init/.gitkeep create mode 100644 orchestration/runtime/db-init/010_schema.sql create mode 100644 orchestration/runtime/db-init/040_ticket_workflow.sql create mode 100644 orchestration/runtime/db-init/050_ticket_attachments.sql create mode 100755 orchestration/scripts/apply-hosts.sh diff --git a/examples/app_hub/hub.css b/examples/app_hub/hub.css index 5ae8d07..17bcd6f 100644 --- a/examples/app_hub/hub.css +++ b/examples/app_hub/hub.css @@ -169,6 +169,157 @@ z-index: 1; } +.hub-compass-hit { + position: fixed; + left: calc(50% + 252px); + top: calc(50% + 226px); + width: 110px; + height: 110px; + z-index: 2; + border-radius: 50%; + border: 2px solid rgba(219, 229, 241, 0.45); + background: + radial-gradient(circle at 35% 35%, rgba(231, 236, 243, 0.35), rgba(10, 14, 20, 0.22) 68%, rgba(10, 14, 20, 0.1) 100%); + box-shadow: + -6px -8px 8px rgba(160, 215, 255, 0.12), + 8px 10px 14px rgba(9, 13, 19, 0.24); + cursor: pointer; + transform: translate(-50%, -50%) scale(1); + transition: transform 0.22s ease, border-width 0.22s ease, box-shadow 0.22s ease; +} + +.hub-compass-hit::before, +.hub-compass-hit::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + border-radius: 1px; + background: rgba(231, 236, 243, 0.82); + transition: width 0.22s ease, height 0.22s ease; +} + +.hub-compass-hit::before { + width: 3px; + height: 46px; +} + +.hub-compass-hit::after { + width: 46px; + height: 3px; +} + +.hub-compass-hit:hover, +.hub-compass-hit:focus-visible { + transform: translate(-50%, -50%) scale(1.25); + border-width: 2.3px; + box-shadow: + -8px -10px 10px rgba(180, 228, 255, 0.22), + 14px 16px 22px rgba(7, 10, 16, 0.34), + 22px 26px 34px rgba(6, 8, 14, 0.42); + outline: none; +} + +.hub-compass-hit:hover::before, +.hub-compass-hit:hover::after, +.hub-compass-hit:focus-visible::before, +.hub-compass-hit:focus-visible::after { + width: 52px; + height: 3.45px; +} + +.hub-compass-hit:hover::before, +.hub-compass-hit:focus-visible::before { + width: 3.45px; + height: 52px; +} + +.hub-docs-modal { + position: fixed; + inset: 0; + z-index: 20; +} + +.hub-docs-backdrop { + position: absolute; + inset: 0; + background: rgba(4, 8, 14, 0.66); +} + +.hub-docs-panel { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: min(980px, calc(100vw - 40px)); + max-height: calc(100vh - 56px); + display: flex; + flex-direction: column; +} + +.hub-docs-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 16px 10px; + border-bottom: 1px solid var(--border); +} + +.hub-docs-header h2 { + margin: 0; + font-size: 1.15rem; + font-weight: 700; +} + +.hub-docs-close { + border: 1px solid var(--border); + background: var(--bg2); + color: var(--text); + border-radius: 8px; + width: 32px; + height: 32px; + cursor: pointer; +} + +.hub-docs-body { + overflow: auto; + padding: 16px; + font-size: 14px; + line-height: 1.5; +} + +.hub-docs-body h3 { + margin: 16px 0 8px; + font-size: 1.03rem; +} + +.hub-docs-body h4 { + margin: 12px 0 6px; + font-size: 0.96rem; +} + +.hub-docs-body table { + width: 100%; + border-collapse: collapse; + margin: 10px 0 14px; +} + +.hub-docs-body th, +.hub-docs-body td { + border: 1px solid var(--border); + padding: 8px; + text-align: left; + vertical-align: top; +} + +.hub-docs-body a[data-doc-link] { + color: var(--accent); + text-decoration: underline; + cursor: pointer; +} + @media (max-width: 860px) { .hub-cards { grid-template-columns: 1fr; diff --git a/examples/app_hub/index.html b/examples/app_hub/index.html index d97df20..0070a26 100644 --- a/examples/app_hub/index.html +++ b/examples/app_hub/index.html @@ -16,6 +16,19 @@ + +
@@ -120,6 +133,58 @@ stage.hidden = false; stage.setAttribute('aria-hidden', 'true'); + + var docsModal = document.getElementById('hub-docs-modal'); + var docsBody = document.getElementById('hub-docs-body'); + var compassHit = document.getElementById('hub-compass-hit'); + if (!docsModal || !docsBody || !compassHit) return; + + function closeDocs() { + docsModal.hidden = true; + docsModal.setAttribute('aria-hidden', 'true'); + } + + function openDocs() { + docsModal.hidden = false; + docsModal.setAttribute('aria-hidden', 'false'); + } + + function loadDocs(section) { + var url = '/app/androidcast_project/crashes/api/hub_deploy_docs.php'; + if (section) url += '?section=' + encodeURIComponent(section); + docsBody.innerHTML = '

Loading...

'; + openDocs(); + fetch(url, { headers: { 'Accept': 'application/json' } }) + .then(function (res) { return res.json(); }) + .then(function (payload) { + if (!payload || !payload.ok) { + docsBody.innerHTML = '

Failed to load deployment model.

'; + return; + } + docsBody.innerHTML = payload.html || '

No content.

'; + }) + .catch(function () { + docsBody.innerHTML = '

Network error while loading deployment model.

'; + }); + } + + compassHit.addEventListener('click', function () { loadDocs('intro'); }); + docsModal.addEventListener('click', function (ev) { + var target = ev.target; + if (!(target instanceof Element)) return; + if (target.hasAttribute('data-close-docs')) { + closeDocs(); + return; + } + var link = target.closest('[data-doc-link]'); + if (link) { + ev.preventDefault(); + loadDocs(link.getAttribute('data-doc-link') || 'intro'); + } + }); + document.addEventListener('keydown', function (ev) { + if (ev.key === 'Escape' && !docsModal.hidden) closeDocs(); + }); })(); diff --git a/examples/crash_reporter/backend/public/api/hub_deploy_docs.php b/examples/crash_reporter/backend/public/api/hub_deploy_docs.php new file mode 100644 index 0000000..e0001b7 --- /dev/null +++ b/examples/crash_reporter/backend/public/api/hub_deploy_docs.php @@ -0,0 +1,96 @@ +(2) TABLE OF CONTENTS'; +} + +function hub_docs_intro(): string { + return '

(1) INTRODUCTION

' + . '

(1.1) authors

' + . '

Project owner: Anton Afanaasyeu. Implementation assistant: Cursor coding agent.

' + . '

(1.2) revision details

' + . '

Revision scope: landing compass interaction, XHR-driven deployment docs view, Docker orchestration model integration for next branch.

' + . hub_docs_nav(); +} + +function hub_docs_deploy(): string { + return '

(3) LOCAL SINGLE-STEP TEST DEPLOYMENT

' + . '

(3.1) preparations, environment

' + . '

Prerequisites: docker + docker compose plugin, git checkout of android cast on branch next.

' + . '

Run one command from repo: cd orchestration && ./deploy.sh

' + . '

(3.2) package prerequisites by distro

' + . '' + . '' + . '' + . '' + . '' + . '
DistroInstall steps
Alpineapk add docker docker-cli-compose bash git curl
rc-update add docker default && service docker start
Debian / Ubuntuapt update && apt install -y docker.io docker-compose-plugin git curl
systemctl enable --now docker
Fedoradnf install -y docker docker-compose-plugin git curl
systemctl enable --now docker
Archpacman -S --noconfirm docker docker-compose git curl
systemctl enable --now docker
' + . '

(3.3) local checks

' + . '
  • http://localhost:8080/app/androidcast_project/ (hub)
  • ' + . '
  • http://localhost:8080/app/androidcast_project/crashes/ (crashes/tasks)
  • ' + . '
  • http://localhost:8080/app/androidcast_project/git/ (redirect to gitea FE)
' + . hub_docs_nav(); +} + +function hub_docs_orchestration(): string { + return '

(4) ORCHESTRATION MODEL

' + . '

All infra runs in docker with shared source bind-mount where possible.

' + . '
    ' + . '
  • landing FE (nginx): serves hub and routes subpaths
  • ' + . '
  • gitea FE (nginx) + gitea app
  • ' + . '
  • crashes FE (nginx) + php-fpm + mariadb
  • ' + . '
  • single command create/update: orchestration/deploy.sh
  • ' + . '
' + . '

Base images: nginx, php-fpm, mariadb. This stays minimal and reproducible on laptop devices.

' + . hub_docs_nav(); +} + +function hub_docs_routing(): string { + return '

(5) XHR SUPERVISED VIEW MODEL

' + . '

The Compass click opens a modal. Content is requested via XMLHttpRequest/fetch from BE endpoint ' + . '/app/androidcast_project/crashes/api/hub_deploy_docs.php.

' + . '

Internal links in this modal use data-doc-link and are intercepted on frontend, then re-requested through BE. ' + . 'Browser URL bar is not changed.

' + . '

Static assets remain at original paths and are not rewritten in this view.

' + . hub_docs_nav(); +} + +function hub_docs_ops(): string { + return '

(6) OPERATIONS CHECKLIST

' + . '
    ' + . '
  1. Pull latest next
  2. ' + . '
  3. Run orchestration/deploy.sh
  4. ' + . '
  5. Verify hub/crashes/gitea endpoints
  6. ' + . '
  7. For app changes: edit source in repo, rerun deploy script
  8. ' + . '
' + . hub_docs_nav(); +} + +$map = [ + 'intro' => 'hub_docs_intro', + 'deploy' => 'hub_docs_deploy', + 'orchestration' => 'hub_docs_orchestration', + 'routing' => 'hub_docs_routing', + 'ops' => 'hub_docs_ops', +]; + +if (!isset($map[$section])) $section = 'intro'; + +echo safe_json_encode([ + 'ok' => true, + 'section' => $section, + 'html' => $map[$section](), +]); diff --git a/examples/crash_reporter/backend/public/index.php b/examples/crash_reporter/backend/public/index.php index 6da630b..25859cd 100644 --- a/examples/crash_reporter/backend/public/index.php +++ b/examples/crash_reporter/backend/public/index.php @@ -85,6 +85,11 @@ if ($route === '/api/ticket_attachment.php' || str_ends_with($route, '/api/ticke exit; } +if ($route === '/api/hub_deploy_docs.php' || str_ends_with($route, '/api/hub_deploy_docs.php')) { + require __DIR__ . '/api/hub_deploy_docs.php'; + exit; +} + if ($route === '/logout') { Auth::logout(); header('Location: ' . $base . '/login'); diff --git a/orchestration/.env.example b/orchestration/.env.example new file mode 100644 index 0000000..0cebba8 --- /dev/null +++ b/orchestration/.env.example @@ -0,0 +1,17 @@ +COMPOSE_PROJECT_NAME=androidcast_local + +# Public entry (landing FE) +LANDING_HTTP_PORT=8080 + +# Optional direct debug ports +GITEA_FE_PORT=8081 +CRASHES_FE_PORT=8082 + +# Internal app creds (local dev defaults) +MARIADB_ROOT_PASSWORD=androidcast_root +MARIADB_DATABASE=androidcast_crashes +MARIADB_USER=androidcast +MARIADB_PASSWORD=androidcast + +# Used for generated crash backend config + gitea ROOT_URL +PUBLIC_BASE_URL=http://localhost:8080 diff --git a/orchestration/.gitignore b/orchestration/.gitignore new file mode 100644 index 0000000..cb026c8 --- /dev/null +++ b/orchestration/.gitignore @@ -0,0 +1,4 @@ +.env +runtime/* +!runtime/db-init/ +!runtime/db-init/.gitkeep diff --git a/orchestration/README.md b/orchestration/README.md new file mode 100644 index 0000000..9c6d489 --- /dev/null +++ b/orchestration/README.md @@ -0,0 +1,64 @@ +# Local orchestration (one-click) + +This folder runs the Android Cast web stack locally without external DNS/server: + +- Landing hub FE (nginx) +- Gitea FE (nginx) + Gitea app +- Crashes/tickets FE (nginx) + PHP-FPM + MariaDB + +All services run in Docker and share the same repo checkout via bind mounts where possible. + +## Quick start + +```bash +cd orchestration +./deploy.sh +``` + +To fully stop and clean all orchestration resources on demand: + +```bash +./cleanup.sh +``` + +That single command does both: + +1. First run: creates config/runtime directories and starts containers +2. Next runs: rebuilds/updates and restarts changed services + +## Local URLs + +- Landing (hub + routed subapps): `http://localhost:8080/app/androidcast_project/` +- Gitea direct FE: `http://localhost:8081/` +- Crashes direct FE (optional debug): `http://localhost:8082/app/androidcast_project/crashes/` + +Under landing, subpaths are proxied: + +- `.../git/` -> HTTP redirect to `http://localhost:8081/` (dedicated gitea FE) +- `.../crashes/` -> crashes-fe + +## Shared code base + +- Hub and crash backend code are mounted from the current repo checkout into containers. +- Editing local files is reflected immediately (no image rebuild for static/PHP code paths). +- Gitea data uses persistent Docker volumes (`gitea-data`, `gitea-db` style via named volumes where applicable). + +## Configuration + +Copy `.env.example` to `.env` and tune if needed: + +```bash +cp .env.example .env +``` + +Defaults are safe for local laptop use and avoid host DNS changes by using localhost + ports. + +## Optional hosts aliases + +No host edits are required. If you prefer hostnames, use: + +```bash +./scripts/apply-hosts.sh +``` + +This adds/removes localhost aliases in `/etc/hosts`. diff --git a/orchestration/cleanup.sh b/orchestration/cleanup.sh new file mode 100755 index 0000000..44ce6a5 --- /dev/null +++ b/orchestration/cleanup.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="$ROOT_DIR/docker-compose.yml" +ENV_FILE="$ROOT_DIR/.env" +ENV_EXAMPLE="$ROOT_DIR/.env.example" +RUNTIME_DIR="$ROOT_DIR/runtime" + +if ! command -v docker >/dev/null 2>&1; then + echo "docker not found in PATH" + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "docker compose plugin is required" + exit 1 +fi + +if [[ ! -f "$ENV_FILE" && -f "$ENV_EXAMPLE" ]]; then + cp "$ENV_EXAMPLE" "$ENV_FILE" +fi + +set -a +if [[ -f "$ENV_FILE" ]]; then + # shellcheck disable=SC1090 + source "$ENV_FILE" +fi +set +a + +PROJECT_NAME="${COMPOSE_PROJECT_NAME:-androidcast_local}" + +echo "Stopping and removing orchestration stack..." +docker compose -f "$COMPOSE_FILE" down --remove-orphans --volumes --rmi local || true + +echo "Removing leftover containers with compose project label..." +docker ps -aq --filter "label=com.docker.compose.project=${PROJECT_NAME}" | while read -r cid; do + [[ -n "$cid" ]] && docker rm -f "$cid" || true +done + +echo "Removing leftover compose networks..." +docker network ls -q --filter "label=com.docker.compose.project=${PROJECT_NAME}" | while read -r nid; do + [[ -n "$nid" ]] && docker network rm "$nid" || true +done + +echo "Removing leftover compose volumes..." +docker volume ls -q --filter "label=com.docker.compose.project=${PROJECT_NAME}" | while read -r vid; do + [[ -n "$vid" ]] && docker volume rm "$vid" || true +done + +echo "Removing orchestration screen sessions if present..." +if command -v screen >/dev/null 2>&1; then + screen_output="$(screen -ls 2>/dev/null || true)" + printf '%s\n' "$screen_output" | awk '/androidcast|orchestration/ {print $1}' | while read -r session; do + [[ -n "$session" ]] && screen -S "$session" -X quit || true + done +fi + +echo "Cleaning generated runtime artifacts..." +rm -f "$RUNTIME_DIR/crash-config.php" +rm -f "$RUNTIME_DIR/db-init/010_schema.sql" \ + "$RUNTIME_DIR/db-init/040_ticket_workflow.sql" \ + "$RUNTIME_DIR/db-init/050_ticket_attachments.sql" +mkdir -p "$RUNTIME_DIR/db-init" +touch "$RUNTIME_DIR/db-init/.gitkeep" + +echo "Orchestration cleanup complete." diff --git a/orchestration/deploy.sh b/orchestration/deploy.sh new file mode 100755 index 0000000..1a59ce3 --- /dev/null +++ b/orchestration/deploy.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$ROOT_DIR/.env" +ENV_EXAMPLE="$ROOT_DIR/.env.example" +RUNTIME_DIR="$ROOT_DIR/runtime" +DB_INIT_DIR="$RUNTIME_DIR/db-init" +CRASH_CONFIG="$RUNTIME_DIR/crash-config.php" + +SRC_SCHEMA="$ROOT_DIR/../examples/crash_reporter/backend/sql/schema.mariadb.sql" +SRC_MIG_004="$ROOT_DIR/../examples/crash_reporter/backend/sql/migrations/004_ticket_workflow.sql" +SRC_MIG_005="$ROOT_DIR/../examples/crash_reporter/backend/sql/migrations/005_ticket_attachments.sql" + +if ! command -v docker >/dev/null 2>&1; then + echo "docker not found in PATH" + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "docker compose plugin is required" + exit 1 +fi + +if [[ ! -f "$ENV_FILE" ]]; then + cp "$ENV_EXAMPLE" "$ENV_FILE" + echo "Created .env from .env.example" +fi + +set -a +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +a + +mkdir -p "$DB_INIT_DIR" + +cat >"$CRASH_CONFIG" < 'Android Cast Crashes', + 'base_path' => '/app/androidcast_project/crashes', + 'db' => [ + 'driver' => 'mysql', + 'sqlite_path' => __DIR__ . '/../data/crashes.sqlite', + 'mysql' => [ + 'host' => 'mariadb', + 'port' => 3306, + 'socket' => '', + 'database' => '${MARIADB_DATABASE}', + 'username' => '${MARIADB_USER}', + 'password' => '${MARIADB_PASSWORD}', + 'charset' => 'utf8mb4', + ], + ], + 'session_name' => 'ac_crash_sess', + 'debug' => true, + 'rbac' => [ + 'default_company_slug' => 'default', + 'default_company_id' => 1, + ], +]; +EOF + +cp "$SRC_SCHEMA" "$DB_INIT_DIR/010_schema.sql" +{ + echo "USE \`${MARIADB_DATABASE}\`;" + cat "$SRC_MIG_004" +} >"$DB_INIT_DIR/040_ticket_workflow.sql" +{ + echo "USE \`${MARIADB_DATABASE}\`;" + cat "$SRC_MIG_005" +} >"$DB_INIT_DIR/050_ticket_attachments.sql" + +echo "Pulling images..." +docker compose -f "$ROOT_DIR/docker-compose.yml" pull landing-fe gitea-fe crashes-fe mariadb gitea || true + +echo "Starting/updating stack..." +docker compose -f "$ROOT_DIR/docker-compose.yml" up -d --build --remove-orphans --force-recreate + +echo +echo "Stack is up." +echo "Landing: ${PUBLIC_BASE_URL}/app/androidcast_project/" +echo "Gitea (direct FE): http://localhost:${GITEA_FE_PORT}/app/androidcast_project/git/" +echo "Crashes (direct FE): http://localhost:${CRASHES_FE_PORT}/app/androidcast_project/crashes/" +echo +docker compose -f "$ROOT_DIR/docker-compose.yml" ps diff --git a/orchestration/docker-compose.yml b/orchestration/docker-compose.yml new file mode 100644 index 0000000..9620c83 --- /dev/null +++ b/orchestration/docker-compose.yml @@ -0,0 +1,96 @@ +services: + landing-fe: + image: nginx:1.27-alpine + depends_on: + - gitea-fe + - crashes-fe + ports: + - "${LANDING_HTTP_PORT}:8080" + volumes: + - ../:/workspace:ro + - ./nginx/landing.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - appnet + restart: unless-stopped + + gitea-fe: + image: nginx:1.27-alpine + depends_on: + - gitea + ports: + - "${GITEA_FE_PORT}:8080" + volumes: + - ./nginx/gitea-fe.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - appnet + restart: unless-stopped + + crashes-fe: + image: nginx:1.27-alpine + depends_on: + - crash-php + ports: + - "${CRASHES_FE_PORT}:8080" + volumes: + - ../:/workspace + - ./nginx/crashes-fe.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - appnet + restart: unless-stopped + + crash-php: + build: + context: . + dockerfile: images/crash-php-fpm/Dockerfile + depends_on: + - mariadb + volumes: + - ../:/workspace + - ./runtime/crash-config.php:/workspace/examples/crash_reporter/backend/config/config.php + networks: + - appnet + restart: unless-stopped + + mariadb: + image: mariadb:11.4 + environment: + MARIADB_ROOT_PASSWORD: "${MARIADB_ROOT_PASSWORD}" + MARIADB_DATABASE: "${MARIADB_DATABASE}" + MARIADB_USER: "${MARIADB_USER}" + MARIADB_PASSWORD: "${MARIADB_PASSWORD}" + command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"] + volumes: + - mariadb-data:/var/lib/mysql + - ./runtime/db-init:/docker-entrypoint-initdb.d:ro + networks: + - appnet + restart: unless-stopped + + gitea: + image: gitea/gitea:1.22 + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__server__ROOT_URL: "http://localhost:${GITEA_FE_PORT}/" + GITEA__server__DOMAIN: "localhost" + GITEA__server__HTTP_PORT: "3000" + GITEA__server__PROTOCOL: "http" + GITEA__security__INSTALL_LOCK: "true" + GITEA__service__DISABLE_REGISTRATION: "true" + GITEA__database__DB_TYPE: "sqlite3" + volumes: + - gitea-data:/data + networks: + - appnet + restart: unless-stopped + +networks: + appnet: + driver: bridge + ipam: + config: + - subnet: 10.203.0.0/24 + +volumes: + mariadb-data: + gitea-data: diff --git a/orchestration/images/crash-php-fpm/Dockerfile b/orchestration/images/crash-php-fpm/Dockerfile new file mode 100644 index 0000000..d063fa9 --- /dev/null +++ b/orchestration/images/crash-php-fpm/Dockerfile @@ -0,0 +1,7 @@ +FROM php:8.2-fpm-alpine + +RUN apk add --no-cache bash icu-dev libzip-dev oniguruma-dev mariadb-client sqlite-dev \ + && docker-php-ext-install pdo pdo_mysql pdo_sqlite \ + && rm -rf /var/cache/apk/* + +WORKDIR /workspace/examples/crash_reporter/backend diff --git a/orchestration/nginx/crashes-fe.conf b/orchestration/nginx/crashes-fe.conf new file mode 100644 index 0000000..36c2e94 --- /dev/null +++ b/orchestration/nginx/crashes-fe.conf @@ -0,0 +1,35 @@ +server { + listen 8080; + server_name _; + + client_max_body_size 16m; + index index.php; + + location = / { + return 302 /app/androidcast_project/crashes/; + } + + location /app/androidcast_project/crashes/ { + alias /workspace/examples/crash_reporter/backend/public/; + index index.php; + try_files $uri $uri/ @crashes_front; + } + + location @crashes_front { + rewrite ^ /app/androidcast_project/crashes/index.php?$query_string last; + } + + location ~ ^/app/androidcast_project/crashes/(.+\.php)$ { + alias /workspace/examples/crash_reporter/backend/public/$1; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /workspace/examples/crash_reporter/backend/public/$1; + fastcgi_param SCRIPT_NAME /app/androidcast_project/crashes/$1; + fastcgi_param DOCUMENT_ROOT /workspace/examples/crash_reporter/backend/public; + fastcgi_pass crash-php:9000; + fastcgi_read_timeout 120s; + } + + location ~ /\. { + deny all; + } +} diff --git a/orchestration/nginx/gitea-fe.conf b/orchestration/nginx/gitea-fe.conf new file mode 100644 index 0000000..d38e889 --- /dev/null +++ b/orchestration/nginx/gitea-fe.conf @@ -0,0 +1,33 @@ +server { + listen 8080; + server_name _; + + client_max_body_size 64m; + + location /app/androidcast_project/git/ { + rewrite ^/app/androidcast_project/git/?(.*)$ /$1 break; + proxy_pass http://gitea:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + location / { + proxy_pass http://gitea:3000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} diff --git a/orchestration/nginx/landing.conf b/orchestration/nginx/landing.conf new file mode 100644 index 0000000..5c5f06a --- /dev/null +++ b/orchestration/nginx/landing.conf @@ -0,0 +1,29 @@ +server { + listen 8080; + server_name _; + + client_max_body_size 16m; + + location = / { + return 302 /app/androidcast_project/; + } + + location /app/androidcast_project/git/ { + return 302 http://localhost:8081/; + } + + location /app/androidcast_project/crashes/ { + proxy_pass http://crashes-fe:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /app/androidcast_project/ { + alias /workspace/examples/app_hub/; + index index.html; + try_files $uri $uri/ /app/androidcast_project/index.html; + } +} diff --git a/orchestration/runtime/db-init/.gitkeep b/orchestration/runtime/db-init/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/orchestration/runtime/db-init/.gitkeep @@ -0,0 +1 @@ + diff --git a/orchestration/runtime/db-init/010_schema.sql b/orchestration/runtime/db-init/010_schema.sql new file mode 100644 index 0000000..0722610 --- /dev/null +++ b/orchestration/runtime/db-init/010_schema.sql @@ -0,0 +1,127 @@ +-- Fresh MariaDB install for Android Cast crash reporter. +-- Run as root: mysql -u root -p < sql/schema.mariadb.sql +-- App user needs: SELECT, INSERT, UPDATE, DELETE on androidcast_crashes.* + +CREATE DATABASE IF NOT EXISTS androidcast_crashes + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE androidcast_crashes; + +CREATE TABLE IF NOT EXISTS companies ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + slug VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_companies_slug (slug) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS users ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(64) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role ENUM('root', 'platform_admin', 'admin', 'viewer') NOT NULL DEFAULT 'viewer', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_users_username (username) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS company_memberships ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NOT NULL, + company_id INT UNSIGNED NOT NULL, + role ENUM('owner', 'admin', 'member') NOT NULL DEFAULT 'member', + permissions_json LONGTEXT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_membership_user_company (user_id, company_id), + KEY idx_membership_company (company_id), + CONSTRAINT fk_membership_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + CONSTRAINT fk_membership_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS devices ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + company_id INT UNSIGNED NOT NULL, + external_id VARCHAR(128) NOT NULL, + display_name VARCHAR(256) NULL, + manufacturer VARCHAR(128) NULL, + model VARCHAR(128) NULL, + source ENUM('auto', 'manual', 'barcode') NOT NULL DEFAULT 'auto', + registered_at_ms BIGINT NOT NULL, + last_seen_at_ms BIGINT NOT NULL, + meta_json LONGTEXT NULL, + UNIQUE KEY uq_devices_company_external (company_id, external_id), + KEY idx_devices_company (company_id), + CONSTRAINT fk_devices_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS reports ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + company_id INT UNSIGNED NOT NULL DEFAULT 1, + device_id BIGINT UNSIGNED NULL, + report_id VARCHAR(128) NOT NULL, + fingerprint VARCHAR(64) NOT NULL, + crash_type VARCHAR(32) NOT NULL, + generated_at_ms BIGINT NOT NULL, + received_at_ms BIGINT NOT NULL, + device_model VARCHAR(128) NULL, + app_version VARCHAR(64) NULL, + payload_json LONGTEXT NOT NULL, + tags_json LONGTEXT NOT NULL, + UNIQUE KEY uq_reports_report_id (report_id), + KEY idx_reports_company (company_id), + KEY idx_reports_device (device_id), + KEY idx_reports_generated (generated_at_ms), + KEY idx_reports_received (received_at_ms), + KEY idx_reports_fingerprint (fingerprint), + KEY idx_reports_fp_received (fingerprint, received_at_ms), + CONSTRAINT fk_reports_company FOREIGN KEY (company_id) REFERENCES companies (id), + CONSTRAINT fk_reports_device FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS report_views ( + user_id INT UNSIGNED NOT NULL, + report_id BIGINT UNSIGNED NOT NULL, + viewed_at_ms BIGINT NOT NULL, + PRIMARY KEY (user_id, report_id), + CONSTRAINT fk_report_views_report + FOREIGN KEY (report_id) REFERENCES reports (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS tickets ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + company_id INT UNSIGNED NOT NULL DEFAULT 1, + device_id BIGINT UNSIGNED NULL, + ticket_id VARCHAR(128) NOT NULL, + title VARCHAR(256) NOT NULL, + brief TEXT NULL, + body LONGTEXT NOT NULL, + opened_at_ms BIGINT NOT NULL, + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + owner_username VARCHAR(64) NOT NULL DEFAULT '', + verified_by_username VARCHAR(64) NULL, + device_model VARCHAR(128) NULL, + app_package VARCHAR(128) NULL, + app_version VARCHAR(64) NULL, + os_name VARCHAR(64) NULL, + os_version VARCHAR(128) NULL, + rating TINYINT UNSIGNED NOT NULL DEFAULT 0, + tags_json LONGTEXT NOT NULL, + payload_json LONGTEXT NOT NULL, + UNIQUE KEY uq_tickets_ticket_id (ticket_id), + KEY idx_tickets_company (company_id), + KEY idx_tickets_opened (opened_at_ms), + KEY idx_tickets_updated (updated_at_ms), + KEY idx_tickets_owner (owner_username), + CONSTRAINT fk_tickets_company FOREIGN KEY (company_id) REFERENCES companies (id), + CONSTRAINT fk_tickets_device FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default'); + +INSERT IGNORE INTO users (id, username, password_hash, role) +VALUES (1, 'admin', '$2y$10$t2JyYCyjBwRxAHfmLwsnG.APoj/in0i6nxpKcQggdl.AkhFJR13o.', 'root'); +-- default password: admin — change in production + +INSERT IGNORE INTO company_memberships (user_id, company_id, role) +VALUES (1, 1, 'owner'); diff --git a/orchestration/runtime/db-init/040_ticket_workflow.sql b/orchestration/runtime/db-init/040_ticket_workflow.sql new file mode 100644 index 0000000..ceee3bd --- /dev/null +++ b/orchestration/runtime/db-init/040_ticket_workflow.sql @@ -0,0 +1,19 @@ +USE `androidcast_crashes`; +-- Ticket workflow, assignees, comments. Run as MySQL root on existing DBs. +-- mysql -u root -p androidcast_crashes < sql/migrations/004_ticket_workflow.sql + +ALTER TABLE tickets + ADD COLUMN workflow_state VARCHAR(32) NOT NULL DEFAULT 'triage' AFTER tags_json, + ADD COLUMN assignees_json LONGTEXT NOT NULL DEFAULT '[]' AFTER workflow_state; + +CREATE TABLE IF NOT EXISTS ticket_comments ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + ticket_id BIGINT UNSIGNED NOT NULL, + author_username VARCHAR(64) NOT NULL, + body LONGTEXT NOT NULL, + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + KEY idx_ticket_comments_ticket (ticket_id), + KEY idx_ticket_comments_created (created_at_ms), + CONSTRAINT fk_ticket_comments_ticket FOREIGN KEY (ticket_id) REFERENCES tickets (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/orchestration/runtime/db-init/050_ticket_attachments.sql b/orchestration/runtime/db-init/050_ticket_attachments.sql new file mode 100644 index 0000000..cc898cd --- /dev/null +++ b/orchestration/runtime/db-init/050_ticket_attachments.sql @@ -0,0 +1,21 @@ +USE `androidcast_crashes`; +-- Ticket attachments: uploaded files + external links (incl. Google URLs). +-- mysql -u root -p androidcast_crashes < sql/migrations/005_ticket_attachments.sql + +CREATE TABLE IF NOT EXISTS ticket_attachments ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + ticket_id BIGINT UNSIGNED NOT NULL, + kind ENUM('file', 'link') NOT NULL, + title VARCHAR(256) NOT NULL DEFAULT '', + external_url TEXT NULL, + provider VARCHAR(32) NOT NULL DEFAULT 'generic', + storage_name VARCHAR(128) NULL, + original_name VARCHAR(256) NULL, + mime_type VARCHAR(128) NULL, + size_bytes BIGINT UNSIGNED NULL, + created_by VARCHAR(64) NOT NULL, + created_at_ms BIGINT NOT NULL, + KEY idx_ticket_attachments_ticket (ticket_id), + KEY idx_ticket_attachments_created (created_at_ms), + CONSTRAINT fk_ticket_attachments_ticket FOREIGN KEY (ticket_id) REFERENCES tickets (id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/orchestration/scripts/apply-hosts.sh b/orchestration/scripts/apply-hosts.sh new file mode 100755 index 0000000..410d24f --- /dev/null +++ b/orchestration/scripts/apply-hosts.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +MODE="${1:-add}" +HOSTS_FILE="/etc/hosts" +BEGIN_MARK="# >>> androidcast-local >>>" +END_MARK="# <<< androidcast-local <<<" + +BLOCK=$(cat <<'EOF' +# >>> androidcast-local >>> +127.0.0.1 apps.androidcast.local +127.0.0.1 git.androidcast.local +127.0.0.1 crashes.androidcast.local +# <<< androidcast-local <<< +EOF +) + +if [[ "$MODE" == "remove" ]]; then + sudo awk -v begin="$BEGIN_MARK" -v end="$END_MARK" ' + $0==begin {skip=1; next} + $0==end {skip=0; next} + !skip {print} + ' "$HOSTS_FILE" | sudo tee "$HOSTS_FILE" >/dev/null + echo "Removed androidcast-local hosts entries." + exit 0 +fi + +if grep -Fq "$BEGIN_MARK" "$HOSTS_FILE"; then + echo "Entries already exist in $HOSTS_FILE" + exit 0 +fi + +printf "\n%s\n" "$BLOCK" | sudo tee -a "$HOSTS_FILE" >/dev/null +echo "Added androidcast-local hosts entries."