@payconductor/houndsman 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/arg-parser.ts +8 -0
- package/src/cli/commands/command.ts +23 -0
- package/src/cli/commands/context-command.ts +38 -0
- package/src/cli/commands/doctor-command.ts +55 -0
- package/src/cli/commands/findings-command.ts +28 -0
- package/src/cli/commands/help-command.ts +13 -0
- package/src/cli/commands/registry.ts +34 -0
- package/src/cli/commands/review-command.ts +22 -0
- package/src/cli/commands/server-command.ts +37 -0
- package/src/cli/commands/setup-command.ts +34 -0
- package/src/cli/commands/watchdog-command.ts +34 -0
- package/src/cli/help.ts +43 -0
- package/src/cli/index.ts +62 -260
- package/src/core/arg-parser.ts +48 -0
- package/src/core/commands/findings.ts +163 -147
- package/src/core/commands/review.ts +18 -35
- package/src/core/commands/scaffold.ts +174 -64
- package/src/core/commands/setup.ts +16 -11
- package/src/core/commands/update.ts +7 -7
- package/src/core/commands/watchdog.ts +8 -80
- package/src/core/config.ts +9 -19
- package/src/core/defaults.ts +20 -0
- package/src/core/diff-source.ts +1 -13
- package/src/core/doc-discoverer.ts +25 -15
- package/src/core/domain/budget.ts +42 -0
- package/src/core/domain/classification.ts +94 -0
- package/src/core/domain/diff.ts +53 -0
- package/src/core/domain/watchdog.ts +96 -0
- package/src/core/engine/carryover.ts +67 -0
- package/src/core/engine/houndsman-engine.ts +51 -0
- package/src/core/engine/review-scope.ts +95 -0
- package/src/core/engine/review-session.ts +51 -0
- package/src/core/file-classifier.ts +11 -67
- package/src/core/findings-state.ts +189 -38
- package/src/core/git-revision.ts +14 -40
- package/src/core/git-runner.ts +46 -0
- package/src/core/github-review.ts +423 -0
- package/src/core/orchestrator.ts +116 -337
- package/src/core/paths.ts +9 -0
- package/src/core/pr-context.ts +28 -54
- package/src/core/project-config.ts +57 -0
- package/src/core/renderers/batch-presenter.ts +40 -0
- package/src/core/renderers/brief-renderer.ts +23 -0
- package/src/core/renderers/findings-presenter.ts +74 -0
- package/src/core/renderers/manifest-writer.ts +112 -0
- package/src/core/renderers/report-renderer.ts +66 -0
- package/src/core/renderers/verify-presenter.ts +50 -0
- package/src/core/renderers/watchdog-presenter.ts +100 -0
- package/src/core/report-builder.ts +12 -7
- package/src/core/run-layout.ts +108 -0
- package/src/core/run-status.ts +6 -11
- package/src/core/services/clock.ts +12 -0
- package/src/core/services/file-classifier.ts +34 -0
- package/src/core/services/fix-batch-builder.ts +13 -0
- package/src/core/services/gh-client.ts +54 -0
- package/src/core/services/git-repository.ts +123 -0
- package/src/core/services/project-files.ts +79 -0
- package/src/core/services/verifier.ts +58 -0
- package/src/core/verify.ts +2 -1
- package/src/core/watchdog-registry.ts +58 -16
- package/src/index.ts +31 -1
- package/src/server/client/App.tsx +174 -49
- package/src/server/client/ArtifactsPanel.tsx +2 -2
- package/src/server/client/AutoRefresh.tsx +38 -0
- package/src/server/client/Filters.tsx +72 -53
- package/src/server/client/FindingCard.tsx +212 -71
- package/src/server/client/FindingsList.tsx +8 -6
- package/src/server/client/RunList.tsx +30 -7
- package/src/server/client/SubmitModal.tsx +418 -0
- package/src/server/client/SummaryBar.tsx +65 -12
- package/src/server/client/constants.ts +9 -12
- package/src/server/client/format.ts +40 -0
- package/src/server/client/icons.tsx +143 -0
- package/src/server/client/types.ts +69 -0
- package/src/server/index.ts +316 -222
- package/src/server/run-repository.ts +291 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +814 -0
- package/src/tests/baseline.test.ts +513 -0
- package/src/tests/cli.test.ts +167 -0
- package/src/tests/domain.test.ts +328 -0
- package/src/tests/github-review.test.ts +320 -0
- package/src/tests/infrastructure.test.ts +210 -0
- package/src/tests/server.test.ts +117 -0
package/src/server/index.ts
CHANGED
|
@@ -6,31 +6,16 @@
|
|
|
6
6
|
* Bind padrão em `127.0.0.1` — ferramenta local, sem autenticação.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { join } from "node:path";
|
|
10
9
|
import { mediaPath } from "../core/assets";
|
|
11
|
-
import {
|
|
10
|
+
import type { HoundsmanConfig } from "../core/config";
|
|
11
|
+
import type { FindingUpdate } from "../core/findings-state";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type FindingExecState,
|
|
18
|
-
type FindingsFilter,
|
|
19
|
-
type FindingsStateFile,
|
|
20
|
-
FindingsStateStore,
|
|
21
|
-
type FindingUpdate,
|
|
22
|
-
fileReader,
|
|
23
|
-
filterFindings,
|
|
24
|
-
reorder,
|
|
25
|
-
type SyncResult,
|
|
26
|
-
summarize,
|
|
27
|
-
syncState,
|
|
28
|
-
} from "../core/findings-state";
|
|
29
|
-
import { STATE_FILE } from "../core/paths";
|
|
30
|
-
import { pingFindingsStatus, readRunStatus } from "../core/run-status";
|
|
31
|
-
import type { Severity } from "../core/types";
|
|
32
|
-
import { listRunSlugs, readRunFile, readRunJson, runDirForSlug } from "./artifacts";
|
|
13
|
+
defaultGhRunner,
|
|
14
|
+
type GhRunner,
|
|
15
|
+
type SubmitSelection,
|
|
16
|
+
} from "../core/github-review";
|
|
33
17
|
import { buildClientBundle } from "./client-build";
|
|
18
|
+
import { RunRepository } from "./run-repository";
|
|
34
19
|
|
|
35
20
|
type BunServer = ReturnType<typeof Bun.serve>;
|
|
36
21
|
|
|
@@ -67,118 +52,8 @@ function notFound(detail = "não encontrado"): Response {
|
|
|
67
52
|
// Estado / dados
|
|
68
53
|
// ---------------------------------------------------------------------------
|
|
69
54
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return buildStateFromRunDir(dir, manifest?.branch ?? slug, fileReader(process.cwd()));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Lê o estado da run. Se ausente, reconstrói dos artefatos e PERSISTE — os ids
|
|
77
|
-
* são aleatórios, então precisam ser estáveis entre requisições.
|
|
78
|
-
*/
|
|
79
|
-
function loadState(dir: string, slug: string): FindingsStateFile {
|
|
80
|
-
const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
|
|
81
|
-
const existing = store.read();
|
|
82
|
-
if (existing) return existing;
|
|
83
|
-
const built = buildForRun(dir, slug);
|
|
84
|
-
if (built) {
|
|
85
|
-
store.write(built);
|
|
86
|
-
return built;
|
|
87
|
-
}
|
|
88
|
-
return emptyState(slug);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function listRuns(artifactDir: string): unknown[] {
|
|
92
|
-
return listRunSlugs(artifactDir).map((slug) => {
|
|
93
|
-
const dir = join(artifactDir, slug);
|
|
94
|
-
const state = loadState(dir, slug);
|
|
95
|
-
const manifest = readRunJson<{ branch?: string; generatedAt?: string }>(
|
|
96
|
-
dir,
|
|
97
|
-
"manifest.json",
|
|
98
|
-
);
|
|
99
|
-
const status = readRunStatus(dir);
|
|
100
|
-
return {
|
|
101
|
-
slug,
|
|
102
|
-
branch: state.branch || manifest?.branch || slug,
|
|
103
|
-
updatedAt: status?.updatedAt ?? state.updatedAt,
|
|
104
|
-
status: status?.status ?? null,
|
|
105
|
-
summary: summarize(state),
|
|
106
|
-
};
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function runDetail(dir: string, slug: string): unknown {
|
|
111
|
-
const state = loadState(dir, slug);
|
|
112
|
-
const classification = readRunJson<{
|
|
113
|
-
watchdogs?: Record<string, string[]>;
|
|
114
|
-
unclassified?: string[];
|
|
115
|
-
deleted?: string[];
|
|
116
|
-
}>(dir, "classification.json");
|
|
117
|
-
return {
|
|
118
|
-
slug,
|
|
119
|
-
branch: state.branch || slug,
|
|
120
|
-
status: readRunStatus(dir),
|
|
121
|
-
manifest: readRunJson(dir, "manifest.json"),
|
|
122
|
-
summary: summarize(state),
|
|
123
|
-
watchdogs: Object.keys(classification?.watchdogs ?? {}),
|
|
124
|
-
unclassified: classification?.unclassified ?? [],
|
|
125
|
-
deleted: classification?.deleted ?? [],
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function findingsFor(dir: string, slug: string, params: URLSearchParams): unknown {
|
|
130
|
-
const state = loadState(dir, slug);
|
|
131
|
-
const filter: FindingsFilter = {
|
|
132
|
-
decision: (params.get("decision") as FindingDecision) || undefined,
|
|
133
|
-
execState: (params.get("exec") as FindingExecState) || undefined,
|
|
134
|
-
severity: (params.get("severity") as Severity) || undefined,
|
|
135
|
-
watchdog: params.get("watchdog") || undefined,
|
|
136
|
-
file: params.get("file") || undefined,
|
|
137
|
-
includeAbsent: params.get("all") === "1",
|
|
138
|
-
};
|
|
139
|
-
return {
|
|
140
|
-
branch: state.branch,
|
|
141
|
-
findings: filterFindings(state, filter),
|
|
142
|
-
summary: summarize(state),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function updateFinding(
|
|
147
|
-
dir: string,
|
|
148
|
-
slug: string,
|
|
149
|
-
id: string,
|
|
150
|
-
request: Request,
|
|
151
|
-
): Promise<Response> {
|
|
152
|
-
const body = (await request.json().catch(() => ({}))) as FindingUpdate;
|
|
153
|
-
const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
|
|
154
|
-
const state = store.read() ?? buildForRun(dir, slug) ?? emptyState(slug);
|
|
155
|
-
const finding = state.findings[id];
|
|
156
|
-
if (!finding) return notFound(`finding '${id}' não encontrado`);
|
|
157
|
-
applyFindingUpdate(finding, body);
|
|
158
|
-
reorder(state);
|
|
159
|
-
store.write(state);
|
|
160
|
-
pingFindingsStatus(dir, state);
|
|
161
|
-
return json({ finding });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function syncRun(dir: string, slug: string): Response {
|
|
165
|
-
const store = new FindingsStateStore(join(dir, STATE_FILE), slug);
|
|
166
|
-
const state = store.read() ?? buildForRun(dir, slug) ?? emptyState(slug);
|
|
167
|
-
const result: SyncResult = syncState(state, fileReader(process.cwd()));
|
|
168
|
-
store.write(state);
|
|
169
|
-
pingFindingsStatus(dir, state);
|
|
170
|
-
return json({ result, summary: summarize(state) });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function serveArtifact(dir: string, name: string): Response {
|
|
174
|
-
if (!/\.(json|md)$/i.test(name)) return notFound("tipo de artefato não servido");
|
|
175
|
-
const content = readRunFile(dir, name);
|
|
176
|
-
if (content === null) return notFound(`artefato '${name}' não encontrado`);
|
|
177
|
-
const type = name.toLowerCase().endsWith(".json")
|
|
178
|
-
? "application/json; charset=utf-8"
|
|
179
|
-
: "text/markdown; charset=utf-8";
|
|
180
|
-
return text(content, type);
|
|
181
|
-
}
|
|
55
|
+
/** Repositório das runs (dados) — ver `run-repository.ts`. */
|
|
56
|
+
type ServerDeps = { ghRunner?: GhRunner };
|
|
182
57
|
|
|
183
58
|
// ---------------------------------------------------------------------------
|
|
184
59
|
// Rotas
|
|
@@ -196,7 +71,11 @@ async function serveLogo(): Promise<Response> {
|
|
|
196
71
|
});
|
|
197
72
|
}
|
|
198
73
|
|
|
199
|
-
async function route(
|
|
74
|
+
async function route(
|
|
75
|
+
request: Request,
|
|
76
|
+
runs: RunRepository,
|
|
77
|
+
ghRunner: GhRunner,
|
|
78
|
+
): Promise<Response> {
|
|
200
79
|
const url = new URL(request.url);
|
|
201
80
|
const path = url.pathname;
|
|
202
81
|
|
|
@@ -210,54 +89,77 @@ async function route(request: Request, artifactDir: string): Promise<Response> {
|
|
|
210
89
|
if (path === "/api/health") return json({ ok: true });
|
|
211
90
|
|
|
212
91
|
if (path === "/api/runs" && request.method === "GET") {
|
|
213
|
-
return json({ runs:
|
|
92
|
+
return json({ runs: runs.list() });
|
|
214
93
|
}
|
|
215
94
|
|
|
216
95
|
const match = path.match(API_RUN);
|
|
217
96
|
if (!match) return notFound();
|
|
218
97
|
const slug = decodeURIComponent(match[1]);
|
|
219
98
|
const rest = match[2] ?? "";
|
|
220
|
-
|
|
221
|
-
if (!dir) return notFound(`run '${slug}' não encontrada`);
|
|
99
|
+
if (!runs.exists(slug)) return notFound(`run '${slug}' não encontrada`);
|
|
222
100
|
|
|
223
|
-
if (rest === "" || rest === "/") return json(
|
|
101
|
+
if (rest === "" || rest === "/") return json(runs.detail(slug));
|
|
224
102
|
|
|
225
103
|
if (rest === "/findings" && request.method === "GET") {
|
|
226
|
-
return json(
|
|
104
|
+
return json(runs.findings(slug, url.searchParams));
|
|
105
|
+
}
|
|
106
|
+
if (rest === "/sync" && request.method === "POST") return json(runs.sync(slug));
|
|
107
|
+
if (rest === "/submit-preview" && request.method === "GET") {
|
|
108
|
+
return json(runs.submitPreview(slug, ghRunner));
|
|
109
|
+
}
|
|
110
|
+
if (rest === "/submit" && request.method === "POST") {
|
|
111
|
+
const body = (await request.json().catch(() => ({}))) as SubmitSelection;
|
|
112
|
+
const selection: SubmitSelection = {
|
|
113
|
+
severities: body.severities,
|
|
114
|
+
execStates: body.execStates,
|
|
115
|
+
includeSent: body.includeSent,
|
|
116
|
+
};
|
|
117
|
+
return json(runs.submit(slug, selection, ghRunner));
|
|
227
118
|
}
|
|
228
|
-
if (rest === "/sync" && request.method === "POST") return syncRun(dir, slug);
|
|
229
119
|
|
|
230
120
|
const findingMatch = rest.match(/^\/findings\/([^/]+)$/);
|
|
231
121
|
if (findingMatch) {
|
|
232
122
|
const id = decodeURIComponent(findingMatch[1]);
|
|
233
123
|
if (request.method === "GET") {
|
|
234
|
-
const
|
|
235
|
-
const finding = state.findings[id];
|
|
124
|
+
const finding = runs.finding(slug, id);
|
|
236
125
|
return finding
|
|
237
126
|
? json({ finding })
|
|
238
127
|
: notFound(`finding '${id}' não encontrado`);
|
|
239
128
|
}
|
|
240
129
|
if (request.method === "PATCH" || request.method === "POST") {
|
|
241
|
-
|
|
130
|
+
const body = (await request.json().catch(() => ({}))) as FindingUpdate;
|
|
131
|
+
const finding = runs.update(slug, id, body);
|
|
132
|
+
return finding
|
|
133
|
+
? json({ finding })
|
|
134
|
+
: notFound(`finding '${id}' não encontrado`);
|
|
242
135
|
}
|
|
243
136
|
}
|
|
244
137
|
|
|
245
138
|
const artifactMatch = rest.match(/^\/artifacts\/(.+)$/);
|
|
246
139
|
if (artifactMatch && request.method === "GET") {
|
|
247
|
-
|
|
140
|
+
const name = decodeURIComponent(artifactMatch[1]);
|
|
141
|
+
const artifact = runs.artifact(slug, name);
|
|
142
|
+
if (!artifact) return notFound(`artefato '${name}' não encontrado`);
|
|
143
|
+
return text(artifact.content, artifact.type);
|
|
248
144
|
}
|
|
249
145
|
|
|
250
146
|
return notFound();
|
|
251
147
|
}
|
|
252
148
|
|
|
253
|
-
/**
|
|
254
|
-
export
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
149
|
+
/** Servidor local do houndsman (rotas + shell). */
|
|
150
|
+
export class HoundsmanServer {
|
|
151
|
+
private readonly runs: RunRepository;
|
|
152
|
+
private readonly ghRunner: GhRunner;
|
|
153
|
+
|
|
154
|
+
constructor(config: HoundsmanConfig, deps: ServerDeps = {}) {
|
|
155
|
+
this.runs = RunRepository.fromConfig(config);
|
|
156
|
+
this.ghRunner = deps.ghRunner ?? defaultGhRunner;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Handler de requisições (testável sem abrir porta). */
|
|
160
|
+
readonly handler = async (request: Request): Promise<Response> => {
|
|
259
161
|
try {
|
|
260
|
-
return await route(request,
|
|
162
|
+
return await route(request, this.runs, this.ghRunner);
|
|
261
163
|
} catch (error) {
|
|
262
164
|
return json(
|
|
263
165
|
{ error: error instanceof Error ? error.message : String(error) },
|
|
@@ -265,24 +167,41 @@ export function createRequestHandler(
|
|
|
265
167
|
);
|
|
266
168
|
}
|
|
267
169
|
};
|
|
170
|
+
|
|
171
|
+
/** Sobe o servidor e devolve a instância + URL. */
|
|
172
|
+
start(options: { port?: number; host?: string } = {}): {
|
|
173
|
+
server: BunServer;
|
|
174
|
+
url: string;
|
|
175
|
+
} {
|
|
176
|
+
const host = options.host ?? "127.0.0.1";
|
|
177
|
+
const port = options.port ?? 7331;
|
|
178
|
+
const server = Bun.serve({ hostname: host, port, fetch: this.handler });
|
|
179
|
+
return { server, url: `http://${host}:${server.port}` };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Cria o handler de requisições (testável sem abrir porta). */
|
|
184
|
+
export function createRequestHandler(
|
|
185
|
+
config: HoundsmanConfig,
|
|
186
|
+
deps: ServerDeps = {},
|
|
187
|
+
): (request: Request) => Promise<Response> {
|
|
188
|
+
return new HoundsmanServer(config, deps).handler;
|
|
268
189
|
}
|
|
269
190
|
|
|
270
191
|
export type ServerOptions = {
|
|
271
192
|
config: HoundsmanConfig;
|
|
272
193
|
port?: number;
|
|
273
194
|
host?: string;
|
|
195
|
+
/** Runner do `gh` (injetável nos testes). */
|
|
196
|
+
ghRunner?: GhRunner;
|
|
274
197
|
};
|
|
275
198
|
|
|
276
199
|
/** Sobe o servidor e devolve a instância + URL. */
|
|
277
200
|
export function startServer(options: ServerOptions): { server: BunServer; url: string } {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
hostname: host,
|
|
282
|
-
port,
|
|
283
|
-
fetch: createRequestHandler(options.config),
|
|
201
|
+
return new HoundsmanServer(options.config, { ghRunner: options.ghRunner }).start({
|
|
202
|
+
port: options.port,
|
|
203
|
+
host: options.host,
|
|
284
204
|
});
|
|
285
|
-
return { server, url: `http://${host}:${server.port}` };
|
|
286
205
|
}
|
|
287
206
|
|
|
288
207
|
// ---------------------------------------------------------------------------
|
|
@@ -290,70 +209,240 @@ export function startServer(options: ServerOptions): { server: BunServer; url: s
|
|
|
290
209
|
// ---------------------------------------------------------------------------
|
|
291
210
|
|
|
292
211
|
const STYLES = `
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
.
|
|
316
|
-
.
|
|
317
|
-
|
|
318
|
-
.
|
|
319
|
-
.
|
|
320
|
-
.
|
|
321
|
-
.
|
|
322
|
-
.
|
|
323
|
-
.
|
|
324
|
-
.
|
|
325
|
-
|
|
326
|
-
.
|
|
327
|
-
.
|
|
328
|
-
.
|
|
329
|
-
.
|
|
330
|
-
.
|
|
331
|
-
.
|
|
332
|
-
.
|
|
333
|
-
.
|
|
334
|
-
.
|
|
335
|
-
.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
.
|
|
339
|
-
.
|
|
340
|
-
.
|
|
341
|
-
.
|
|
342
|
-
.
|
|
343
|
-
.
|
|
344
|
-
.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
.
|
|
348
|
-
.
|
|
349
|
-
|
|
350
|
-
.
|
|
351
|
-
.btn
|
|
352
|
-
.
|
|
353
|
-
.
|
|
354
|
-
.
|
|
355
|
-
.
|
|
212
|
+
@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
|
213
|
+
|
|
214
|
+
:root {
|
|
215
|
+
color-scheme: dark;
|
|
216
|
+
--bg:#0a0a0c; --surface:#101014; --surface-2:#17171c; --card:#131318;
|
|
217
|
+
--border:rgba(255,255,255,.07); --border-strong:rgba(255,255,255,.14);
|
|
218
|
+
--fg:#ece9e4; --muted:#9d978e; --muted-2:#6d6862;
|
|
219
|
+
--accent:#f0a52c; --accent-strong:#ffc247; --accent-soft:rgba(240,165,44,.07); --accent-line:rgba(240,165,44,.25);
|
|
220
|
+
--blocking:#f2595e; --attention:#eda13a; --suggestion:#5aa4f0; --nit:#8d8a85; --success:#45c07a;
|
|
221
|
+
--radius:16px; --radius-lg:18px;
|
|
222
|
+
--font-sans:"Poppins","Space Grotesk","Geist Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
223
|
+
--font-mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
224
|
+
}
|
|
225
|
+
* { box-sizing:border-box; }
|
|
226
|
+
html, body { height:100%; }
|
|
227
|
+
body { margin:0; font-size:14px; background:var(--bg); color:var(--fg); font-family:var(--font-sans); -webkit-font-smoothing:antialiased; }
|
|
228
|
+
body::before { content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
|
|
229
|
+
background:radial-gradient(820px 420px at 8% -12%, rgba(240,165,44,.08), transparent 62%); }
|
|
230
|
+
code, pre, .mono { font-family:var(--font-mono); }
|
|
231
|
+
::-webkit-scrollbar { width:10px; height:10px; }
|
|
232
|
+
::-webkit-scrollbar-thumb { background:rgba(255,255,255,.1); border:2px solid transparent; border-radius:999px; background-clip:padding-box; }
|
|
233
|
+
::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,.18); background-clip:padding-box; }
|
|
234
|
+
.app { position:relative; z-index:1; display:flex; height:100vh; overflow:hidden; }
|
|
235
|
+
.sidebar { width:292px; flex:0 0 292px; display:flex; flex-direction:column; gap:14px; padding:18px 14px;
|
|
236
|
+
background:var(--surface); border-right:1px solid var(--border); overflow-y:auto; }
|
|
237
|
+
.brand { display:flex; align-items:center; gap:12px; }
|
|
238
|
+
.brand .logo { width:42px; height:42px; object-fit:contain; border-radius:12px; background:var(--surface-2); border:1px solid var(--border); }
|
|
239
|
+
.sidebar h1 { font-size:15.5px; font-weight:700; letter-spacing:-.01em; margin:0; }
|
|
240
|
+
.sub { color:var(--muted); font-size:11.5px; margin:0; }
|
|
241
|
+
.section-label { margin:4px 0 0; font-size:10px; font-weight:700; letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
|
|
242
|
+
.runs { display:flex; flex-direction:column; gap:6px; }
|
|
243
|
+
.run { display:flex; flex-direction:column; gap:8px; text-align:left; cursor:pointer; font:inherit; color:inherit; padding:11px 13px;
|
|
244
|
+
background:transparent; border:1px solid transparent; border-radius:var(--radius); transition:background .15s ease, border-color .15s ease; }
|
|
245
|
+
.run:hover { background:var(--surface-2); border-color:var(--border); }
|
|
246
|
+
.run.active { background:var(--accent-soft); border-color:var(--accent-line); }
|
|
247
|
+
.run-top { display:flex; align-items:flex-start; gap:8px; }
|
|
248
|
+
.run-icon { flex:none; margin-top:2px; color:var(--muted-2); }
|
|
249
|
+
.run.active .run-icon { color:var(--accent); }
|
|
250
|
+
.run-branch { font-size:13.5px; font-weight:600; line-height:1.35; word-break:break-word; }
|
|
251
|
+
.run-meta { display:flex; align-items:center; gap:10px; font-family:var(--font-mono); font-size:11px; color:var(--muted); }
|
|
252
|
+
.run-when { display:inline-flex; align-items:center; gap:6px; }
|
|
253
|
+
.run-count { margin-left:auto; color:var(--muted-2); }
|
|
254
|
+
.run-badge { align-self:flex-start; display:inline-flex; align-items:center; gap:6px; padding:4px 9px; border-radius:999px;
|
|
255
|
+
font-size:10px; font-weight:700; letter-spacing:.07em; text-transform:uppercase;
|
|
256
|
+
color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.32); }
|
|
257
|
+
.run-status { font-size:10px; font-weight:600; letter-spacing:.07em; text-transform:uppercase; color:var(--muted-2); }
|
|
258
|
+
.status-reviewing, .status-fixing { color:var(--accent); }
|
|
259
|
+
.status-review_done { color:var(--suggestion); }
|
|
260
|
+
.status-fix_done { color:var(--success); }
|
|
261
|
+
.status-timeout { color:var(--blocking); }
|
|
262
|
+
.main { flex:1; min-width:0; display:flex; flex-direction:column; overflow-y:auto; }
|
|
263
|
+
.topbar { position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; gap:16px;
|
|
264
|
+
padding:13px 24px; border-bottom:1px solid var(--border); background:rgba(16,16,20,.74);
|
|
265
|
+
backdrop-filter:blur(16px) saturate(140%); -webkit-backdrop-filter:blur(16px) saturate(140%); }
|
|
266
|
+
.segmented { display:inline-flex; gap:4px; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
|
|
267
|
+
.seg-btn { display:inline-flex; align-items:center; gap:8px; font:inherit; font-size:12.5px; font-weight:600; cursor:pointer;
|
|
268
|
+
color:var(--muted); background:transparent; border:1px solid transparent; border-radius:999px; padding:7px 16px;
|
|
269
|
+
transition:color .15s ease, background .15s ease; }
|
|
270
|
+
.seg-btn:hover { color:var(--fg); }
|
|
271
|
+
.seg-btn.active { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); box-shadow:0 4px 14px rgba(240,165,44,.24); }
|
|
272
|
+
.topbar-right { display:flex; align-items:center; gap:14px; }
|
|
273
|
+
.auto-refresh { display:inline-flex; align-items:center; gap:8px; white-space:nowrap; font-family:var(--font-mono); font-size:11.5px; color:var(--muted); }
|
|
274
|
+
.dot { width:8px; height:8px; border-radius:50%; background:var(--success); box-shadow:0 0 0 3px rgba(69,192,122,.14); }
|
|
275
|
+
.dot.busy { background:var(--accent); box-shadow:0 0 0 3px rgba(240,165,44,.18); animation:pulse 1.1s ease-in-out infinite; }
|
|
276
|
+
.btn { display:inline-flex; align-items:center; gap:8px; font:inherit; font-size:12.5px; font-weight:500; cursor:pointer; color:var(--fg);
|
|
277
|
+
padding:8px 15px; background:var(--surface-2); border:1px solid var(--border-strong); border-radius:999px;
|
|
278
|
+
transition:background .15s ease, border-color .15s ease, color .15s ease; }
|
|
279
|
+
.btn:hover { background:#202027; border-color:var(--accent-line); }
|
|
280
|
+
.btn:disabled { opacity:.55; cursor:not-allowed; }
|
|
281
|
+
.icon.spin { animation:spin .8s linear infinite; }
|
|
282
|
+
.content { width:100%; max-width:1180px; margin:0 auto; padding:22px 24px 64px; }
|
|
283
|
+
.page-header { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; }
|
|
284
|
+
.page-header-text { min-width:0; }
|
|
285
|
+
.page-header-actions { display:flex; align-items:center; gap:10px; flex:none; }
|
|
286
|
+
.btn.submit-review { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); border-color:transparent; box-shadow:0 4px 14px rgba(240,165,44,.22); }
|
|
287
|
+
.btn.submit-review:hover { background:linear-gradient(180deg, var(--accent-strong), var(--accent)); border-color:transparent; filter:brightness(1.07); }
|
|
288
|
+
.page-title { margin:0 0 8px; font-size:26px; font-weight:700; letter-spacing:-.02em; word-break:break-word; }
|
|
289
|
+
.page-sub { margin:0; max-width:78ch; color:var(--muted); font-size:12.5px; line-height:1.55; }
|
|
290
|
+
.page-sub code { color:var(--fg); background:rgba(255,255,255,.06); border-radius:6px; padding:1px 6px; font-size:11.5px; }
|
|
291
|
+
.error { color:#ff8b8f; font-size:12.5px; }
|
|
356
292
|
.empty { color:var(--muted); font-size:13px; }
|
|
293
|
+
.kpis { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:12px; margin:20px 0 18px; }
|
|
294
|
+
.kpi { display:flex; flex-direction:column; gap:8px; padding:15px 16px; background:var(--card);
|
|
295
|
+
border:1px solid var(--border); border-radius:var(--radius-lg); }
|
|
296
|
+
.kpi-label { font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); }
|
|
297
|
+
.kpi-value { font-size:30px; font-weight:700; line-height:1; letter-spacing:-.02em; font-variant-numeric:tabular-nums; }
|
|
298
|
+
.kpi-hint { font-family:var(--font-mono); font-size:11px; color:var(--muted-2); }
|
|
299
|
+
.kpi-execute .kpi-value { color:var(--accent); }
|
|
300
|
+
.kpi-ignore .kpi-value { color:var(--muted); }
|
|
301
|
+
.kpi-done .kpi-value { color:var(--success); }
|
|
302
|
+
.progress { margin:0 0 22px; }
|
|
303
|
+
.progress-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; font-size:11.5px; color:var(--muted); }
|
|
304
|
+
.progress-head .pct { color:var(--fg); font-family:var(--font-mono); }
|
|
305
|
+
.progress-track { height:8px; border-radius:999px; background:var(--surface-2); border:1px solid var(--border); overflow:hidden; }
|
|
306
|
+
.progress-fill { display:block; height:100%; border-radius:999px; background:linear-gradient(90deg, #2c8f57, var(--success)); transition:width .35s ease; }
|
|
307
|
+
.filters { display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-bottom:20px; }
|
|
308
|
+
.search { display:flex; align-items:center; gap:8px; flex:1 1 240px; max-width:330px; padding:8px 14px; color:var(--muted);
|
|
309
|
+
background:var(--surface-2); border:1px solid var(--border-strong); border-radius:999px; }
|
|
310
|
+
.search:focus-within { color:var(--accent); border-color:var(--accent-line); }
|
|
311
|
+
.search input { flex:1; min-width:0; font:inherit; font-size:12.5px; color:var(--fg); background:transparent; border:0; outline:0; }
|
|
312
|
+
.search input::placeholder { color:var(--muted-2); }
|
|
313
|
+
.search-clear { padding:0 2px; font-size:15px; line-height:1; color:var(--muted); background:transparent; border:0; cursor:pointer; }
|
|
314
|
+
.chips { display:inline-flex; gap:3px; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
|
|
315
|
+
.chip { display:inline-flex; align-items:center; gap:6px; font:inherit; font-size:12px; font-weight:600; cursor:pointer; color:var(--muted);
|
|
316
|
+
padding:6px 13px; background:transparent; border:1px solid transparent; border-radius:999px; transition:color .15s ease, background .15s ease; }
|
|
317
|
+
.chip:hover { color:var(--fg); }
|
|
318
|
+
.chip.active { color:#14100a; background:#e9e5df!important; }
|
|
319
|
+
.sev-group { margin-bottom:24px; }
|
|
320
|
+
.sev-head { display:flex; align-items:center; gap:8px; margin:0 0 10px; font-size:11px; font-weight:700;
|
|
321
|
+
letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
|
|
322
|
+
.sev-head .n { font-family:var(--font-mono); letter-spacing:0; }
|
|
323
|
+
.sev-group.sev-blocking .sev-head { color:var(--blocking); }
|
|
324
|
+
.sev-group.sev-attention .sev-head { color:var(--attention); }
|
|
325
|
+
.sev-group.sev-suggestion .sev-head { color:var(--suggestion); }
|
|
326
|
+
.sev-group.sev-nit .sev-head { color:var(--nit); }
|
|
327
|
+
.card { position:relative; margin-bottom:20px; overflow:hidden; background:var(--card);
|
|
328
|
+
border:1px solid var(--border); border-left:4px solid var(--border-strong); border-radius:var(--radius-lg);
|
|
329
|
+
transition:background .15s ease, border-color .15s ease, opacity .2s ease; }
|
|
330
|
+
.card:hover { background:#16161b; }
|
|
331
|
+
.card.sev-blocking { border-left-color:var(--blocking); }
|
|
332
|
+
.card.sev-attention { border-left-color:var(--attention); }
|
|
333
|
+
.card.sev-suggestion { border-left-color:var(--suggestion); }
|
|
334
|
+
.card.sev-nit { border-left-color:var(--nit); }
|
|
335
|
+
.card.absent { opacity:.5; }
|
|
336
|
+
.card.done { opacity:.6; }
|
|
337
|
+
.card-head { display:flex; align-items:center; gap:10px; padding:12px 14px 0; }
|
|
338
|
+
.chev { display:grid; place-items:center; flex:none; width:22px; height:22px; cursor:pointer; color:var(--muted-2);
|
|
339
|
+
background:transparent; border:1px solid transparent; border-radius:7px; }
|
|
340
|
+
.chev:hover { color:var(--fg); background:rgba(255,255,255,.06); }
|
|
341
|
+
.chev:focus-visible { color:var(--fg); border-color:var(--accent-line); outline:none; }
|
|
342
|
+
.chev svg { transition:transform .18s ease; }
|
|
343
|
+
.chev.open svg { transform:rotate(90deg); }
|
|
344
|
+
.sev-badge { display:inline-flex; align-items:center; flex:none; padding:3px 9px; border-radius:999px;
|
|
345
|
+
font-size:10px; letter-spacing:.08em; text-transform:uppercase; }
|
|
346
|
+
.card.sev-blocking .sev-badge { color:var(--blocking); background:rgba(242,89,94,.12); }
|
|
347
|
+
.card.sev-attention .sev-badge { color:var(--attention); background:rgba(237,161,58,.12); }
|
|
348
|
+
.card.sev-suggestion .sev-badge { color:var(--suggestion); background:rgba(90,164,240,.12); }
|
|
349
|
+
.card.sev-nit .sev-badge { color:var(--nit); background:rgba(255,255,255,.06); }
|
|
350
|
+
.loc { display:inline-flex; align-items:center; gap:7px; max-width: 406ch; overflow:hidden; white-space:break-spaces; text-overflow:ellipsis;
|
|
351
|
+
padding:4px 9px; font-family:var(--font-mono); font-size:11.5px; color:#aba7a1; text-decoration:none; }
|
|
352
|
+
.loc:hover { color:#fff; }
|
|
353
|
+
.loc .ext { flex:none; opacity:0; transition:opacity .15s ease; }
|
|
354
|
+
.loc:hover .ext { opacity:1; }
|
|
355
|
+
.loc.static { color:var(--muted); }
|
|
356
|
+
.count { padding:2px 7px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
|
|
357
|
+
background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
|
|
358
|
+
.spacer { flex:1; }
|
|
359
|
+
.done-btn { display:inline-flex; align-items:center; gap:7px; flex:none; font:inherit; font-size:12px; font-weight:600; cursor:pointer;
|
|
360
|
+
padding:7px 13px; color:var(--muted); background:transparent; border:1px solid var(--border-strong); border-radius:999px;
|
|
361
|
+
transition:color .15s ease, background .15s ease, border-color .15s ease; }
|
|
362
|
+
.done-btn:hover { color:var(--fg); border-color:rgba(69,192,122,.55); }
|
|
363
|
+
.done-btn:disabled { opacity:.55; cursor:not-allowed; }
|
|
364
|
+
.done-btn.is-done { color:var(--success); background:rgba(69,192,122,.14); border-color:rgba(69,192,122,.5); }
|
|
365
|
+
.card-title { margin:9px 0 0; padding:0 16px 20px; font-size:14px; font-weight:400; }
|
|
366
|
+
.card.done .card-title { color:#b7b7b7; text-decoration:line-through; text-decoration-color:rgba(255,255,255,.15); }
|
|
367
|
+
.card-body { display:flex; flex-direction:column; gap:12px; padding:20px 16px 15px; background:rgba(255,255,255,.012); border-top:1px solid var(--border); }
|
|
368
|
+
.meta-row { display:flex; flex-wrap:wrap; gap:14px; font-size:11.5px; color:var(--muted); }
|
|
369
|
+
.meta { display:inline-flex; align-items:center; gap:7px; }
|
|
370
|
+
.meta-k { font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
|
|
371
|
+
.meta code { padding:2px 6px; font-size:11.5px; color:var(--fg); background:rgba(255,255,255,.04); border:1px solid var(--border); border-radius:6px; }
|
|
372
|
+
.action-block { display:flex; gap:11px; padding:16px; background:var(--accent-soft); border:1px solid var(--accent-line); border-radius:var(--radius); }
|
|
373
|
+
.action-icon { flex:none; display:inline-block; margin-right:5px; margin-bottom:-5px; color:var(--accent); }
|
|
374
|
+
.action-label { display:inline-block; font-size:10px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--accent); margin-right:5px; }
|
|
375
|
+
.action-block p { margin:0; font-size:13px; color:#f0ece6; }
|
|
376
|
+
.decision-tags-block { display:flex; align-items:center; gap:8px; }
|
|
377
|
+
.evidence { margin:0; padding: 16px; white-space:break-spaces; font-size:12px; line-height:1.6; color:#cdc7bf;
|
|
378
|
+
background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
|
|
379
|
+
.tags { display:flex; flex-wrap:wrap; align-items:center; gap:7px; margin-left: auto; }
|
|
380
|
+
.tags-label { display:inline-flex; align-items:center; gap:6px; font-size:10px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted-2); }
|
|
381
|
+
.tag { display:inline-flex; align-items:center; gap:6px; padding:3px 9px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
|
|
382
|
+
background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
|
|
383
|
+
.decisions { display:inline-flex; gap:3px; align-self:flex-start; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
|
|
384
|
+
.dec-btn { font:inherit; font-size:12px; font-weight:600; cursor:pointer; color:var(--muted); padding:6px 15px;
|
|
385
|
+
background:transparent; border:1px solid transparent; border-radius:999px; transition:color .15s ease, background .15s ease; }
|
|
386
|
+
.dec-btn:hover { color:var(--fg); }
|
|
387
|
+
.dec-btn:disabled { opacity:.55; cursor:not-allowed; }
|
|
388
|
+
.dec-btn.dec-execute.active { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); }
|
|
389
|
+
.dec-btn.dec-ignore.active { color:#ffd2d3; background:rgba(242,89,94,.18); box-shadow:inset 0 0 0 1px rgba(242,89,94,.45); }
|
|
390
|
+
.dec-btn.dec-pending.active { color:var(--fg); background:rgba(255,255,255,.08); box-shadow:inset 0 0 0 1px var(--border-strong); }
|
|
391
|
+
.comment { position:relative; }
|
|
392
|
+
.comment textarea { width:100%; min-height:58px; padding:11px 13px 26px; resize:vertical; font:inherit; font-size:12.5px; line-height:1.5;
|
|
393
|
+
color:var(--fg); background:var(--surface-2); border:1px solid var(--border-strong); border-radius:var(--radius); outline:none;
|
|
394
|
+
transition:border-color .15s ease, box-shadow .15s ease; }
|
|
395
|
+
.comment textarea::placeholder { color:var(--muted-2); }
|
|
396
|
+
.comment textarea:focus { border-color:var(--accent-line); box-shadow:0 0 0 3px rgba(240,165,44,.12); }
|
|
397
|
+
.comment-status { position:absolute; right:12px; bottom:9px; display:inline-flex; align-items:center; gap:6px;
|
|
398
|
+
font-family:var(--font-mono); font-size:11px; color:var(--muted); }
|
|
399
|
+
.comment-status.saved { color:var(--success); }
|
|
400
|
+
.artifacts { display:flex; flex-direction:column; }
|
|
401
|
+
.artifact-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:12px; }
|
|
402
|
+
.artifact-tabs .chip { border:1px solid var(--border); background:var(--surface-2); }
|
|
403
|
+
.artifact-desc { margin:0 0 12px; color:var(--muted); font-size:12.5px; line-height:1.55; }
|
|
404
|
+
.artifact-view { margin:0; padding:14px; max-height:70vh; overflow:auto; font-size:12px; line-height:1.6; color:#cdc7bf;
|
|
405
|
+
background:#0b0b0e; border:1px solid var(--border); border-radius:var(--radius); }
|
|
406
|
+
.sent-tag { display:inline-flex; align-items:center; gap:5px; padding:2px 8px; font-size:10px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
|
|
407
|
+
color:var(--success); background:rgba(69,192,122,.12); border:1px solid rgba(69,192,122,.32); border-radius:999px; text-decoration:none; }
|
|
408
|
+
.sent-tag:hover { color:#fff; }
|
|
409
|
+
.modal-layer { position:fixed; inset:0; z-index:50; display:grid; place-items:center; padding:24px; }
|
|
410
|
+
.modal-backdrop { position:absolute; inset:0; width:100%; padding:0; background:rgba(5,5,7,.66); border:0; cursor:default; backdrop-filter:blur(5px); -webkit-backdrop-filter:blur(5px); }
|
|
411
|
+
.modal { position:relative; width:min(640px, 100%); max-height:min(78vh, 780px); display:flex; flex-direction:column; overflow:hidden;
|
|
412
|
+
background:var(--surface); border:1px solid var(--border-strong); border-radius:var(--radius-lg); box-shadow:0 24px 70px rgba(0,0,0,.5); }
|
|
413
|
+
.modal-head { display:flex; align-items:flex-start; justify-content:space-between; gap:14px; padding:18px 20px 14px; border-bottom:1px solid var(--border); }
|
|
414
|
+
.modal-title { margin:0; font-size:17px; font-weight:700; letter-spacing:-.01em; }
|
|
415
|
+
.modal-sub { margin:5px 0 0; font-size:12px; color:var(--muted); line-height:1.5; }
|
|
416
|
+
.modal-close { flex:none; display:grid; place-items:center; width:30px; height:30px; font:inherit; font-size:19px; line-height:1; color:var(--muted); background:transparent; border:1px solid var(--border); border-radius:9px; cursor:pointer; }
|
|
417
|
+
.modal-close:hover { color:var(--fg); border-color:var(--border-strong); }
|
|
418
|
+
.modal-body { display:flex; flex-direction:column; gap:16px; padding:16px 20px; overflow-y:auto; }
|
|
419
|
+
.modal-foot { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 20px; background:var(--surface-2); border-top:1px solid var(--border); }
|
|
420
|
+
.submit-field { display:flex; flex-direction:column; gap:8px; }
|
|
421
|
+
.submit-label { font-size:10px; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:var(--muted-2); }
|
|
422
|
+
.submit-summary { padding:13px 15px; font-size:12.5px; color:var(--fg); background:var(--accent-soft); border:1px solid var(--accent-line); border-radius:var(--radius); }
|
|
423
|
+
.submit-summary b { font-family:var(--font-mono); }
|
|
424
|
+
.submit-list { display:flex; flex-direction:column; gap:6px; max-height:240px; overflow-y:auto; padding-right:2px; }
|
|
425
|
+
.submit-item { display:flex; align-items:center; gap:10px; padding:8px 11px; font-size:12px; background:var(--surface-2); border:1px solid var(--border); border-radius:10px; }
|
|
426
|
+
.submit-item.sent { opacity:.7; }
|
|
427
|
+
.submit-item .rule { flex:1; min-width:0; color:var(--muted); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
428
|
+
.submit-empty { color:var(--muted); font-size:12.5px; }
|
|
429
|
+
.submit-field .chips { flex-wrap:wrap; }
|
|
430
|
+
.submit-check { display:inline-flex; align-items:center; gap:8px; font-size:12.5px; color:var(--muted); cursor:pointer; }
|
|
431
|
+
.submit-check input { accent-color:var(--accent); }
|
|
432
|
+
.submit-actions { display:flex; align-items:center; gap:8px; }
|
|
433
|
+
.sev-badge.sev-blocking { color:var(--blocking); background:rgba(242,89,94,.12); }
|
|
434
|
+
.sev-badge.sev-attention { color:var(--attention); background:rgba(237,161,58,.12); }
|
|
435
|
+
.sev-badge.sev-suggestion { color:var(--suggestion); background:rgba(90,164,240,.12); }
|
|
436
|
+
.sev-badge.sev-nit { color:var(--nit); background:rgba(255,255,255,.06); }
|
|
437
|
+
@keyframes pulse { 0%,100% { opacity:1; transform:scale(1); } 50% { opacity:.4; transform:scale(.8); } }
|
|
438
|
+
@keyframes spin { to { transform:rotate(360deg); } }
|
|
439
|
+
@media (max-width:980px) { .kpis { grid-template-columns:repeat(2, minmax(0,1fr)); } }
|
|
440
|
+
@media (max-width:820px) {
|
|
441
|
+
.sidebar { width:232px; flex-basis:232px; }
|
|
442
|
+
.topbar { padding:12px 16px; }
|
|
443
|
+
.content { padding:18px 16px 56px; }
|
|
444
|
+
.auto-refresh { display:none; }
|
|
445
|
+
}
|
|
357
446
|
`;
|
|
358
447
|
|
|
359
448
|
/** Id de build por processo — muda a URL do bundle a cada reinício do servidor. */
|
|
@@ -365,7 +454,12 @@ function renderShell(): string {
|
|
|
365
454
|
<head>
|
|
366
455
|
<meta charset="utf-8" />
|
|
367
456
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
457
|
+
<meta name="color-scheme" content="dark" />
|
|
368
458
|
<title>Houndsman — Revisões</title>
|
|
459
|
+
<!-- Fontes de CDN: se estiver offline, o CSS cai para a pilha de sistema. -->
|
|
460
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
461
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
462
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" />
|
|
369
463
|
<style>${STYLES}</style>
|
|
370
464
|
</head>
|
|
371
465
|
<body>
|