feat(awooop): link telegram alerts to incident runs
Some checks failed
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Failing after 1m58s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
Your Name
2026-05-17 22:17:21 +08:00
parent 3aabceb234
commit 6868a9a93d
8 changed files with 163 additions and 15 deletions

View File

@@ -116,6 +116,7 @@ interface RecentEventsResponse {
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "";
const PER_PAGE = 50;
const AUTO_REFRESH_INTERVAL = 30_000; // 30 秒
const INCIDENT_ID_FILTER_RE = /^INC-\d{8}-[A-Z0-9]{4,}$/;
const STATE_CONFIG: Record<
RunState,
@@ -518,10 +519,20 @@ export default function RunsPage() {
const [projectFilter, setProjectFilter] = useState<string>("");
const [statusFilter, setStatusFilter] = useState<string>("");
const [evidenceFilter, setEvidenceFilter] = useState<"" | RemediationStatus>("");
const [incidentFilter, setIncidentFilter] = useState<string>("");
const [page, setPage] = useState(1);
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const linkedProject = params.get("project_id") || "";
const linkedIncident = params.get("incident_id") || "";
if (linkedProject) setProjectFilter(linkedProject);
if (linkedIncident) setIncidentFilter(linkedIncident);
if (linkedProject || linkedIncident) setPage(1);
}, []);
// 取得租戶清單
useEffect(() => {
fetch(`${API_BASE}/api/v1/platform/tenants`)
@@ -540,6 +551,10 @@ export default function RunsPage() {
if (projectFilter) params.set("project_id", projectFilter);
if (statusFilter) params.set("state", statusFilter);
if (evidenceFilter) params.set("remediation_status", evidenceFilter);
const normalizedIncidentFilter = incidentFilter.trim().toUpperCase();
if (INCIDENT_ID_FILTER_RE.test(normalizedIncidentFilter)) {
params.set("incident_id", normalizedIncidentFilter);
}
params.set("page", String(page));
params.set("per_page", String(PER_PAGE));
@@ -572,7 +587,7 @@ export default function RunsPage() {
} finally {
setLoading(false);
}
}, [projectFilter, statusFilter, evidenceFilter, page]);
}, [projectFilter, statusFilter, evidenceFilter, incidentFilter, page]);
// 初次載入
useEffect(() => {
@@ -791,6 +806,18 @@ export default function RunsPage() {
</select>
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" aria-hidden="true" />
</div>
{/* Incident Filter */}
<input
value={incidentFilter}
onChange={(e) => {
setIncidentFilter(e.target.value.trim().toUpperCase());
setPage(1);
}}
className="min-w-[260px] rounded-lg border border-border bg-background px-3 py-1.5 font-mono text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-brand-accent/50"
aria-label={tEvidence("filters.incidentLabel")}
placeholder={tEvidence("filters.incidentPlaceholder")}
/>
</div>
{/* Error State */}