mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:18:09 +03:00
snap
This commit is contained in:
@@ -169,6 +169,157 @@
|
|||||||
z-index: 1;
|
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) {
|
@media (max-width: 860px) {
|
||||||
.hub-cards {
|
.hub-cards {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -16,6 +16,19 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="hub-page">
|
<body class="hub-page">
|
||||||
<div id="hub-logo-stage" class="hub-logo-stage" hidden aria-hidden="true"></div>
|
<div id="hub-logo-stage" class="hub-logo-stage" hidden aria-hidden="true"></div>
|
||||||
|
<button id="hub-compass-hit" class="hub-compass-hit" type="button" aria-label="Open deployment model"></button>
|
||||||
|
<div id="hub-docs-modal" class="hub-docs-modal" hidden aria-hidden="true">
|
||||||
|
<div class="hub-docs-backdrop" data-close-docs></div>
|
||||||
|
<section class="hub-docs-panel card card--lift" role="dialog" aria-modal="true" aria-labelledby="hub-docs-title">
|
||||||
|
<header class="hub-docs-header">
|
||||||
|
<h2 id="hub-docs-title">Deployment model</h2>
|
||||||
|
<button type="button" class="hub-docs-close" data-close-docs aria-label="Close">x</button>
|
||||||
|
</header>
|
||||||
|
<div id="hub-docs-body" class="hub-docs-body">
|
||||||
|
<p class="muted">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
<div class="shell hub-shell">
|
<div class="shell hub-shell">
|
||||||
<main class="main-pane hub-main">
|
<main class="main-pane hub-main">
|
||||||
<header class="hub-header">
|
<header class="hub-header">
|
||||||
@@ -120,6 +133,58 @@
|
|||||||
|
|
||||||
stage.hidden = false;
|
stage.hidden = false;
|
||||||
stage.setAttribute('aria-hidden', 'true');
|
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 = '<p class="muted">Loading...</p>';
|
||||||
|
openDocs();
|
||||||
|
fetch(url, { headers: { 'Accept': 'application/json' } })
|
||||||
|
.then(function (res) { return res.json(); })
|
||||||
|
.then(function (payload) {
|
||||||
|
if (!payload || !payload.ok) {
|
||||||
|
docsBody.innerHTML = '<p class="muted">Failed to load deployment model.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
docsBody.innerHTML = payload.html || '<p class="muted">No content.</p>';
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
docsBody.innerHTML = '<p class="muted">Network error while loading deployment model.</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
require_once __DIR__ . '/../../src/bootstrap.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
$section = strtolower(trim((string)($_GET['section'] ?? 'intro')));
|
||||||
|
if ($section === '') $section = 'intro';
|
||||||
|
|
||||||
|
function hub_docs_nav(): string {
|
||||||
|
return '<nav><strong>(2) TABLE OF CONTENTS</strong><ul>'
|
||||||
|
. '<li><a data-doc-link="intro" href="?section=intro">(1) INTRODUCTION</a></li>'
|
||||||
|
. '<li><a data-doc-link="deploy" href="?section=deploy">(3) LOCAL SINGLE-STEP TEST DEPLOYMENT</a></li>'
|
||||||
|
. '<li><a data-doc-link="orchestration" href="?section=orchestration">(4) ORCHESTRATION MODEL</a></li>'
|
||||||
|
. '<li><a data-doc-link="routing" href="?section=routing">(5) XHR SUPERVISED VIEW MODEL</a></li>'
|
||||||
|
. '<li><a data-doc-link="ops" href="?section=ops">(6) OPERATIONS CHECKLIST</a></li>'
|
||||||
|
. '</ul></nav>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hub_docs_intro(): string {
|
||||||
|
return '<h3>(1) INTRODUCTION</h3>'
|
||||||
|
. '<h4>(1.1) authors</h4>'
|
||||||
|
. '<p>Project owner: Anton Afanaasyeu. Implementation assistant: Cursor coding agent.</p>'
|
||||||
|
. '<h4>(1.2) revision details</h4>'
|
||||||
|
. '<p>Revision scope: landing compass interaction, XHR-driven deployment docs view, Docker orchestration model integration for next branch.</p>'
|
||||||
|
. hub_docs_nav();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hub_docs_deploy(): string {
|
||||||
|
return '<h3>(3) LOCAL SINGLE-STEP TEST DEPLOYMENT</h3>'
|
||||||
|
. '<h4>(3.1) preparations, environment</h4>'
|
||||||
|
. '<p>Prerequisites: docker + docker compose plugin, git checkout of android cast on branch <code>next</code>.</p>'
|
||||||
|
. '<p>Run one command from repo: <code>cd orchestration && ./deploy.sh</code></p>'
|
||||||
|
. '<h4>(3.2) package prerequisites by distro</h4>'
|
||||||
|
. '<table><thead><tr><th>Distro</th><th>Install steps</th></tr></thead><tbody>'
|
||||||
|
. '<tr><td>Alpine</td><td><code>apk add docker docker-cli-compose bash git curl</code><br><code>rc-update add docker default && service docker start</code></td></tr>'
|
||||||
|
. '<tr><td>Debian / Ubuntu</td><td><code>apt update && apt install -y docker.io docker-compose-plugin git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
||||||
|
. '<tr><td>Fedora</td><td><code>dnf install -y docker docker-compose-plugin git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
||||||
|
. '<tr><td>Arch</td><td><code>pacman -S --noconfirm docker docker-compose git curl</code><br><code>systemctl enable --now docker</code></td></tr>'
|
||||||
|
. '</tbody></table>'
|
||||||
|
. '<h4>(3.3) local checks</h4>'
|
||||||
|
. '<ul><li><code>http://localhost:8080/app/androidcast_project/</code> (hub)</li>'
|
||||||
|
. '<li><code>http://localhost:8080/app/androidcast_project/crashes/</code> (crashes/tasks)</li>'
|
||||||
|
. '<li><code>http://localhost:8080/app/androidcast_project/git/</code> (redirect to gitea FE)</li></ul>'
|
||||||
|
. hub_docs_nav();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hub_docs_orchestration(): string {
|
||||||
|
return '<h3>(4) ORCHESTRATION MODEL</h3>'
|
||||||
|
. '<p>All infra runs in docker with shared source bind-mount where possible.</p>'
|
||||||
|
. '<ul>'
|
||||||
|
. '<li>landing FE (nginx): serves hub and routes subpaths</li>'
|
||||||
|
. '<li>gitea FE (nginx) + gitea app</li>'
|
||||||
|
. '<li>crashes FE (nginx) + php-fpm + mariadb</li>'
|
||||||
|
. '<li>single command create/update: <code>orchestration/deploy.sh</code></li>'
|
||||||
|
. '</ul>'
|
||||||
|
. '<p>Base images: nginx, php-fpm, mariadb. This stays minimal and reproducible on laptop devices.</p>'
|
||||||
|
. hub_docs_nav();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hub_docs_routing(): string {
|
||||||
|
return '<h3>(5) XHR SUPERVISED VIEW MODEL</h3>'
|
||||||
|
. '<p>The Compass click opens a modal. Content is requested via XMLHttpRequest/fetch from BE endpoint '
|
||||||
|
. '<code>/app/androidcast_project/crashes/api/hub_deploy_docs.php</code>.</p>'
|
||||||
|
. '<p>Internal links in this modal use <code>data-doc-link</code> and are intercepted on frontend, then re-requested through BE. '
|
||||||
|
. 'Browser URL bar is not changed.</p>'
|
||||||
|
. '<p>Static assets remain at original paths and are not rewritten in this view.</p>'
|
||||||
|
. hub_docs_nav();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hub_docs_ops(): string {
|
||||||
|
return '<h3>(6) OPERATIONS CHECKLIST</h3>'
|
||||||
|
. '<ol>'
|
||||||
|
. '<li>Pull latest <code>next</code></li>'
|
||||||
|
. '<li>Run <code>orchestration/deploy.sh</code></li>'
|
||||||
|
. '<li>Verify hub/crashes/gitea endpoints</li>'
|
||||||
|
. '<li>For app changes: edit source in repo, rerun deploy script</li>'
|
||||||
|
. '</ol>'
|
||||||
|
. 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](),
|
||||||
|
]);
|
||||||
@@ -85,6 +85,11 @@ if ($route === '/api/ticket_attachment.php' || str_ends_with($route, '/api/ticke
|
|||||||
exit;
|
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') {
|
if ($route === '/logout') {
|
||||||
Auth::logout();
|
Auth::logout();
|
||||||
header('Location: ' . $base . '/login');
|
header('Location: ' . $base . '/login');
|
||||||
|
|||||||
17
orchestration/.env.example
Normal file
17
orchestration/.env.example
Normal 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
4
orchestration/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.env
|
||||||
|
runtime/*
|
||||||
|
!runtime/db-init/
|
||||||
|
!runtime/db-init/.gitkeep
|
||||||
64
orchestration/README.md
Normal file
64
orchestration/README.md
Normal 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
67
orchestration/cleanup.sh
Executable 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
86
orchestration/deploy.sh
Executable 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
|
||||||
96
orchestration/docker-compose.yml
Normal file
96
orchestration/docker-compose.yml
Normal 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:
|
||||||
7
orchestration/images/crash-php-fpm/Dockerfile
Normal file
7
orchestration/images/crash-php-fpm/Dockerfile
Normal 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
|
||||||
35
orchestration/nginx/crashes-fe.conf
Normal file
35
orchestration/nginx/crashes-fe.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
orchestration/nginx/gitea-fe.conf
Normal file
33
orchestration/nginx/gitea-fe.conf
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
29
orchestration/nginx/landing.conf
Normal file
29
orchestration/nginx/landing.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
orchestration/runtime/db-init/.gitkeep
Normal file
1
orchestration/runtime/db-init/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
127
orchestration/runtime/db-init/010_schema.sql
Normal file
127
orchestration/runtime/db-init/010_schema.sql
Normal 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');
|
||||||
19
orchestration/runtime/db-init/040_ticket_workflow.sql
Normal file
19
orchestration/runtime/db-init/040_ticket_workflow.sql
Normal 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;
|
||||||
21
orchestration/runtime/db-init/050_ticket_attachments.sql
Normal file
21
orchestration/runtime/db-init/050_ticket_attachments.sql
Normal 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;
|
||||||
34
orchestration/scripts/apply-hosts.sh
Executable file
34
orchestration/scripts/apply-hosts.sh
Executable 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."
|
||||||
Reference in New Issue
Block a user