mirror of
git://f0xx.org/android_cast
synced 2026-07-29 03:57:50 +03:00
new stuff
This commit is contained in:
35
.cursor/rules/be-deploy-alerts.mdc
Normal file
35
.cursor/rules/be-deploy-alerts.mdc
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
description: Prominent BE/DB deploy alerts when crash reporter or backend changes need server action
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# BE and database deploy alerts
|
||||
|
||||
When your work touches the crash reporter backend, SQLite schema, nginx, or PHP on the BE (`apps.f0xx.org`), **always** end the relevant reply with a clearly separated block in **BIG CAPITAL LETTERS** if any of the following apply:
|
||||
|
||||
## Trigger — show the alert when
|
||||
|
||||
- **Database**: new/changed tables or columns (`schema.sqlite.sql`, `Database::ensureSchema`, `ALTER TABLE`, `tags_json`, `report_views`, migrations, or data-dir permissions).
|
||||
- **BE deploy**: PHP/views/JS/CSS/config must be copied or synced to the Alpine tree under `android_cast/examples/crash_reporter/backend/`.
|
||||
- **Hard reload on server**: nginx config fragment changed → `nginx -t` + reload; PHP-FPM restart after extension/config changes; opcache may need FPM restart.
|
||||
- **Browser**: static assets (`app.js`, `app.css`) changed → user should hard-refresh after BE files are updated.
|
||||
|
||||
## Format (use exactly this style)
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════════════════
|
||||
⚠️ ACTION REQUIRED ON BE / DB
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
• DATABASE: … (what changed; auto-migrate on first request vs manual sqlite step)
|
||||
• SYNC FILES: … (paths to copy)
|
||||
• NGINX: … (reload if api locations changed)
|
||||
• PHP-FPM: … (restart only if needed)
|
||||
• BROWSER: HARD REFRESH after deploy
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
```
|
||||
|
||||
Use short bullet lines. If only one category applies, still use the banner and list only that item. If nothing requires BE/DB/nginx/browser action, do **not** show the banner.
|
||||
|
||||
Do not bury this in prose — the banner must be impossible to miss.
|
||||
@@ -479,7 +479,30 @@ a:hover { text-decoration: underline; }
|
||||
}
|
||||
.th-drag:active { cursor: grabbing; }
|
||||
.th-label { flex: 1; min-width: 0; }
|
||||
.th-draggable.th-dragging { opacity: .55; }
|
||||
.th-draggable.th-dragging { opacity: .45; }
|
||||
.th-draggable.th-dragging .th-label {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.data-table th.th-drop-target {
|
||||
box-shadow: inset 0 -3px 0 var(--accent);
|
||||
background: var(--row-hover);
|
||||
}
|
||||
.col-drag-ghost {
|
||||
position: fixed;
|
||||
top: -1000px;
|
||||
left: -1000px;
|
||||
z-index: 9999;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, .35);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.col-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -548,20 +571,33 @@ a:hover { text-decoration: underline; }
|
||||
|
||||
.reports-search-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
gap: 10px 14px;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px 12px;
|
||||
width: 100%;
|
||||
margin: 12px 0 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.reports-search-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
align-self: center;
|
||||
}
|
||||
.reports-search-field {
|
||||
position: relative;
|
||||
flex: 1 1 280px;
|
||||
min-width: 200px;
|
||||
flex: 0 0 40%;
|
||||
width: 40%;
|
||||
max-width: 40%;
|
||||
align-self: center;
|
||||
}
|
||||
.reports-search-row .btn {
|
||||
flex: 0 0 auto;
|
||||
align-self: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
.reports-search-input {
|
||||
width: 100%;
|
||||
|
||||
@@ -331,34 +331,79 @@
|
||||
cg.innerHTML = html;
|
||||
}
|
||||
|
||||
function columnLabelByKey(key) {
|
||||
const c = LIST_COLUMNS.find((col) => col.key === key);
|
||||
return c ? c.label : key;
|
||||
}
|
||||
|
||||
function clearColumnDropMarkers(thead) {
|
||||
thead.querySelectorAll('.th-drop-target').forEach((el) => {
|
||||
el.classList.remove('th-drop-target');
|
||||
});
|
||||
thead.querySelectorAll('.th-dragging').forEach((el) => {
|
||||
el.classList.remove('th-dragging');
|
||||
});
|
||||
}
|
||||
|
||||
function bindReportsTableLayout(table, thead, ctx) {
|
||||
if (!table || !thead || table.dataset.layoutBound === '1') return;
|
||||
table.dataset.layoutBound = '1';
|
||||
let dragKey = null;
|
||||
|
||||
thead.addEventListener('dragstart', (e) => {
|
||||
const handle = e.target.closest('.th-drag');
|
||||
if (!handle || ctx.getGrouped()) return;
|
||||
dragKey = handle.getAttribute('data-col-key');
|
||||
const sourceKey = handle.getAttribute('data-col-key');
|
||||
if (!sourceKey) return;
|
||||
table.dataset.dragColKey = sourceKey;
|
||||
const th = handle.closest('th');
|
||||
if (th) th.classList.add('th-dragging');
|
||||
const label = columnLabelByKey(sourceKey);
|
||||
const ghost = document.createElement('div');
|
||||
ghost.className = 'col-drag-ghost';
|
||||
ghost.textContent = label;
|
||||
ghost.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(ghost);
|
||||
table._dragGhost = ghost;
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', dragKey || '');
|
||||
e.dataTransfer.setData('text/plain', sourceKey);
|
||||
try {
|
||||
e.dataTransfer.setDragImage(ghost, 16, 14);
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
});
|
||||
|
||||
thead.addEventListener('dragend', (e) => {
|
||||
const th = e.target.closest('th');
|
||||
if (th) th.classList.remove('th-dragging');
|
||||
dragKey = null;
|
||||
thead.addEventListener('dragend', () => {
|
||||
clearColumnDropMarkers(thead);
|
||||
delete table.dataset.dragColKey;
|
||||
if (table._dragGhost) {
|
||||
table._dragGhost.remove();
|
||||
table._dragGhost = null;
|
||||
}
|
||||
});
|
||||
|
||||
thead.addEventListener('dragenter', (e) => {
|
||||
if (ctx.getGrouped() || !table.dataset.dragColKey) return;
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
thead.addEventListener('dragover', (e) => {
|
||||
if (ctx.getGrouped()) return;
|
||||
if (!e.target.closest('th[data-col-key]')) return;
|
||||
if (ctx.getGrouped() || !table.dataset.dragColKey) return;
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
||||
const th = e.target.closest('th[data-col-key]');
|
||||
if (!th) return;
|
||||
const active = thead.querySelector('.th-drop-target');
|
||||
if (active && active !== th) active.classList.remove('th-drop-target');
|
||||
th.classList.add('th-drop-target');
|
||||
});
|
||||
|
||||
thead.addEventListener('dragleave', (e) => {
|
||||
const th = e.target.closest('th[data-col-key]');
|
||||
if (!th) return;
|
||||
const rel = e.relatedTarget;
|
||||
if (rel && th.contains(rel)) return;
|
||||
th.classList.remove('th-drop-target');
|
||||
});
|
||||
|
||||
thead.addEventListener('drop', (e) => {
|
||||
@@ -366,9 +411,18 @@
|
||||
const th = e.target.closest('th[data-col-key]');
|
||||
if (!th) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const sourceKey =
|
||||
(e.dataTransfer && e.dataTransfer.getData('text/plain')) || dragKey;
|
||||
table.dataset.dragColKey ||
|
||||
(e.dataTransfer && e.dataTransfer.getData('text/plain')) ||
|
||||
'';
|
||||
const targetKey = th.getAttribute('data-col-key');
|
||||
clearColumnDropMarkers(thead);
|
||||
delete table.dataset.dragColKey;
|
||||
if (table._dragGhost) {
|
||||
table._dragGhost.remove();
|
||||
table._dragGhost = null;
|
||||
}
|
||||
if (!sourceKey || !targetKey || sourceKey === targetKey) return;
|
||||
const order = getOrderedColumnKeys();
|
||||
const from = order.indexOf(sourceKey);
|
||||
@@ -377,7 +431,6 @@
|
||||
order.splice(from, 1);
|
||||
order.splice(to, 0, sourceKey);
|
||||
saveColumnOrder(order);
|
||||
dragKey = null;
|
||||
ctx.refreshLayout();
|
||||
});
|
||||
|
||||
@@ -907,7 +960,6 @@
|
||||
const searchSuggest = document.getElementById('reports-search-suggest');
|
||||
const searchClear = document.getElementById('reports-search-clear');
|
||||
let suggestTimer = null;
|
||||
let searchRunTimer = null;
|
||||
|
||||
function hideSuggest() {
|
||||
if (searchSuggest) {
|
||||
@@ -988,22 +1040,18 @@
|
||||
if (searchClear) searchClear.hidden = false;
|
||||
}
|
||||
searchInput.addEventListener('input', () => {
|
||||
const q = searchInput.value.trim();
|
||||
if (searchClear) searchClear.hidden = !q;
|
||||
const q = searchInput.value;
|
||||
if (searchClear) searchClear.hidden = !q.trim();
|
||||
hideSuggest();
|
||||
clearTimeout(suggestTimer);
|
||||
clearTimeout(searchRunTimer);
|
||||
if (q.length < 2) {
|
||||
hideSuggest();
|
||||
return;
|
||||
}
|
||||
suggestTimer = setTimeout(() => fetchSuggest(q), 1000);
|
||||
searchRunTimer = setTimeout(() => applySearchQuery(q, true), 1600);
|
||||
const trimmed = q.trim();
|
||||
if (trimmed.length < 2) return;
|
||||
suggestTimer = setTimeout(() => fetchSuggest(trimmed), 1000);
|
||||
});
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
clearTimeout(suggestTimer);
|
||||
clearTimeout(searchRunTimer);
|
||||
hideSuggest();
|
||||
applySearchQuery(searchInput.value.trim(), true);
|
||||
}
|
||||
@@ -1012,14 +1060,26 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
function appendSearchTerm(term) {
|
||||
if (!searchInput) return;
|
||||
const t = (term || '').trim();
|
||||
if (!t) return;
|
||||
const cur = searchInput.value.trim();
|
||||
if (!cur) {
|
||||
searchInput.value = t;
|
||||
} else if (!cur.toLowerCase().includes(t.toLowerCase())) {
|
||||
searchInput.value = cur + ' ' + t;
|
||||
}
|
||||
if (searchClear) searchClear.hidden = !searchInput.value.trim();
|
||||
searchInput.focus();
|
||||
}
|
||||
if (searchSuggest) {
|
||||
searchSuggest.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-suggest]');
|
||||
if (!btn) return;
|
||||
const term = btn.getAttribute('data-suggest') || '';
|
||||
if (searchInput) searchInput.value = term;
|
||||
appendSearchTerm(term);
|
||||
hideSuggest();
|
||||
applySearchQuery(term, true);
|
||||
});
|
||||
}
|
||||
if (searchClear) {
|
||||
|
||||
101
examples/crash_reporter/upload_test.sh
Executable file
101
examples/crash_reporter/upload_test.sh
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# Batch-upload fake crash reports (java + native) from test_reports/.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPORTS_DIR="${REPORTS_DIR:-$ROOT/test_reports}"
|
||||
BASE="${BASE:-https://apps.f0xx.org/app/androidcast_project/crashes}"
|
||||
UPLOAD_URL="$BASE/api/upload.php"
|
||||
JAR="${JAR:-/tmp/ac_crash_cookies.txt}"
|
||||
LOGIN="${LOGIN:-0}"
|
||||
DELAY_MS="${DELAY_MS:-50}"
|
||||
ONLY="${ONLY:-all}" # all | java | native
|
||||
|
||||
if [[ ! -d "$REPORTS_DIR" ]]; then
|
||||
echo "Missing $REPORTS_DIR — run: python3 $ROOT/generate_test_reports.py" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
FILES=()
|
||||
case "$ONLY" in
|
||||
java)
|
||||
FILES=("$REPORTS_DIR"/crash_java_*.json)
|
||||
;;
|
||||
native)
|
||||
FILES=("$REPORTS_DIR"/crash_native_*.json)
|
||||
;;
|
||||
all|*)
|
||||
FILES=("$REPORTS_DIR"/crash_java_*.json "$REPORTS_DIR"/crash_native_*.json)
|
||||
# legacy single-prefix files from older generator
|
||||
LEGACY=("$REPORTS_DIR"/crash_[0-9][0-9][0-9].json)
|
||||
if [[ ${#FILES[@]} -eq 0 && ${#LEGACY[@]} -gt 0 ]]; then
|
||||
FILES=("${LEGACY[@]}")
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
shopt -u nullglob
|
||||
|
||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
||||
echo "No reports in $REPORTS_DIR (expected crash_java_*.json / crash_native_*.json)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=$'\n' FILES=($(printf '%s\n' "${FILES[@]}" | sort))
|
||||
unset IFS
|
||||
|
||||
java_n=0
|
||||
native_n=0
|
||||
for f in "${FILES[@]}"; do
|
||||
case "$(basename "$f")" in
|
||||
crash_java_*) ((java_n++)) || true ;;
|
||||
crash_native_*) ((native_n++)) || true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$LOGIN" == "1" ]]; then
|
||||
curl -sk -c "$JAR" -b "$JAR" -L -X POST "$BASE/login" \
|
||||
-d 'username=admin&password=admin' -o /dev/null
|
||||
echo "Logged in (cookie jar: $JAR)"
|
||||
fi
|
||||
|
||||
ok=0
|
||||
fail=0
|
||||
dup=0
|
||||
ok_java=0
|
||||
ok_native=0
|
||||
|
||||
echo "Uploading ${#FILES[@]} reports to $UPLOAD_URL (java=$java_n native=$native_n only=$ONLY)"
|
||||
|
||||
for f in "${FILES[@]}"; do
|
||||
name="$(basename "$f")"
|
||||
body="$(curl -sk -w '\n%{http_code}' -H 'Content-Type: application/json' --data-binary "@$f" "$UPLOAD_URL")"
|
||||
code="$(echo "$body" | tail -n1)"
|
||||
json="$(echo "$body" | sed '$d')"
|
||||
|
||||
case "$code" in
|
||||
200)
|
||||
((ok++)) || true
|
||||
case "$name" in
|
||||
crash_java_*) ((ok_java++)) || true; echo " OK [java] $name" ;;
|
||||
crash_native_*) ((ok_native++)) || true; echo " OK [native] $name" ;;
|
||||
*) echo " OK $name" ;;
|
||||
esac
|
||||
;;
|
||||
409)
|
||||
((dup++)) || true
|
||||
echo " DUP $name (report_id already stored)"
|
||||
;;
|
||||
*)
|
||||
((fail++)) || true
|
||||
echo " FAIL $name HTTP $code — $json" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$DELAY_MS" -gt 0 ]]; then
|
||||
sleep "$(awk "BEGIN { print $DELAY_MS / 1000 }")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Done: ok=$ok (java=$ok_java native=$ok_native) duplicate=$dup failed=$fail (total=${#FILES[@]})"
|
||||
[[ "$fail" -eq 0 ]]
|
||||
38
mount_be_alpine.sh
Executable file
38
mount_be_alpine.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
## FE - frontend mounts
|
||||
[[ -z "$(mount | grep FE | grep f0xx)" ]] && sshfs foxx@f0xx.org:/etc/nginx/ tmp/FE_gentoo/etc/nginx/ -o -p 222
|
||||
[[ -d "tmp/FE_gentoo/var/www/localhost/htdocs/android_cast/" && -d "tmp/FE_gentoo/var/www/localhost/htdocs/android_cast/" && -z "$(mount | grep FE | grep f0xx)" ]] && \
|
||||
|
||||
#sleep 5
|
||||
## BE - backend - through jump server through FE
|
||||
[ -z "${ARTC0_HOST}" ] && ARTC0_HOST="10.7.16.128"
|
||||
#ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ProxyJump=foxx@f0xx.org -P 222 foxx@${ARTC0_HOST}
|
||||
|
||||
## check out the changes in the ~/.ssh/config
|
||||
## and
|
||||
## ssh -o 'ProxyJump=foxx@f0xx.org:222' 'foxx@10.7.16.128'
|
||||
|
||||
sleep 3
|
||||
|
||||
if [[ -z "$(mount | grep BE | grep etc | grep f0xx)" && -d "tmp/BE_alpine/etc/nginx" ]]; then
|
||||
set -x
|
||||
sshfs alpine-be:/etc/nginx tmp/BE_alpine/etc/nginx
|
||||
set +x
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
|
||||
if [[ -z "$(mount | grep BE | grep var | grep f0xx)" && -d "tmp/BE_alpine/var/www/localhost/htdocs/apps" ]]; then
|
||||
set -x
|
||||
sshfs alpine-be:/var/www/localhost/htdocs/apps tmp/BE_alpine/var/www/localhost/htdocs/apps
|
||||
set +x
|
||||
fi
|
||||
|
||||
_umount() {
|
||||
umount "${1}"
|
||||
}
|
||||
|
||||
#_umount_fe {
|
||||
# fe_eps=$(mount | grep FE | grep -e f0xx -w FE_gentoo )
|
||||
#}
|
||||
|
||||
Reference in New Issue
Block a user