From e5ac069dc44407c2ef8f7fa5309e49cb3cd11820 Mon Sep 17 00:00:00 2001 From: "tom.hempel" Date: Mon, 1 Jun 2026 22:46:48 +0200 Subject: [PATCH] expanded log activity --- lib/api_log.php | 59 +++++++++++++++++++++++--------- website/css/style.css | 1 + website/js/api.js | 8 +++++ website/js/pages/dev.js | 16 ++++----- website/js/pages/export.js | 12 ++----- website/js/pages/translations.js | 7 ++-- 6 files changed, 62 insertions(+), 41 deletions(-) diff --git a/lib/api_log.php b/lib/api_log.php index 0422f66..753e189 100644 --- a/lib/api_log.php +++ b/lib/api_log.php @@ -7,13 +7,14 @@ * Recorded activity (not routine website page loads): * - app_sync: GET /app_questionnaires (mobile sync) * - app_change: POST/PUT/PATCH/DELETE from the Android app - * - web_change: POST/PUT/PATCH/DELETE from the management website + * - web_change: POST/PUT/PATCH/DELETE from the management website (incl. translation labels) + * - web_export: Website downloads (CSV/ZIP/JSON exports, translation bundle) * * Set QDB_API_LOG=0 to disable. Logs live under logs/api/ (gitignored). */ /** @var list */ -const QDB_API_LOG_ACTIVITIES = ['app_sync', 'app_change', 'web_change']; +const QDB_API_LOG_ACTIVITIES = ['app_sync', 'app_change', 'web_change', 'web_export']; function qdb_api_log_enabled(): bool { @@ -69,13 +70,30 @@ function qdb_api_log_classify_activity(string $method, string $client, string $r return $client === 'web' ? 'web_change' : 'app_change'; } - if ($method === 'GET' && $route === 'app_questionnaires' && $client !== 'web') { - return 'app_sync'; + if ($method === 'GET') { + if ($route === 'app_questionnaires' && $client !== 'web') { + return 'app_sync'; + } + if (qdb_api_log_is_website_download($route)) { + return 'web_export'; + } } return null; } +/** Website GET that downloads or exports data (not routine list/load). */ +function qdb_api_log_is_website_download(string $route): bool +{ + if (in_array($route, ['export', 'backup'], true)) { + return true; + } + if ($route === 'translations' && !empty($_GET['exportBundle'])) { + return true; + } + return false; +} + /** @param array $context */ function qdb_api_log_begin(string $method, string $route, array $context = []): void { @@ -753,19 +771,26 @@ function qdb_api_log_sort_changes_for_display(array $changes): array { $priority = [ 'username' => 0, - 'role' => 1, - 'clientcode' => 2, - 'action' => 3, - 'query.id' => 4, - 'query.clients' => 5, - 'query.clientcode' => 6, - 'query.answers' => 7, - 'query.translations' => 8, - 'supervisorid' => 9, - 'userid' => 10, - 'mustchangepassword' => 11, - 'password' => 12, - 'location' => 13, + 'type' => 1, + 'text' => 2, + 'languagecode' => 3, + 'id' => 4, + 'role' => 5, + 'clientcode' => 6, + 'action' => 7, + 'query.id' => 10, + 'query.clients' => 11, + 'query.clientcode' => 12, + 'query.answers' => 13, + 'query.translations' => 14, + 'query.bundle' => 15, + 'query.exportall' => 16, + 'query.exportbundle' => 17, + 'supervisorid' => 18, + 'userid' => 19, + 'mustchangepassword' => 20, + 'password' => 21, + 'location' => 22, ]; usort($changes, static function (array $a, array $b) use ($priority): int { diff --git a/website/css/style.css b/website/css/style.css index 6caea1a..ec937a8 100644 --- a/website/css/style.css +++ b/website/css/style.css @@ -326,6 +326,7 @@ code { .badge-activity-app_sync { background: var(--badge-coach-bg); color: var(--badge-coach-fg); } .badge-activity-app_change { background: var(--badge-in-progress-bg); color: var(--badge-in-progress-fg); } .badge-activity-web_change { background: var(--badge-supervisor-bg); color: var(--badge-supervisor-fg); } +.badge-activity-web_export { background: var(--badge-admin-bg); color: var(--badge-admin-fg); } .activity-log-card .table-wrapper { max-height: 420px; overflow: auto; } .activity-log-detail { font-size: 0.8rem; diff --git a/website/js/api.js b/website/js/api.js index 5746f41..4145c56 100644 --- a/website/js/api.js +++ b/website/js/api.js @@ -92,3 +92,11 @@ export function apiDownloadUrl(endpoint) { const cleanEndpoint = endpoint.replace(/\.php(\?|$)/, '$1'); return `${API_BASE}/${cleanEndpoint}${cleanEndpoint.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`; } + +/** Authenticated file/download fetch (marks request as website for activity logging). */ +export async function apiDownloadFetch(url, options = {}) { + const token = getToken(); + const headers = { ...(options.headers || {}), 'X-QDB-Client': 'web' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + return fetch(url, { ...options, headers }); +} diff --git a/website/js/pages/dev.js b/website/js/pages/dev.js index fbcc952..9791933 100644 --- a/website/js/pages/dev.js +++ b/website/js/pages/dev.js @@ -1,4 +1,4 @@ -import { apiGet, apiPost } from '../api.js'; +import { apiGet, apiPost, apiDownloadFetch } from '../api.js'; import { getRole, pageHeaderHTML, showToast } from '../app.js'; import { navigate } from '../router.js'; @@ -6,6 +6,7 @@ const ACTIVITY_LABELS = { app_sync: 'App sync', app_change: 'App change', web_change: 'Website change', + web_export: 'Website export', }; export function devPage() { @@ -61,8 +62,8 @@ export function devPage() {

API activity log

- App syncs (GET /app_questionnaires) and data changes - (POST, PUT, PATCH, DELETE). Routine website page loads are not logged. + App syncs, website edits (translations, users, questionnaires, …), + and exports/downloads. Routine page loads (lists, dashboards) are not logged.

@@ -73,6 +74,7 @@ export function devPage() { +
@@ -405,13 +407,7 @@ function filenameFromContentDisposition(res, fallback) { } async function downloadExportZip(url, defaultFilename) { - const token = localStorage.getItem('qdb_token'); - const res = await fetch(url, { - headers: { - Authorization: `Bearer ${token}`, - 'X-QDB-Client': 'web', - }, - }); + const res = await apiDownloadFetch(url); if (!res.ok) { const err = await res.json().catch(() => ({ error: 'Download failed' })); throw new Error(err.error?.message || err.error || 'Download failed'); diff --git a/website/js/pages/export.js b/website/js/pages/export.js index af7b1d6..41ee675 100644 --- a/website/js/pages/export.js +++ b/website/js/pages/export.js @@ -1,4 +1,4 @@ -import { apiGet } from '../api.js'; +import { apiGet, apiDownloadFetch } from '../api.js'; import { canEdit, pageHeaderHTML, showToast } from '../app.js'; let questionnairesList = []; let filterSearch = ''; @@ -153,10 +153,7 @@ function wireExportActions() { const prev = btn.textContent; btn.textContent = 'Preparing…'; try { - const token = localStorage.getItem('qdb_token'); - const res = await fetch('../api/export?bundle=1', { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await apiDownloadFetch('../api/export?bundle=1'); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || err.error || 'Export failed'); @@ -180,10 +177,7 @@ function wireExportActions() { } async function downloadExportCsv(btn, url, filename) { - const token = localStorage.getItem('qdb_token'); - const res = await fetch(url, { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await apiDownloadFetch(url); if (!res.ok) { const err = await res.json().catch(() => ({ error: 'Download failed' })); throw new Error(err.error?.message || err.error || 'Download failed'); diff --git a/website/js/pages/translations.js b/website/js/pages/translations.js index 881aefd..51d7e7a 100644 --- a/website/js/pages/translations.js +++ b/website/js/pages/translations.js @@ -1,4 +1,4 @@ -import { apiGet, apiPost, apiPut, apiDelete } from '../api.js'; +import { apiGet, apiPost, apiPut, apiDelete, apiDownloadFetch } from '../api.js'; import { canEdit, pageHeaderHTML, showToast } from '../app.js'; import { SOURCE_LANG, @@ -406,10 +406,7 @@ function bindTranslationBundleActions() { const prev = exportBtn.textContent; exportBtn.textContent = 'Preparing…'; try { - const token = localStorage.getItem('qdb_token'); - const res = await fetch('../api/translations?exportBundle=1', { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await apiDownloadFetch('../api/translations?exportBundle=1'); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || err.error || 'Export failed');