diff --git a/platform_web/components/ServerManagementTerminalDrawer.tsx b/platform_web/components/ServerManagementTerminalDrawer.tsx index 9695070..88b6249 100644 --- a/platform_web/components/ServerManagementTerminalDrawer.tsx +++ b/platform_web/components/ServerManagementTerminalDrawer.tsx @@ -6,6 +6,7 @@ import type { JobResponse, LogEntryBody, LogStreamResponse } from "../api/types" import { scumSourceRCONCommandRequest } from "../schemas/scumManagementRcon"; import { cx } from "../utils/classes"; import { mergeLogStreams, parseLogSessionEvent, parseLogStreamEvent, parseServerLogEvent, streamFromServerLogEvent } from "../utils/logEvents"; +import { formatTerminalLogTime } from "../utils/logTime"; import { EmptyState, ResultBadge } from "./StateViews"; type LoadState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T }; @@ -410,7 +411,7 @@ function terminalLineFromLog(stream: LogStreamResponse, entry: LogEntryBody): Te id: `log-${stream.id}-${entry.seq}`, tone: terminalTone(entry), text: entry.line, - at: new Date(entry.timestamp).toLocaleTimeString(), + at: formatTerminalLogTime(entry.timestamp, entry.line), sortKey: Date.parse(entry.timestamp) || Date.now(), streamKey: stream.streamKey || stream.source, level: entry.level, diff --git a/platform_web/theme/base-css.test.js b/platform_web/theme/base-css.test.js index 2aa0ac7..598d983 100644 --- a/platform_web/theme/base-css.test.js +++ b/platform_web/theme/base-css.test.js @@ -58,6 +58,14 @@ describe("platform web shared theme CSS", () => { expect(css).toContain(":root[data-custom-background=true].server-card-stat"); }); + it("gives server cards enough room for long status labels", () => { + const css = compact(readThemeCss()); + + expect(css).toContain(".server-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr))"); + expect(css).toContain(".server-card-head{display:grid;gap:8px;min-width:0}"); + expect(css).toContain(".server-card-head>span:last-child{justify-self:start;white-space:normal"); + }); + it("keeps magical custom-background signal rows readable without the jelly wash", () => { const css = compact(readThemeCss()); diff --git a/platform_web/theme/base.css b/platform_web/theme/base.css index 94b0e1a..449e580 100644 --- a/platform_web/theme/base.css +++ b/platform_web/theme/base.css @@ -450,11 +450,13 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))} .server-toolbar select{min-height:38px;border:1px solid var(--line-strong);border-radius:8px;padding:0 10px;background:var(--surface-solid);color:var(--ink);font:inherit;min-width:180px} .server-toolbar select:focus{border-color:var(--accent);outline:2px solid var(--accent-soft)} .server-toolbar input[type=search]:focus{border-color:var(--accent);outline:2px solid var(--accent-soft)} -.server-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(310px,1fr));gap:14px} +.server-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));gap:16px} .server-card{display:grid;gap:12px;padding:16px;border:1px solid var(--line);border-radius:8px;background:var(--frosted-surface),var(--glass-tint),var(--surface);backdrop-filter:blur(22px) saturate(1.28);text-align:left;transition:transform 120ms ease,border-color 120ms ease;box-shadow:var(--jelly-inset),inset 0 0 0 1px var(--diamond-line),0 18px 42px var(--glass-shadow),0 0 28px rgba(255,255,255,.2);position:relative;overflow:hidden;min-width:0} .server-card:focus-visible,.server-card:hover{border-color:var(--accent);outline:0;transform:translateY(-2px)} -.server-card-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px} -.server-card-head>span:first-child{min-width:0} +.server-card-head{display:grid;gap:8px;min-width:0} +.server-card-head>span:first-child,.server-card-head>span:last-child{min-width:0;max-width:100%} +.server-card-head>span:last-child{justify-self:start;white-space:normal;line-height:1.3} +.server-card>*{min-width:0} .server-card-head strong{display:block;font-size:16px;color:var(--ink);overflow-wrap:anywhere} .server-card-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px} .server-card-stat{display:grid;gap:2px;padding:8px;border:1px solid color-mix(in srgb,var(--line-strong) 56%,rgba(255,255,255,.16));border-radius:8px;background:linear-gradient(180deg,color-mix(in srgb,var(--surface-solid) 88%,rgba(255,255,255,.06)),color-mix(in srgb,var(--surface-solid) 72%,var(--accent-soft))),var(--glass-wash);min-height:52px;align-content:center;box-shadow:inset 0 1px 0 color-mix(in srgb,var(--crystal-rim) 68%,transparent),0 8px 18px rgba(0,0,0,.18);min-width:0;backdrop-filter:blur(10px) saturate(0.92)} @@ -767,6 +769,8 @@ button.operations-inline-warning{cursor:pointer} .inline-link-command{border:0;padding:0;background:0 0;color:var(--accent-deep);text-decoration:underline;cursor:pointer} .inline-link-command:focus-visible,.inline-link-command:hover{color:var(--ink);outline:0} .server-card-warning{justify-self:start;color:var(--gold)} +.server-card .action-strip{min-width:0} +.server-card .action-strip button{min-width:0;white-space:nowrap} .resource-filter-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:12px 0} .resource-filter-bar input,.resource-filter-bar select{min-height:36px;min-width:150px;border:1px solid var(--line-strong);border-radius:6px;padding:0 10px;background:var(--surface-solid);color:var(--ink);font:inherit} .resource-filter-bar input{flex:1 1 240px} diff --git a/platform_web/utils/logTime.test.ts b/platform_web/utils/logTime.test.ts new file mode 100644 index 0000000..e902e2f --- /dev/null +++ b/platform_web/utils/logTime.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { formatTerminalLogTime } from "./logTime"; + +describe("formatTerminalLogTime", () => { + it("uses the SCUM log clock instead of the browser timezone", () => { + expect(formatTerminalLogTime("2026-08-24T01:23:44Z", "[2026.08.23-17.23.44][418]LogSCUM: Global Stats")) + .toBe("17:23:44"); + }); + + it("falls back to the event timestamp for generic logs", () => { + expect(formatTerminalLogTime("not-a-timestamp")).toBe("时间未知"); + }); +}); diff --git a/platform_web/utils/logTime.ts b/platform_web/utils/logTime.ts new file mode 100644 index 0000000..6ae3f7b --- /dev/null +++ b/platform_web/utils/logTime.ts @@ -0,0 +1,11 @@ +const scumLogTimestampPattern = /\[?(\d{4})\.(\d{2})\.(\d{2})-(\d{2})\.(\d{2})\.(\d{2})(?:[.:]\d{1,3})?\]?/; + +export function formatTerminalLogTime(timestamp: string, line?: string): string { + const embedded = line?.match(scumLogTimestampPattern); + if (embedded) { + return `${embedded[4]}:${embedded[5]}:${embedded[6]}`; + } + + const date = new Date(timestamp); + return Number.isNaN(date.getTime()) ? "时间未知" : date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); +}