1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:18:09 +03:00
This commit is contained in:
Anton Afanasyeu
2026-05-27 16:44:50 +02:00
parent 92fc65e02e
commit 43f802de5d
19 changed files with 957 additions and 0 deletions

View File

@@ -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

4
orchestration/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.env
runtime/*
!runtime/db-init/
!runtime/db-init/.gitkeep

64
orchestration/README.md Normal file
View File

@@ -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`.

67
orchestration/cleanup.sh Executable file
View File

@@ -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."

86
orchestration/deploy.sh Executable file
View File

@@ -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" <<EOF
<?php
return [
'app_name' => '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

View File

@@ -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:

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1 @@

View File

@@ -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');

View File

@@ -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;

View File

@@ -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;

View File

@@ -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."