From 91e9b1d9ed8f07d6c2379c76050f8708e82ffec2 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Sat, 23 May 2026 00:12:44 +0200 Subject: [PATCH] new stuff --- .cursor/rules/be-deploy-alerts.mdc | 35 ++++++ .../backend/public/assets/css/app.css | 48 +++++++- .../backend/public/assets/js/app.js | 108 ++++++++++++++---- examples/crash_reporter/upload_test.sh | 101 ++++++++++++++++ mount_be_alpine.sh | 38 ++++++ 5 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 .cursor/rules/be-deploy-alerts.mdc create mode 100755 examples/crash_reporter/upload_test.sh create mode 100755 mount_be_alpine.sh diff --git a/.cursor/rules/be-deploy-alerts.mdc b/.cursor/rules/be-deploy-alerts.mdc new file mode 100644 index 0000000..a2bf0ba --- /dev/null +++ b/.cursor/rules/be-deploy-alerts.mdc @@ -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. diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index 91ded25..226913b 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -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%; diff --git a/examples/crash_reporter/backend/public/assets/js/app.js b/examples/crash_reporter/backend/public/assets/js/app.js index a762cdd..5ae83c2 100644 --- a/examples/crash_reporter/backend/public/assets/js/app.js +++ b/examples/crash_reporter/backend/public/assets/js/app.js @@ -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) { diff --git a/examples/crash_reporter/upload_test.sh b/examples/crash_reporter/upload_test.sh new file mode 100755 index 0000000..8aa13d5 --- /dev/null +++ b/examples/crash_reporter/upload_test.sh @@ -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 ]] diff --git a/mount_be_alpine.sh b/mount_be_alpine.sh new file mode 100755 index 0000000..7451897 --- /dev/null +++ b/mount_be_alpine.sh @@ -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 ) +#} +