Quiet the logs and capture the errors that were missing

- Activity page: "Delete all" empties the open tab (api/admin/logs/clear-logs,
  super admin only, logged under system)
- Stop logging every WebSocket connect
- Log unexpected API errors (safeErrorMessage -> logUnexpectedError), every
  failed bot job attempt and every job the queue lost
- Drop the cron jobs' console output; a failed model sync goes to the logs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-25 05:11:31 +01:00
co-authored by Claude Opus 5.5
parent 3a9e57f682
commit e6c278198b
16 changed files with 245 additions and 47 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
- **Auth layout:** `/auth/*` (except `/auth/logout`) renders in `src/layouts/auth/` with no site header/footer. - **Auth layout:** `/auth/*` (except `/auth/logout`) renders in `src/layouts/auth/` with no site header/footer.
- **User types** (`UserTypes` in `src/dict/user-types-dict.ts`, the only place they're defined): `super_admin` (the first account, runs the install), `admin` (manages members; only the super admin grants it), `member` (a regular user: new users start with it), `read_only`, `revoked` (can't sign in). A user needs at least one to sign in. Admins grant and remove `member`, `read_only` and `revoked` on anyone but admins and the super admin. - **User types** (`UserTypes` in `src/dict/user-types-dict.ts`, the only place they're defined): `super_admin` (the first account, runs the install), `admin` (manages members; only the super admin grants it), `member` (a regular user: new users start with it), `read_only`, `revoked` (can't sign in). A user needs at least one to sign in. Admins grant and remove `member`, `read_only` and `revoked` on anyone but admins and the super admin.
- **RBAC:** `src/utils/check-user-access.ts` (includes/excludes primitive, `super_admin` bypass) plus the declarative engine in `src/utils/rbac/` - `userTypeAssignment` interprets `UserTypeAssignmentMatrix`/`UserTypeDeletionMatrix` in `src/dict/user-types-dict.ts`. `read_only` users are blocked from non-GET CRUD in the router. - **RBAC:** `src/utils/check-user-access.ts` (includes/excludes primitive, `super_admin` bypass) plus the declarative engine in `src/utils/rbac/` - `userTypeAssignment` interprets `UserTypeAssignmentMatrix`/`UserTypeDeletionMatrix` in `src/dict/user-types-dict.ts`. `read_only` users are blocked from non-GET CRUD in the router.
- **Audit trail:** `src/functions/backend/logger.ts` - `logEvent`/`logError` write to the `logs` table. Signup, login, Google login, logout, cron runs, and WebSocket connects are all logged. - **Audit trail:** `src/functions/backend/logger.ts` - `logEvent`/`logError` write to the `logs` table. Signup, login, Google login, logout and cron runs are logged (WebSocket connects are not: one per tab and reconnect drowned the page). Errors land under `error`: every unexpected one an API route catches (`safeErrorMessage` calls `logUnexpectedError` for anything but an `AppError`, skipped under tests; pass `log: false` where the caller already logs it), every failed bot job attempt with its bot, kind and try (`processBotJob`), and every job the queue lost (`reportLostBotJobs`). The Activity page's "Delete all" empties the open tab (`api/admin/logs/clear-logs`, super admin only, logged under `system`).
### WebSocket ### WebSocket
+32
View File
@@ -290,6 +290,38 @@ export default async function processBotJob({
return null; return null;
}); });
/**
* Every failed try is on the Activity page, so a model or provider
* that keeps failing shows there and not only in one chat.
*/
if (failed) {
const final = failed.status == "failed";
await logError({
title: final
? `Bot job #${job_id} failed`
: `Bot job #${job_id} failed, retrying (try ${tries})`,
error: timed_out ? new Error(timeout_message) : error,
user_id: user_id || undefined,
details: [
{ label: "Bot", value: bot_id },
{ label: "Kind", value: job.kind },
{ label: "Title", value: job.title || "" },
{ label: "Try", value: tries },
],
metadata: {
job_id,
bot_id,
kind: job.kind,
session_id: job.session_id,
workspace_id: job.workspace_id,
schedule_id,
fatal: error instanceof BotJobFatalError,
harness_code: error instanceof HarnessError ? error.code : undefined,
},
}).catch(() => {});
}
if (failed?.notice) { if (failed?.notice) {
await pushBotNotice({ bot_id, ...failed.notice }); await pushBotNotice({ bot_id, ...failed.notice });
} }
+13 -1
View File
@@ -1,10 +1,11 @@
import grabUnreportedFailedBotJobs from "@/src/functions/backend/db/bots-queue/grab-unreported-failed-bot-jobs"; import grabUnreportedFailedBotJobs from "@/src/functions/backend/db/bots-queue/grab-unreported-failed-bot-jobs";
import notifyBotJobFailed from "@/src/functions/backend/db/bots-queue/notify-bot-job-failed"; import notifyBotJobFailed from "@/src/functions/backend/db/bots-queue/notify-bot-job-failed";
import { logError } from "@/src/functions/backend/logger";
import pushBotNotice from "./push-bot-notice"; import pushBotNotice from "./push-bot-notice";
/** /**
* Tells the user about jobs that ran out of attempts by going silent * Tells the user about jobs that ran out of attempts by going silent
* (bun-sqlite fails those itself, without a message). * (bun-sqlite fails those itself, without a message), and logs each.
*/ */
export default async function reportLostBotJobs() { export default async function reportLostBotJobs() {
const jobs = await grabUnreportedFailedBotJobs(); const jobs = await grabUnreportedFailedBotJobs();
@@ -18,5 +19,16 @@ export default async function reportLostBotJobs() {
}); });
await pushBotNotice({ bot_id: job.bot_id, ...notice }); await pushBotNotice({ bot_id: job.bot_id, ...notice });
await logError({
title: `Bot job #${job.id} lost`,
error: job.last_error || "The job's lock timed out.",
details: [
{ label: "Bot", value: job.bot_id },
{ label: "Kind", value: job.kind },
{ label: "Title", value: job.title || "" },
],
metadata: { job_id: job.id, bot_id: job.bot_id, kind: job.kind },
}).catch(() => {});
} }
} }
-5
View File
@@ -1,4 +1,3 @@
import bunext from "@moduletrace/bunext";
import BunSQLite from "@moduletrace/bun-sqlite"; import BunSQLite from "@moduletrace/bun-sqlite";
import { AppData } from "@/src/data/app-data"; import { AppData } from "@/src/data/app-data";
@@ -15,9 +14,5 @@ export default async function pruneBotsQueue() {
values: [cutoff], values: [cutoff],
}); });
bunext.bunextLog.info(
`Cron: pruned ${res.payload?.length || 0} finished bot job(s)`,
);
return res; return res;
} }
+11 -11
View File
@@ -1,23 +1,23 @@
import bunext from "@moduletrace/bunext";
import syncAllHarnessModels from "@/src/functions/backend/db/harness/sync-all-harness-models"; import syncAllHarnessModels from "@/src/functions/backend/db/harness/sync-all-harness-models";
import { logError } from "@/src/functions/backend/logger";
/** /**
* Refreshes every enabled provider's model list. Free models on OpenRouter * Refreshes every enabled provider's model list. Free models on OpenRouter
* come and go, and automatic picks follow them from this list. * come and go, and automatic picks follow them from this list. A provider
* that fails doesn't fail the run, so its error goes to the logs here.
*/ */
export default async function cronSyncHarnessModels() { export default async function cronSyncHarnessModels() {
const results = await syncAllHarnessModels(); const results = await syncAllHarnessModels();
for (const result of results) { for (const result of results) {
if (result.error) { if (!result.error) continue;
bunext.bunextLog.error(
`Cron: model sync for provider #${result.provider_id} failed: ${result.error}`, await logError({
); title: `Cron: model sync for provider #${result.provider_id} failed`,
} else { category: "cron",
bunext.bunextLog.info( error: result.error,
`Cron: provider #${result.provider_id} lists ${result.count} model(s)`, metadata: { provider_id: result.provider_id },
); }).catch(() => {});
}
} }
return results; return results;
@@ -87,7 +87,7 @@ export default async function fireCronJob({ cron_job, now = Date.now() }: Params
metadata: { cron_job_id: id, job_id, bot_id: cron_job.bot_id }, metadata: { cron_job_id: id, job_id, bot_id: cron_job.bot_id },
}); });
} catch (error) { } catch (error) {
await saveRun({ id, last_error: safeErrorMessage({ error, fallback: `Couldn't queue the run.` }) }); await saveRun({ id, last_error: safeErrorMessage({ error, fallback: `Couldn't queue the run.`, log: false }) });
await logError({ await logError({
title: `Schedule #${id} couldn't run`, title: `Schedule #${id} couldn't run`,
@@ -0,0 +1,22 @@
import { logError } from "./logger";
type Params = {
error: unknown;
};
/**
* Writes an error nobody meant to show (anything but an `AppError`) to the
* logs, so a failure the client only sees as "Something went wrong" is
* still on the Activity page with its stack. Never during tests, which
* throw plain errors on purpose, and never throws.
*/
export default function logUnexpectedError({ error }: Params) {
if (process.env.NODE_ENV == "test") return;
const name = error instanceof Error ? error.name : typeof error;
void logError({
title: `Unexpected server error (${name})`,
error,
}).catch(() => {});
}
@@ -0,0 +1,36 @@
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
import type { APIResponseObject } from "@moduletrace/bunext/types";
import type { ClearLogsRequest } from "@/src/schemas/log-schema";
type Params = {
category?: ClearLogsRequest["category"];
/**
* The tab's name, for the confirmation.
*/
title: string;
count?: number;
};
/**
* Deletes every log in the open tab, after asking.
*/
export default async function clearLogs({ category, title, count }: Params) {
const amount = typeof count == "number" ? `${count} ` : "";
const confirm_msg = category
? `Delete all ${amount}${title} logs? This cannot be undone.`
: `Delete all ${amount}logs? This cannot be undone.`;
if (!window.confirm(confirm_msg)) {
return { success: false, cancelled: true };
}
const res = await fetchApi<ClearLogsRequest, APIResponseObject<{ deleted: number }>>(
`/api/admin/logs/clear-logs`,
{ method: "POST", body: { category } },
);
return {
success: Boolean(res?.success),
msg: res?.success ? undefined : res?.msg || `Couldn't delete logs`,
};
}
@@ -78,6 +78,20 @@ export default function useLogsListInit(_props?: Props) {
setRes(undefined); setRes(undefined);
}; };
/**
* The tab was emptied: back to its first page, reloaded.
*/
const afterClear = () => {
setSelectedIds([]);
if (page > 1) {
setPage(1);
return;
}
setRes(undefined);
};
const all_selected = const all_selected =
Boolean(logs?.[0]) && selected_ids.length == logs?.length; Boolean(logs?.[0]) && selected_ids.length == logs?.length;
@@ -100,6 +114,7 @@ export default function useLogsListInit(_props?: Props) {
toggleSelectAll, toggleSelectAll,
all_selected, all_selected,
afterDelete, afterDelete,
afterClear,
active_log, active_log,
openLog, openLog,
log_modal_open, log_modal_open,
@@ -0,0 +1,61 @@
import { Trash2 } from "lucide-react";
import Button from "@moduletrace/twui/layout/Button";
import Tag from "@moduletrace/twui/elements/Tag";
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import useStatus from "@moduletrace/twui/hooks/useStatus";
import { LogCategoryFilters } from "../(data)/log-category-filters";
import type { LogCategoryValue } from "../(data)/log-category-styles";
import clearLogs from "../(functions)/clear-logs";
type Props = {
category?: LogCategoryValue;
count?: number;
afterClear: () => void;
};
/**
* Deletes every log in the open tab ("All" deletes every log).
*/
export default function ClearLogsButton({ category, count, afterClear }: Props) {
const { loading, setLoading, status, setStatus } = useStatus();
const title = LogCategoryFilters.find((filter) => filter.value == category)?.title || "All";
const clear_logs = async () => {
setStatus(undefined);
setLoading(true);
const res = await clearLogs({ category, title, count });
setLoading(false);
if (res.success) {
afterClear();
} else if (res.msg) {
setStatus({ error: true, msg: res.msg });
}
};
return (
<Row className="gap-2! flex-nowrap">
{status?.error && <Tag color="error">{status.msg}</Tag>}
<Button
title={category ? `Delete all ${title} logs` : "Delete all logs"}
variant="ghost"
color="error"
size="small"
loading={loading}
disabled={!count}
onClick={() => {
clear_logs();
}}
>
<Row className="gap-2! flex-nowrap">
<Trash2 size={15} />
<Span>{category ? `Delete all ${title}` : "Delete all"}</Span>
</Row>
</Button>
</Row>
);
}
@@ -4,6 +4,7 @@ import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span"; import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack"; import Stack from "@moduletrace/twui/layout/Stack";
import type useLogsListInit from "../(hooks)/use-logs-list-init"; import type useLogsListInit from "../(hooks)/use-logs-list-init";
import ClearLogsButton from "./clear-logs-button";
import DeleteLogsButton from "./delete-logs-button"; import DeleteLogsButton from "./delete-logs-button";
import LogCategoryFilter from "./log-category-filter"; import LogCategoryFilter from "./log-category-filter";
@@ -18,6 +19,7 @@ export default function LogsToolbar({ init }: Props) {
selectCategory, selectCategory,
selected_ids, selected_ids,
afterDelete, afterDelete,
afterClear,
} = init; } = init;
return ( return (
@@ -33,6 +35,7 @@ export default function LogsToolbar({ init }: Props) {
? `${count} log${count == 1 ? "" : "s"}` ? `${count} log${count == 1 ? "" : "s"}`
: "--"} : "--"}
</Span> </Span>
<Row className="gap-2! flex-nowrap">
{selected_ids[0] ? ( {selected_ids[0] ? (
<DeleteLogsButton <DeleteLogsButton
ids={selected_ids} ids={selected_ids}
@@ -47,6 +50,12 @@ export default function LogsToolbar({ init }: Props) {
</Row> </Row>
</DeleteLogsButton> </DeleteLogsButton>
) : null} ) : null}
<ClearLogsButton
category={category}
count={count}
afterClear={afterClear}
/>
</Row>
</Row> </Row>
</Stack> </Stack>
</Paper> </Paper>
+1 -1
View File
@@ -114,7 +114,7 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
return { return {
success: false, success: false,
msg: safeErrorMessage({ error }), msg: safeErrorMessage({ error, log: false }),
}; };
} }
}; };
+1 -1
View File
@@ -89,7 +89,7 @@ export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
return { return {
success: false, success: false,
msg: safeErrorMessage({ error }), msg: safeErrorMessage({ error, log: false }),
}; };
} }
}; };
+17
View File
@@ -0,0 +1,17 @@
import { z } from "zod";
import { LogCategories } from "@/src/dict/log-categories";
const LogCategoryValues = LogCategories.map((lc) => lc.value) as [
(typeof LogCategories)[number]["value"],
...(typeof LogCategories)[number]["value"][],
];
/**
* `POST api/admin/logs/clear-logs`: deletes every log in one category, or
* every log when it is left out (the "All" tab).
*/
export const ClearLogsRequestSchema = z.object({
category: z.enum(LogCategoryValues).optional(),
});
export type ClearLogsRequest = z.infer<typeof ClearLogsRequestSchema>;
+11 -1
View File
@@ -1,23 +1,33 @@
import AppError from "./app-error"; import AppError from "./app-error";
import logUnexpectedError from "@/src/functions/backend/log-unexpected-error";
type Params = { type Params = {
error: unknown; error: unknown;
fallback?: string; fallback?: string;
/**
* Write an unexpected error to the logs. Off where the caller already
* logs it.
*/
log?: boolean;
}; };
/** /**
* # Safe Error Message * # Safe Error Message
* Returns the message of an `AppError` (deliberate, user-facing) and a * Returns the message of an `AppError` (deliberate, user-facing) and a
* generic fallback for everything else, so internal details (SQL errors, * generic fallback for everything else, so internal details (SQL errors,
* file paths, stack messages) never reach the client. * file paths, stack messages) never reach the client. The unexpected ones
* are logged (`logUnexpectedError`), since the client never sees why.
*/ */
export default function safeErrorMessage({ export default function safeErrorMessage({
error, error,
fallback = "Something went wrong. Please try again.", fallback = "Something went wrong. Please try again.",
log = true,
}: Params): string { }: Params): string {
if (error instanceof AppError) { if (error instanceof AppError) {
return error.message; return error.message;
} }
if (log) logUnexpectedError({ error });
return fallback; return fallback;
} }
-11
View File
@@ -9,7 +9,6 @@ import { getAllClients, getAllRooms } from "./(data)/rooms";
import type { WebSocketDataType } from "@/src/types"; import type { WebSocketDataType } from "@/src/types";
import { wsSendData } from "./(functions)/ws-send-data"; import { wsSendData } from "./(functions)/ws-send-data";
import checkSessionAccess from "./(functions)/check-session-access"; import checkSessionAccess from "./(functions)/check-session-access";
import logEvent from "@/src/functions/backend/logger";
import { computerProxySocket } from "@/src/functions/server/computers/computer-proxy-socket"; import { computerProxySocket } from "@/src/functions/server/computers/computer-proxy-socket";
import touchUserPresence from "@/src/functions/backend/db/push/touch-user-presence"; import touchUserPresence from "@/src/functions/backend/db/push/touch-user-presence";
@@ -46,16 +45,6 @@ export const websocket = {
message: "WebSocket connected", message: "WebSocket connected",
}, },
}); });
logEvent({
title: "WebSocket connected",
category: "websocket",
user_id: user.id,
plain_text: `User #${user.id} [${user.first_name} ${user.last_name}] connected via WebSocket.`,
metadata: {
user_id: user.id,
},
});
}, },
async message( async message(