@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
|
@@ -1,18 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Registry de watchdogs (biblioteca) — acesso tipado ao `HoundsmanConfig`.
|
|
3
3
|
*
|
|
4
|
-
* O motor não conhece ids concretos
|
|
4
|
+
* O motor não conhece ids concretos: o registry resolve definições, contexto e
|
|
5
|
+
* casamento de arquivos por domínio. As funções antigas continuam como
|
|
6
|
+
* adaptadores para a API pública.
|
|
5
7
|
*/
|
|
6
8
|
|
|
7
9
|
import type { HoundsmanConfig } from "./config";
|
|
10
|
+
import { Watchdog, type WatchdogContext } from "./domain/watchdog";
|
|
8
11
|
import type { WatchdogDefinition } from "./types";
|
|
9
12
|
|
|
13
|
+
/** Coleção tipada de watchdogs do projeto. */
|
|
14
|
+
export class WatchdogRegistry<TId extends string = string> {
|
|
15
|
+
private readonly config: HoundsmanConfig<TId>;
|
|
16
|
+
|
|
17
|
+
constructor(config: HoundsmanConfig<TId>) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Ids na ordem de declaração. */
|
|
22
|
+
get ids(): readonly TId[] {
|
|
23
|
+
return this.config.watchdogIds;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Definição de um watchdog (id string em runtime). */
|
|
27
|
+
get(watchdogId: string): WatchdogDefinition {
|
|
28
|
+
return this.config.watchdogs[watchdogId as TId];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Busca null-safe por id. */
|
|
32
|
+
find(watchdogId: string): Watchdog | null {
|
|
33
|
+
const definition = this.config.watchdogs[watchdogId as TId];
|
|
34
|
+
return definition ? new Watchdog(definition) : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Nome legível (default: o próprio id). */
|
|
38
|
+
nameOf(watchdogId: string): string {
|
|
39
|
+
return this.find(watchdogId)?.name ?? watchdogId;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Contexto essencial (sem regras). */
|
|
43
|
+
context(watchdogId: string): WatchdogContext {
|
|
44
|
+
return new Watchdog(this.get(watchdogId)).context();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Ids dos watchdogs que cobrem o arquivo. */
|
|
48
|
+
matching(filePath: string, content?: string): TId[] {
|
|
49
|
+
return this.config.watchdogIds.filter((id) =>
|
|
50
|
+
new Watchdog(this.config.watchdogs[id]).matches(filePath, content),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Todos os watchdogs (na ordem de declaração). */
|
|
55
|
+
all(): Watchdog[] {
|
|
56
|
+
return this.config.watchdogIds.map(
|
|
57
|
+
(id) => new Watchdog(this.config.watchdogs[id]),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
10
62
|
/** Acesso tipado a um watchdog do projeto (id string em runtime). */
|
|
11
63
|
export function getWatchdog<TId extends string>(
|
|
12
64
|
config: HoundsmanConfig<TId>,
|
|
13
65
|
watchdogId: string,
|
|
14
66
|
): WatchdogDefinition {
|
|
15
|
-
return config.
|
|
67
|
+
return new WatchdogRegistry(config).get(watchdogId);
|
|
16
68
|
}
|
|
17
69
|
|
|
18
70
|
/** Busca null-safe por id string (runtime). */
|
|
@@ -20,23 +72,13 @@ export function findWatchdog<TId extends string>(
|
|
|
20
72
|
config: HoundsmanConfig<TId>,
|
|
21
73
|
watchdogId: string,
|
|
22
74
|
): WatchdogDefinition | null {
|
|
23
|
-
return config.
|
|
75
|
+
return new WatchdogRegistry(config).find(watchdogId)?.definition ?? null;
|
|
24
76
|
}
|
|
25
77
|
|
|
26
78
|
/** Contexto "essencial" do watchdog — SEM regras (docs/UI/inspeção). */
|
|
27
79
|
export function getWatchdogContext<TId extends string>(
|
|
28
80
|
config: HoundsmanConfig<TId>,
|
|
29
81
|
watchdogId: string,
|
|
30
|
-
) {
|
|
31
|
-
|
|
32
|
-
getWatchdog(config, watchdogId);
|
|
33
|
-
return {
|
|
34
|
-
id,
|
|
35
|
-
name,
|
|
36
|
-
description,
|
|
37
|
-
scope,
|
|
38
|
-
reviewFocus,
|
|
39
|
-
exclude,
|
|
40
|
-
contextFiles,
|
|
41
|
-
};
|
|
82
|
+
): WatchdogContext {
|
|
83
|
+
return new WatchdogRegistry(config).context(watchdogId);
|
|
42
84
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* (`houndsman.config.ts`): watchdogs, tipos e helpers de autoria.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export { ArgParser } from "./core/arg-parser";
|
|
8
9
|
export { assetPath, OUTPUT_FORMAT_FILE } from "./core/assets";
|
|
9
10
|
export { defaultRenderBrief, PROCEDURE, renderBrief } from "./core/brief";
|
|
10
11
|
export type { FindingsCommandResult } from "./core/commands/findings";
|
|
11
12
|
export { runFindingsCommand, runVerifyCommand } from "./core/commands/findings";
|
|
13
|
+
export { ProjectScaffolder } from "./core/commands/scaffold";
|
|
12
14
|
export type {
|
|
13
15
|
HoundsmanBudgets,
|
|
14
16
|
HoundsmanConcurrency,
|
|
@@ -36,6 +38,14 @@ export {
|
|
|
36
38
|
parseDiffIndex,
|
|
37
39
|
parseNumstat,
|
|
38
40
|
} from "./core/diff-source";
|
|
41
|
+
export { DocDiscoverer } from "./core/doc-discoverer";
|
|
42
|
+
export { Classification } from "./core/domain/classification";
|
|
43
|
+
export { Diff } from "./core/domain/diff";
|
|
44
|
+
export { Watchdog } from "./core/domain/watchdog";
|
|
45
|
+
export { buildCarryover, mergeCarryover } from "./core/engine/carryover";
|
|
46
|
+
export { HoundsmanEngine } from "./core/engine/houndsman-engine";
|
|
47
|
+
export { ReviewScopeResolver } from "./core/engine/review-scope";
|
|
48
|
+
export { ReviewSession } from "./core/engine/review-session";
|
|
39
49
|
export {
|
|
40
50
|
churnOf,
|
|
41
51
|
classifyEntries,
|
|
@@ -81,6 +91,9 @@ export {
|
|
|
81
91
|
FINDING_DECISIONS,
|
|
82
92
|
FINDING_EXEC_STATES,
|
|
83
93
|
FINDINGS_STATE_VERSION,
|
|
94
|
+
Finding,
|
|
95
|
+
FindingsRepository,
|
|
96
|
+
FindingsState,
|
|
84
97
|
FindingsStateStore,
|
|
85
98
|
fileReader,
|
|
86
99
|
filterFindings,
|
|
@@ -91,6 +104,7 @@ export {
|
|
|
91
104
|
summarize,
|
|
92
105
|
syncState,
|
|
93
106
|
} from "./core/findings-state";
|
|
107
|
+
export type { GitResult, GitRunner } from "./core/git-runner";
|
|
94
108
|
export {
|
|
95
109
|
ARTIFACT_DIR,
|
|
96
110
|
CORE_DIR,
|
|
@@ -100,9 +114,23 @@ export {
|
|
|
100
114
|
relFromRoot,
|
|
101
115
|
STATE_FILE,
|
|
102
116
|
} from "./core/paths";
|
|
117
|
+
export { ProjectConfig } from "./core/project-config";
|
|
103
118
|
export { checkRegistry } from "./core/registry-check";
|
|
119
|
+
export { BriefRenderer } from "./core/renderers/brief-renderer";
|
|
120
|
+
export { ManifestWriter } from "./core/renderers/manifest-writer";
|
|
121
|
+
export { ReportRenderer } from "./core/renderers/report-renderer";
|
|
104
122
|
export type { DomainReport } from "./core/report-builder";
|
|
105
123
|
export { renderDomainReport } from "./core/report-builder";
|
|
124
|
+
export { RunLayout } from "./core/run-layout";
|
|
125
|
+
export type { Clock } from "./core/services/clock";
|
|
126
|
+
export { systemClock } from "./core/services/clock";
|
|
127
|
+
export { FileClassifier } from "./core/services/file-classifier";
|
|
128
|
+
export { FixBatchBuilder } from "./core/services/fix-batch-builder";
|
|
129
|
+
export type { GhResult, GhRunner } from "./core/services/gh-client";
|
|
130
|
+
export { GhClient } from "./core/services/gh-client";
|
|
131
|
+
export { GitRepository } from "./core/services/git-repository";
|
|
132
|
+
export { ProjectFiles } from "./core/services/project-files";
|
|
133
|
+
export { Verifier } from "./core/services/verifier";
|
|
106
134
|
export type {
|
|
107
135
|
ChurnMap,
|
|
108
136
|
ClassifyRule,
|
|
@@ -127,6 +155,8 @@ export {
|
|
|
127
155
|
findWatchdog,
|
|
128
156
|
getWatchdog,
|
|
129
157
|
getWatchdogContext,
|
|
158
|
+
WatchdogRegistry,
|
|
130
159
|
} from "./core/watchdog-registry";
|
|
131
160
|
export type { ServerOptions } from "./server";
|
|
132
|
-
export { createRequestHandler, startServer } from "./server";
|
|
161
|
+
export { createRequestHandler, HoundsmanServer, startServer } from "./server";
|
|
162
|
+
export { RunRepository } from "./server/run-repository";
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* App: layout do servidor — sidebar de
|
|
3
|
-
*
|
|
2
|
+
* App: layout do servidor — sidebar de execuções, header fixo (abas segmentadas,
|
|
3
|
+
* auto-refresh e reancoragem), KPIs, filtros e os painéis de findings/artefatos.
|
|
4
|
+
* Concentra o estado e as chamadas à API.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
7
8
|
import { ArtifactsPanel } from "./ArtifactsPanel";
|
|
9
|
+
import { AutoRefresh } from "./AutoRefresh";
|
|
8
10
|
import { api, computeSummary } from "./api";
|
|
9
|
-
import { TAB_CAPTIONS } from "./constants";
|
|
11
|
+
import { AUTO_REFRESH_MS, TAB_CAPTIONS } from "./constants";
|
|
10
12
|
import { Filters, type FiltersState } from "./Filters";
|
|
11
13
|
import { FindingsList } from "./FindingsList";
|
|
14
|
+
import { Layers, ListIcon, Refresh, Send } from "./icons";
|
|
12
15
|
import { RunList } from "./RunList";
|
|
16
|
+
import { SubmitModal } from "./SubmitModal";
|
|
13
17
|
import { SummaryBar } from "./SummaryBar";
|
|
14
18
|
import type { Finding, FindingsResponse, RunSummary } from "./types";
|
|
15
19
|
|
|
16
20
|
type Tab = "findings" | "artifacts";
|
|
17
21
|
|
|
22
|
+
const TABS: { id: Tab; label: string; hint: string }[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "findings",
|
|
25
|
+
label: "Findings",
|
|
26
|
+
hint: "Achados da revisão: triagem e decisões por finding",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "artifacts",
|
|
30
|
+
label: "Artefatos",
|
|
31
|
+
hint: "Arquivos brutos gerados pela revisão (manifesto, classificação, relatório…)",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
18
35
|
export function App() {
|
|
19
36
|
const [runs, setRuns] = useState<RunSummary[]>([]);
|
|
20
37
|
const [slug, setSlug] = useState<string | null>(null);
|
|
@@ -27,6 +44,23 @@ export function App() {
|
|
|
27
44
|
showAbsent: false,
|
|
28
45
|
});
|
|
29
46
|
const [error, setError] = useState("");
|
|
47
|
+
const [lastSync, setLastSync] = useState(() => Date.now());
|
|
48
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
49
|
+
const [syncing, setSyncing] = useState(false);
|
|
50
|
+
const [submitOpen, setSubmitOpen] = useState(false);
|
|
51
|
+
/** Refs espelhando estado volátil — o polling não deve recriar o intervalo. */
|
|
52
|
+
const lastSyncRef = useRef(lastSync);
|
|
53
|
+
const slugRef = useRef<string | null>(null);
|
|
54
|
+
const refreshingRef = useRef(false);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
slugRef.current = slug;
|
|
58
|
+
}, [slug]);
|
|
59
|
+
|
|
60
|
+
const markSynced = useCallback(() => {
|
|
61
|
+
lastSyncRef.current = Date.now();
|
|
62
|
+
setLastSync(lastSyncRef.current);
|
|
63
|
+
}, []);
|
|
30
64
|
|
|
31
65
|
const reloadRuns = useCallback(async () => {
|
|
32
66
|
try {
|
|
@@ -47,6 +81,31 @@ export function App() {
|
|
|
47
81
|
}
|
|
48
82
|
}, []);
|
|
49
83
|
|
|
84
|
+
/** Revalidação leve (auto-refresh): runs + findings da run selecionada. */
|
|
85
|
+
const refresh = useCallback(async () => {
|
|
86
|
+
if (refreshingRef.current) return;
|
|
87
|
+
refreshingRef.current = true;
|
|
88
|
+
setRefreshing(true);
|
|
89
|
+
try {
|
|
90
|
+
const { runs: list } = await api<{ runs: RunSummary[] }>("/api/runs");
|
|
91
|
+
setRuns(list);
|
|
92
|
+
const target = slugRef.current ?? list[0]?.slug ?? null;
|
|
93
|
+
if (target) {
|
|
94
|
+
setSlug((current) => current ?? target);
|
|
95
|
+
setData(
|
|
96
|
+
await api<FindingsResponse>(`/api/runs/${target}/findings?all=1`),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
setError("");
|
|
100
|
+
} catch (err) {
|
|
101
|
+
setError(String(err));
|
|
102
|
+
} finally {
|
|
103
|
+
refreshingRef.current = false;
|
|
104
|
+
setRefreshing(false);
|
|
105
|
+
markSynced();
|
|
106
|
+
}
|
|
107
|
+
}, [markSynced]);
|
|
108
|
+
|
|
50
109
|
useEffect(() => {
|
|
51
110
|
reloadRuns();
|
|
52
111
|
}, [reloadRuns]);
|
|
@@ -55,6 +114,16 @@ export function App() {
|
|
|
55
114
|
if (slug) loadFindings(slug);
|
|
56
115
|
}, [slug, loadFindings]);
|
|
57
116
|
|
|
117
|
+
// Auto-refresh: só o badge mostra o tempo decorrido; a lista só refaz o fetch
|
|
118
|
+
// quando o intervalo estourou (e a aba está visível).
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const id = setInterval(() => {
|
|
121
|
+
if (document.hidden || refreshingRef.current) return;
|
|
122
|
+
if (Date.now() - lastSyncRef.current >= AUTO_REFRESH_MS) void refresh();
|
|
123
|
+
}, 1000);
|
|
124
|
+
return () => clearInterval(id);
|
|
125
|
+
}, [refresh]);
|
|
126
|
+
|
|
58
127
|
const onUpdated = useCallback(
|
|
59
128
|
(updated: Finding) => {
|
|
60
129
|
setData((current) =>
|
|
@@ -74,10 +143,22 @@ export function App() {
|
|
|
74
143
|
|
|
75
144
|
const sync = useCallback(async () => {
|
|
76
145
|
if (!slug) return;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
setSyncing(true);
|
|
147
|
+
try {
|
|
148
|
+
await api(`/api/runs/${slug}/sync`, { method: "POST" });
|
|
149
|
+
await loadFindings(slug);
|
|
150
|
+
await reloadRuns();
|
|
151
|
+
markSynced();
|
|
152
|
+
} finally {
|
|
153
|
+
setSyncing(false);
|
|
154
|
+
}
|
|
155
|
+
}, [slug, loadFindings, reloadRuns, markSynced]);
|
|
156
|
+
|
|
157
|
+
/** Após enviar a revisão: a triagem ganhou o vínculo remoto — recarrega. */
|
|
158
|
+
const afterSubmit = useCallback(() => {
|
|
159
|
+
if (slugRef.current) void loadFindings(slugRef.current);
|
|
160
|
+
void reloadRuns();
|
|
161
|
+
}, [loadFindings, reloadRuns]);
|
|
81
162
|
|
|
82
163
|
const findings = useMemo(() => {
|
|
83
164
|
const needle = filters.query.trim().toLowerCase();
|
|
@@ -102,56 +183,100 @@ export function App() {
|
|
|
102
183
|
|
|
103
184
|
const summary = useMemo(() => computeSummary(data?.findings ?? []), [data]);
|
|
104
185
|
|
|
186
|
+
const branch =
|
|
187
|
+
data?.branch ?? runs.find((run) => run.slug === slug)?.branch ?? slug ?? "";
|
|
188
|
+
|
|
105
189
|
return (
|
|
106
190
|
<div class="app">
|
|
107
191
|
<RunList runs={runs} slug={slug} onSelect={setSlug} />
|
|
108
192
|
<main class="main">
|
|
109
193
|
<header class="topbar">
|
|
110
|
-
<div class="
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
194
|
+
<div class="segmented">
|
|
195
|
+
{TABS.map((item) => (
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
key={item.id}
|
|
199
|
+
title={item.hint}
|
|
200
|
+
class={`seg-btn${tab === item.id ? " active" : ""}`}
|
|
201
|
+
onClick={() => setTab(item.id)}
|
|
202
|
+
>
|
|
203
|
+
{item.id === "findings" ? (
|
|
204
|
+
<ListIcon size={15} />
|
|
205
|
+
) : (
|
|
206
|
+
<Layers size={15} />
|
|
207
|
+
)}
|
|
208
|
+
{item.label}
|
|
209
|
+
</button>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
<div class="topbar-right">
|
|
213
|
+
<AutoRefresh
|
|
214
|
+
lastSync={lastSync}
|
|
215
|
+
refreshing={refreshing || syncing}
|
|
216
|
+
/>
|
|
217
|
+
{slug && (
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
title="Reancorar: recalcula a linha de cada finding lendo o arquivo atual — use após um auto-fix que deslocou as linhas"
|
|
221
|
+
class="btn sync"
|
|
222
|
+
disabled={syncing}
|
|
223
|
+
onClick={sync}
|
|
224
|
+
>
|
|
225
|
+
<Refresh
|
|
226
|
+
size={15}
|
|
227
|
+
class={`icon${syncing ? " spin" : ""}`}
|
|
228
|
+
/>
|
|
229
|
+
{syncing ? "Reancorando…" : "Reancorar"}
|
|
230
|
+
</button>
|
|
231
|
+
)}
|
|
127
232
|
</div>
|
|
128
|
-
{slug && (
|
|
129
|
-
<button
|
|
130
|
-
type="button"
|
|
131
|
-
title="Reancorar: recalcula a linha de cada finding lendo o arquivo atual — use após um auto-fix que deslocou as linhas"
|
|
132
|
-
class="btn sync"
|
|
133
|
-
onClick={sync}
|
|
134
|
-
>
|
|
135
|
-
↻ Reancorar (sync)
|
|
136
|
-
</button>
|
|
137
|
-
)}
|
|
138
233
|
</header>
|
|
139
234
|
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
235
|
+
<div class="content">
|
|
236
|
+
<div class="page-header">
|
|
237
|
+
<div class="page-header-text">
|
|
238
|
+
{slug && <h1 class="page-title">{branch}</h1>}
|
|
239
|
+
<p class="page-sub">{TAB_CAPTIONS[tab]}</p>
|
|
240
|
+
</div>
|
|
241
|
+
{slug && tab === "findings" && data && (
|
|
242
|
+
<div class="page-header-actions">
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
class="btn submit-review"
|
|
246
|
+
title="Publicar os findings selecionados como comentários na PR (via gh)"
|
|
247
|
+
onClick={() => setSubmitOpen(true)}
|
|
248
|
+
>
|
|
249
|
+
<Send size={15} />
|
|
250
|
+
Enviar revisão
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
{error && <p class="error">{error}</p>}
|
|
256
|
+
|
|
257
|
+
{slug && tab === "findings" && data && (
|
|
258
|
+
<>
|
|
259
|
+
<SummaryBar summary={summary} />
|
|
260
|
+
<Filters value={filters} onChange={setFilters} />
|
|
261
|
+
<FindingsList
|
|
262
|
+
slug={slug}
|
|
263
|
+
findings={findings}
|
|
264
|
+
onUpdated={onUpdated}
|
|
265
|
+
/>
|
|
266
|
+
</>
|
|
267
|
+
)}
|
|
268
|
+
{slug && tab === "artifacts" && <ArtifactsPanel slug={slug} />}
|
|
269
|
+
{!slug && !error && <p class="empty">Nenhuma execução para triar.</p>}
|
|
270
|
+
</div>
|
|
271
|
+
{slug && (
|
|
272
|
+
<SubmitModal
|
|
273
|
+
slug={slug}
|
|
274
|
+
branch={branch}
|
|
275
|
+
open={submitOpen}
|
|
276
|
+
onClose={() => setSubmitOpen(false)}
|
|
277
|
+
onSubmitted={afterSubmit}
|
|
278
|
+
/>
|
|
153
279
|
)}
|
|
154
|
-
{slug && tab === "artifacts" && <ArtifactsPanel slug={slug} />}
|
|
155
280
|
</main>
|
|
156
281
|
</div>
|
|
157
282
|
);
|
|
@@ -25,12 +25,12 @@ export function ArtifactsPanel({ slug }: { slug: string }) {
|
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
27
|
<div class="artifacts">
|
|
28
|
-
<div class="tabs">
|
|
28
|
+
<div class="artifact-tabs">
|
|
29
29
|
{ARTIFACTS.map((artifact) => (
|
|
30
30
|
<button
|
|
31
31
|
type="button"
|
|
32
32
|
title={ARTIFACT_INFO[artifact]}
|
|
33
|
-
class={`
|
|
33
|
+
class={`chip${artifact === name ? " active" : ""}`}
|
|
34
34
|
onClick={() => setName(artifact)}
|
|
35
35
|
>
|
|
36
36
|
{artifact}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indicador de auto-refresh: ponto de pulso (verde estável / âmbar pulsando
|
|
3
|
+
* durante a atualização) + tempo decorrido desde a última sincronização.
|
|
4
|
+
*
|
|
5
|
+
* O tique de 1s vive aqui — a lista de findings não re-renderiza por causa dele.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useEffect, useState } from "preact/hooks";
|
|
9
|
+
import { formatElapsed } from "./format";
|
|
10
|
+
|
|
11
|
+
export function AutoRefresh({
|
|
12
|
+
lastSync,
|
|
13
|
+
refreshing,
|
|
14
|
+
}: {
|
|
15
|
+
lastSync: number;
|
|
16
|
+
refreshing: boolean;
|
|
17
|
+
}) {
|
|
18
|
+
const [now, setNow] = useState(() => Date.now());
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
22
|
+
return () => clearInterval(id);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const seconds = Math.max(0, Math.floor((now - lastSync) / 1000));
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<span
|
|
29
|
+
class="auto-refresh"
|
|
30
|
+
title="A lista é revalidada automaticamente enquanto esta página estiver aberta"
|
|
31
|
+
>
|
|
32
|
+
<span class={`dot${refreshing ? " busy" : ""}`} />
|
|
33
|
+
{refreshing
|
|
34
|
+
? "Auto-refresh · atualizando…"
|
|
35
|
+
: `Auto-refresh · ${formatElapsed(seconds)}`}
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Barra de filtros dos findings: busca,
|
|
2
|
+
* Barra de filtros dos findings: busca, chips de severidade, chips de decisão e
|
|
3
|
+
* o toggle "ausentes".
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
|
-
import { DECISION_LABELS, DECISIONS, SEVERITY_ORDER } from "./constants";
|
|
6
|
+
import { DECISION_LABELS, DECISIONS, SEVERITY_LABELS, SEVERITY_ORDER } from "./constants";
|
|
7
|
+
import { Search, Watch } from "./icons";
|
|
6
8
|
import type { Decision, Severity } from "./types";
|
|
7
9
|
|
|
8
10
|
export type FiltersState = {
|
|
@@ -23,61 +25,78 @@ export function Filters({
|
|
|
23
25
|
|
|
24
26
|
return (
|
|
25
27
|
<div class="filters">
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
placeholder="Buscar arquivo, regra ou mensagem…"
|
|
29
|
-
value={value.query}
|
|
30
|
-
onInput={(event) =>
|
|
31
|
-
patch({ query: (event.target as HTMLInputElement).value })
|
|
32
|
-
}
|
|
33
|
-
/>
|
|
34
|
-
<select
|
|
35
|
-
title="Filtrar por decisão de triagem"
|
|
36
|
-
value={value.decision}
|
|
37
|
-
onChange={(event) =>
|
|
38
|
-
patch({
|
|
39
|
-
decision: (event.target as HTMLSelectElement).value as
|
|
40
|
-
| "all"
|
|
41
|
-
| Decision,
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
>
|
|
45
|
-
<option value="all">Todas as decisões</option>
|
|
46
|
-
{DECISIONS.map((decision) => (
|
|
47
|
-
<option value={decision}>{DECISION_LABELS[decision]}</option>
|
|
48
|
-
))}
|
|
49
|
-
</select>
|
|
50
|
-
<select
|
|
51
|
-
title="Filtrar por severidade"
|
|
52
|
-
value={value.severity}
|
|
53
|
-
onChange={(event) =>
|
|
54
|
-
patch({
|
|
55
|
-
severity: (event.target as HTMLSelectElement).value as
|
|
56
|
-
| "all"
|
|
57
|
-
| Severity,
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
>
|
|
61
|
-
<option value="all">Todas as severidades</option>
|
|
62
|
-
{SEVERITY_ORDER.map((severity) => (
|
|
63
|
-
<option value={severity}>{severity}</option>
|
|
64
|
-
))}
|
|
65
|
-
</select>
|
|
66
|
-
<label
|
|
67
|
-
class="toggle"
|
|
68
|
-
title="Incluir findings que sumiram do último relatório"
|
|
69
|
-
>
|
|
28
|
+
<label class="search" title="Filtrar por arquivo, regra ou mensagem">
|
|
29
|
+
<Search size={15} />
|
|
70
30
|
<input
|
|
71
|
-
type="
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
})
|
|
31
|
+
type="text"
|
|
32
|
+
placeholder="Buscar arquivo, regra ou mensagem…"
|
|
33
|
+
value={value.query}
|
|
34
|
+
onInput={(event) =>
|
|
35
|
+
patch({ query: (event.target as HTMLInputElement).value })
|
|
77
36
|
}
|
|
78
37
|
/>
|
|
79
|
-
|
|
38
|
+
{value.query && (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
class="search-clear"
|
|
42
|
+
title="Limpar busca"
|
|
43
|
+
onClick={() => patch({ query: "" })}
|
|
44
|
+
>
|
|
45
|
+
×
|
|
46
|
+
</button>
|
|
47
|
+
)}
|
|
80
48
|
</label>
|
|
49
|
+
|
|
50
|
+
<div class="chips">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
title="Todas as severidades"
|
|
54
|
+
class={`chip${value.severity === "all" ? " active" : ""}`}
|
|
55
|
+
onClick={() => patch({ severity: "all" })}
|
|
56
|
+
>
|
|
57
|
+
Todas
|
|
58
|
+
</button>
|
|
59
|
+
{SEVERITY_ORDER.map((severity) => (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
title={`Somente severidade ${severity}`}
|
|
63
|
+
class={`chip sev-${severity}${value.severity === severity ? " active" : ""}`}
|
|
64
|
+
onClick={() => patch({ severity })}
|
|
65
|
+
>
|
|
66
|
+
{SEVERITY_LABELS[severity]}
|
|
67
|
+
</button>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="chips">
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
title="Todas as decisões"
|
|
75
|
+
class={`chip${value.decision === "all" ? " active" : ""}`}
|
|
76
|
+
onClick={() => patch({ decision: "all" })}
|
|
77
|
+
>
|
|
78
|
+
Todas
|
|
79
|
+
</button>
|
|
80
|
+
{DECISIONS.map((decision) => (
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
title={`Somente ${DECISION_LABELS[decision]}`}
|
|
84
|
+
class={`chip dec-${decision}${value.decision === decision ? " active" : ""}`}
|
|
85
|
+
onClick={() => patch({ decision })}
|
|
86
|
+
>
|
|
87
|
+
{DECISION_LABELS[decision]}
|
|
88
|
+
</button>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
title="Incluir findings que sumiram do último relatório"
|
|
95
|
+
class={`chip${value.showAbsent ? " active" : ""}`}
|
|
96
|
+
onClick={() => patch({ showAbsent: !value.showAbsent })}
|
|
97
|
+
>
|
|
98
|
+
<Watch size={13} /> Ausentes
|
|
99
|
+
</button>
|
|
81
100
|
</div>
|
|
82
101
|
);
|
|
83
102
|
}
|