1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:18:09 +03:00

new stuff

This commit is contained in:
Anton Afanasyeu
2026-05-23 00:12:44 +02:00
parent 7900441ff6
commit 91e9b1d9ed
5 changed files with 300 additions and 30 deletions

View 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.

View File

@@ -479,7 +479,30 @@ a:hover { text-decoration: underline; }
} }
.th-drag:active { cursor: grabbing; } .th-drag:active { cursor: grabbing; }
.th-label { flex: 1; min-width: 0; } .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 { .col-resizer {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -548,20 +571,33 @@ a:hover { text-decoration: underline; }
.reports-search-row { .reports-search-row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: nowrap;
align-items: flex-end; align-items: center;
gap: 10px 14px; justify-content: flex-end;
gap: 10px 12px;
width: 100%;
margin: 12px 0 8px; margin: 12px 0 8px;
box-sizing: border-box;
} }
.reports-search-label { .reports-search-label {
font-size: 13px; font-size: 13px;
color: var(--muted); color: var(--muted);
flex: 0 0 auto; flex: 0 0 auto;
margin: 0;
line-height: 1.2;
align-self: center;
} }
.reports-search-field { .reports-search-field {
position: relative; position: relative;
flex: 1 1 280px; flex: 0 0 40%;
min-width: 200px; 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 { .reports-search-input {
width: 100%; width: 100%;

View File

@@ -331,34 +331,79 @@
cg.innerHTML = html; 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) { function bindReportsTableLayout(table, thead, ctx) {
if (!table || !thead || table.dataset.layoutBound === '1') return; if (!table || !thead || table.dataset.layoutBound === '1') return;
table.dataset.layoutBound = '1'; table.dataset.layoutBound = '1';
let dragKey = null;
thead.addEventListener('dragstart', (e) => { thead.addEventListener('dragstart', (e) => {
const handle = e.target.closest('.th-drag'); const handle = e.target.closest('.th-drag');
if (!handle || ctx.getGrouped()) return; 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'); const th = handle.closest('th');
if (th) th.classList.add('th-dragging'); 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) { if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move'; 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) => { thead.addEventListener('dragend', () => {
const th = e.target.closest('th'); clearColumnDropMarkers(thead);
if (th) th.classList.remove('th-dragging'); delete table.dataset.dragColKey;
dragKey = null; 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) => { thead.addEventListener('dragover', (e) => {
if (ctx.getGrouped()) return; if (ctx.getGrouped() || !table.dataset.dragColKey) return;
if (!e.target.closest('th[data-col-key]')) return;
e.preventDefault(); e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; 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) => { thead.addEventListener('drop', (e) => {
@@ -366,9 +411,18 @@
const th = e.target.closest('th[data-col-key]'); const th = e.target.closest('th[data-col-key]');
if (!th) return; if (!th) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
const sourceKey = 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'); 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; if (!sourceKey || !targetKey || sourceKey === targetKey) return;
const order = getOrderedColumnKeys(); const order = getOrderedColumnKeys();
const from = order.indexOf(sourceKey); const from = order.indexOf(sourceKey);
@@ -377,7 +431,6 @@
order.splice(from, 1); order.splice(from, 1);
order.splice(to, 0, sourceKey); order.splice(to, 0, sourceKey);
saveColumnOrder(order); saveColumnOrder(order);
dragKey = null;
ctx.refreshLayout(); ctx.refreshLayout();
}); });
@@ -907,7 +960,6 @@
const searchSuggest = document.getElementById('reports-search-suggest'); const searchSuggest = document.getElementById('reports-search-suggest');
const searchClear = document.getElementById('reports-search-clear'); const searchClear = document.getElementById('reports-search-clear');
let suggestTimer = null; let suggestTimer = null;
let searchRunTimer = null;
function hideSuggest() { function hideSuggest() {
if (searchSuggest) { if (searchSuggest) {
@@ -988,22 +1040,18 @@
if (searchClear) searchClear.hidden = false; if (searchClear) searchClear.hidden = false;
} }
searchInput.addEventListener('input', () => { searchInput.addEventListener('input', () => {
const q = searchInput.value.trim(); const q = searchInput.value;
if (searchClear) searchClear.hidden = !q; if (searchClear) searchClear.hidden = !q.trim();
hideSuggest();
clearTimeout(suggestTimer); clearTimeout(suggestTimer);
clearTimeout(searchRunTimer); const trimmed = q.trim();
if (q.length < 2) { if (trimmed.length < 2) return;
hideSuggest(); suggestTimer = setTimeout(() => fetchSuggest(trimmed), 1000);
return;
}
suggestTimer = setTimeout(() => fetchSuggest(q), 1000);
searchRunTimer = setTimeout(() => applySearchQuery(q, true), 1600);
}); });
searchInput.addEventListener('keydown', (e) => { searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
clearTimeout(suggestTimer); clearTimeout(suggestTimer);
clearTimeout(searchRunTimer);
hideSuggest(); hideSuggest();
applySearchQuery(searchInput.value.trim(), true); 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) { if (searchSuggest) {
searchSuggest.addEventListener('click', (e) => { searchSuggest.addEventListener('click', (e) => {
const btn = e.target.closest('[data-suggest]'); const btn = e.target.closest('[data-suggest]');
if (!btn) return; if (!btn) return;
const term = btn.getAttribute('data-suggest') || ''; const term = btn.getAttribute('data-suggest') || '';
if (searchInput) searchInput.value = term; appendSearchTerm(term);
hideSuggest(); hideSuggest();
applySearchQuery(term, true);
}); });
} }
if (searchClear) { if (searchClear) {

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