Add backend log management
This commit is contained in:
40
app/api/logs/route.ts
Normal file
40
app/api/logs/route.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { jsonError, jsonOk } from "@/lib/server/api";
|
||||
import { appLogFilePath, appLogMaxBytes, clearAppLogs, listAppLogs, type AppLogLevel } from "@/lib/server/log-manager";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const level = parseLevel(url.searchParams.get("level"));
|
||||
const q = url.searchParams.get("q") || undefined;
|
||||
const limit = Number(url.searchParams.get("limit") || 100);
|
||||
const entries = await listAppLogs({
|
||||
level,
|
||||
q,
|
||||
limit: Number.isFinite(limit) ? limit : 100
|
||||
});
|
||||
return jsonOk({
|
||||
entries,
|
||||
logPath: appLogFilePath(),
|
||||
maxBytes: appLogMaxBytes()
|
||||
});
|
||||
} catch (error) {
|
||||
return jsonError(error, 500, { request, source: "api.logs" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
await clearAppLogs();
|
||||
return jsonOk({ ok: true });
|
||||
} catch (error) {
|
||||
return jsonError(error, 500, { request, source: "api.logs" });
|
||||
}
|
||||
}
|
||||
|
||||
function parseLevel(value: string | null): AppLogLevel | "all" {
|
||||
if (value === "info" || value === "warning" || value === "error") return value;
|
||||
return "all";
|
||||
}
|
||||
169
app/globals.css
169
app/globals.css
@@ -1207,6 +1207,163 @@ h3 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.log-manager {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.log-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 0.8fr) minmax(260px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.log-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.log-search {
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.log-search svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: var(--muted);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.log-search input {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.log-storage {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) repeat(2, minmax(150px, 0.6fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.log-storage div {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #f8faf8;
|
||||
}
|
||||
|
||||
.log-storage span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.log-storage strong {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.log-row {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.log-row-error {
|
||||
border-color: #e6b8ae;
|
||||
background: #fffaf8;
|
||||
}
|
||||
|
||||
.log-row-warning {
|
||||
border-color: #ead2a4;
|
||||
background: #fffaf0;
|
||||
}
|
||||
|
||||
.log-row-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.log-message strong {
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-message small {
|
||||
min-width: 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.log-detail {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.log-detail summary {
|
||||
cursor: pointer;
|
||||
color: var(--green-dark);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.log-detail pre {
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
margin: 8px 0 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #f8fbfb;
|
||||
color: #27312d;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.log-empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 160px;
|
||||
color: var(--muted);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.image-upload-button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -2221,6 +2378,11 @@ button:active:not(:disabled),
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.log-actions,
|
||||
.log-storage {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.settings-engine-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -2278,6 +2440,7 @@ button:active:not(:disabled),
|
||||
|
||||
.create-mode-bar,
|
||||
.asset-manager-toolbar,
|
||||
.log-actions,
|
||||
.settings-actions {
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -2450,6 +2613,11 @@ button:active:not(:disabled),
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.log-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.asset-view-switch {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2459,6 +2627,7 @@ button:active:not(:disabled),
|
||||
}
|
||||
|
||||
.asset-view-switch button,
|
||||
.log-tabs button,
|
||||
.settings-tabs button {
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
|
||||
7
app/logs/page.tsx
Normal file
7
app/logs/page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LogManager } from "@/components/log-manager";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default function LogsPage() {
|
||||
return <LogManager />;
|
||||
}
|
||||
Reference in New Issue
Block a user