feat(graylog): add project UI and source-aware ticket views
This commit is contained in:
parent
d561578e78
commit
f2be837a1e
9 changed files with 587 additions and 6 deletions
|
|
@ -79,8 +79,7 @@ impl Worktree {
|
|||
w.created_at, w.merged_at, w.merged_into \
|
||||
FROM worktrees w \
|
||||
JOIN processed_tickets pt ON w.ticket_id = pt.id \
|
||||
JOIN watched_trackers wt ON pt.tracker_id = wt.id \
|
||||
WHERE wt.project_id = ?1 \
|
||||
WHERE pt.project_id = ?1 \
|
||||
ORDER BY w.created_at DESC";
|
||||
let mut stmt = conn.prepare(sql)?;
|
||||
let rows = stmt.query_map(params![project_id], from_row)?;
|
||||
|
|
@ -199,6 +198,30 @@ mod tests {
|
|||
assert_eq!(worktrees.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_by_project_includes_external_source_worktrees() {
|
||||
let conn = db::init_in_memory().expect("db init");
|
||||
let project = Project::insert(&conn, "P1", "/path", None, "main").unwrap();
|
||||
|
||||
let ticket = ProcessedTicket::insert_external(
|
||||
&conn,
|
||||
&project.id,
|
||||
"graylog",
|
||||
Some("subject-1"),
|
||||
-101,
|
||||
"Graylog subject",
|
||||
"{}",
|
||||
)
|
||||
.expect("external ticket insert should succeed");
|
||||
|
||||
Worktree::insert(&conn, &ticket.id, "/wt-graylog", "orchai/graylog-101")
|
||||
.expect("worktree insert should succeed");
|
||||
|
||||
let worktrees = Worktree::list_by_project(&conn, &project.id).unwrap();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
assert_eq!(worktrees[0].ticket_id, ticket.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_merged() {
|
||||
let (conn, ticket_id) = setup();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
|||
import AppLayout from "./components/layout/AppLayout";
|
||||
import ProjectForm from "./components/projects/ProjectForm";
|
||||
import ProjectDashboard from "./components/projects/ProjectDashboard";
|
||||
import ProjectGraylog from "./components/projects/ProjectGraylog";
|
||||
import ProjectLiveAgent from "./components/projects/ProjectLiveAgent";
|
||||
import ProjectModules from "./components/projects/ProjectModules";
|
||||
import ProjectTasks from "./components/projects/ProjectTasks";
|
||||
|
|
@ -28,6 +29,7 @@ function App() {
|
|||
<Route index element={<EmptyState />} />
|
||||
<Route path="/projects/new" element={<ProjectForm />} />
|
||||
<Route path="/projects/:projectId" element={<ProjectDashboard />} />
|
||||
<Route path="/projects/:projectId/graylog" element={<ProjectGraylog />} />
|
||||
<Route path="/projects/:projectId/modules" element={<ProjectModules />} />
|
||||
<Route path="/projects/:projectId/live-agent" element={<ProjectLiveAgent />} />
|
||||
<Route path="/projects/:projectId/tasks" element={<ProjectTasks />} />
|
||||
|
|
|
|||
|
|
@ -44,6 +44,18 @@ interface TicketProcessingPayload {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
interface GraylogPollingPayload {
|
||||
project_id: string;
|
||||
triggered_count?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface GraylogSubjectTriggeredPayload {
|
||||
project_id: string;
|
||||
subject_id: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export default function ProjectDashboard() {
|
||||
const { projectId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -103,7 +115,20 @@ export default function ProjectDashboard() {
|
|||
|
||||
async function setup() {
|
||||
try {
|
||||
const [unlistenPollingStarted, unlistenPollingFinished, unlistenPollingError, unlistenTicketsDetected, unlistenTicketStarted, unlistenTicketDone, unlistenTicketError] =
|
||||
const graylogPollKey = "__graylog__";
|
||||
const [
|
||||
unlistenPollingStarted,
|
||||
unlistenPollingFinished,
|
||||
unlistenPollingError,
|
||||
unlistenTicketsDetected,
|
||||
unlistenTicketStarted,
|
||||
unlistenTicketDone,
|
||||
unlistenTicketError,
|
||||
unlistenGraylogPollingStarted,
|
||||
unlistenGraylogSubjectTriggered,
|
||||
unlistenGraylogPollingFinished,
|
||||
unlistenGraylogPollingError,
|
||||
] =
|
||||
await Promise.all([
|
||||
listen<PollingPayload>("polling-started", (event) => {
|
||||
const payload = event.payload;
|
||||
|
|
@ -202,6 +227,54 @@ export default function ProjectDashboard() {
|
|||
);
|
||||
void loadData();
|
||||
}),
|
||||
listen<GraylogPollingPayload>("graylog-polling-started", (event) => {
|
||||
const payload = event.payload;
|
||||
if (payload.project_id !== projectId) return;
|
||||
|
||||
setActivePolls((prev) => ({
|
||||
...prev,
|
||||
[graylogPollKey]: "Graylog",
|
||||
}));
|
||||
appendActivity("info", "Polling Graylog lancé.");
|
||||
}),
|
||||
listen<GraylogSubjectTriggeredPayload>("graylog-subject-triggered", (event) => {
|
||||
const payload = event.payload;
|
||||
if (payload.project_id !== projectId) return;
|
||||
|
||||
appendActivity(
|
||||
"success",
|
||||
`Sujet Graylog déclenché (score ${payload.score}).`
|
||||
);
|
||||
void loadData();
|
||||
}),
|
||||
listen<GraylogPollingPayload>("graylog-polling-finished", (event) => {
|
||||
const payload = event.payload;
|
||||
if (payload.project_id !== projectId) return;
|
||||
|
||||
setActivePolls((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[graylogPollKey];
|
||||
return next;
|
||||
});
|
||||
appendActivity(
|
||||
"success",
|
||||
`Polling Graylog terminé (${payload.triggered_count ?? 0} sujet(s) déclenché(s)).`
|
||||
);
|
||||
}),
|
||||
listen<GraylogPollingPayload>("graylog-polling-error", (event) => {
|
||||
const payload = event.payload;
|
||||
if (payload.project_id !== projectId) return;
|
||||
|
||||
setActivePolls((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[graylogPollKey];
|
||||
return next;
|
||||
});
|
||||
appendActivity(
|
||||
"error",
|
||||
`Erreur Graylog: ${payload.error ?? "erreur inconnue"}.`
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
if (cancelled) {
|
||||
|
|
@ -212,6 +285,10 @@ export default function ProjectDashboard() {
|
|||
unlistenTicketStarted();
|
||||
unlistenTicketDone();
|
||||
unlistenTicketError();
|
||||
unlistenGraylogPollingStarted();
|
||||
unlistenGraylogSubjectTriggered();
|
||||
unlistenGraylogPollingFinished();
|
||||
unlistenGraylogPollingError();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -223,6 +300,10 @@ export default function ProjectDashboard() {
|
|||
unlistenTicketStarted,
|
||||
unlistenTicketDone,
|
||||
unlistenTicketError,
|
||||
unlistenGraylogPollingStarted,
|
||||
unlistenGraylogSubjectTriggered,
|
||||
unlistenGraylogPollingFinished,
|
||||
unlistenGraylogPollingError,
|
||||
];
|
||||
} catch (err) {
|
||||
appendActivity(
|
||||
|
|
@ -240,7 +321,7 @@ export default function ProjectDashboard() {
|
|||
unlisten();
|
||||
}
|
||||
};
|
||||
}, [projectId]);
|
||||
}, [projectId, loadData]);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!projectId) return;
|
||||
|
|
@ -365,7 +446,7 @@ export default function ProjectDashboard() {
|
|||
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-semibold mb-4">Orchestrateur IA</h3>
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<Link
|
||||
to={`/projects/${project.id}/modules`}
|
||||
className="rounded-lg border border-gray-200 bg-white p-4 hover:border-gray-300"
|
||||
|
|
@ -393,6 +474,15 @@ export default function ProjectDashboard() {
|
|||
Crée une file de tâches traitées par des agents pré-définis.
|
||||
</div>
|
||||
</Link>
|
||||
<Link
|
||||
to={`/projects/${project.id}/graylog`}
|
||||
className="rounded-lg border border-gray-200 bg-white p-4 hover:border-gray-300"
|
||||
>
|
||||
<div className="text-sm font-semibold text-gray-900">Graylog</div>
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
Configure le polling Graylog et surveille les sujets scorés.
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
339
src/components/projects/ProjectGraylog.tsx
Normal file
339
src/components/projects/ProjectGraylog.tsx
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import {
|
||||
getGraylogCredentials,
|
||||
listAgents,
|
||||
listGraylogDetections,
|
||||
listGraylogSubjects,
|
||||
manualGraylogPoll,
|
||||
setGraylogCredentials,
|
||||
testGraylogConnection,
|
||||
} from "../../lib/api";
|
||||
import { getErrorMessage } from "../../lib/errors";
|
||||
import type {
|
||||
Agent,
|
||||
GraylogCredentialsSafe,
|
||||
GraylogDetection,
|
||||
GraylogSubject,
|
||||
} from "../../lib/types";
|
||||
|
||||
export default function ProjectGraylog() {
|
||||
const { projectId } = useParams<{ projectId: string }>();
|
||||
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [credentials, setCredentials] = useState<GraylogCredentialsSafe | null>(null);
|
||||
const [subjects, setSubjects] = useState<GraylogSubject[]>([]);
|
||||
const [detections, setDetections] = useState<GraylogDetection[]>([]);
|
||||
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [apiToken, setApiToken] = useState("");
|
||||
const [analystAgentId, setAnalystAgentId] = useState("");
|
||||
const [developerAgentId, setDeveloperAgentId] = useState("");
|
||||
const [streamId, setStreamId] = useState("");
|
||||
const [queryFilter, setQueryFilter] = useState("level:(critical OR error OR warning)");
|
||||
const [pollingIntervalMinutes, setPollingIntervalMinutes] = useState(10);
|
||||
const [lookbackMinutes, setLookbackMinutes] = useState(30);
|
||||
const [scoreThreshold, setScoreThreshold] = useState(70);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const analysts = agents.filter((agent) => agent.role === "analyst");
|
||||
const developers = agents.filter((agent) => agent.role === "developer");
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!projectId) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [agentList, creds, subjectList, detectionList] = await Promise.all([
|
||||
listAgents(),
|
||||
getGraylogCredentials(projectId),
|
||||
listGraylogSubjects(projectId),
|
||||
listGraylogDetections(projectId),
|
||||
]);
|
||||
|
||||
setAgents(agentList);
|
||||
setCredentials(creds);
|
||||
setSubjects(subjectList);
|
||||
setDetections(detectionList);
|
||||
|
||||
if (creds) {
|
||||
setBaseUrl(creds.base_url);
|
||||
setAnalystAgentId(creds.analyst_agent_id);
|
||||
setDeveloperAgentId(creds.developer_agent_id);
|
||||
setStreamId(creds.stream_id ?? "");
|
||||
setQueryFilter(creds.query_filter);
|
||||
setPollingIntervalMinutes(creds.polling_interval_minutes);
|
||||
setLookbackMinutes(creds.lookback_minutes);
|
||||
setScoreThreshold(creds.score_threshold);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
async function handleSave(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (!projectId) return;
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const saved = await setGraylogCredentials(
|
||||
projectId,
|
||||
baseUrl,
|
||||
apiToken,
|
||||
analystAgentId,
|
||||
developerAgentId,
|
||||
streamId.trim() || null,
|
||||
queryFilter,
|
||||
pollingIntervalMinutes,
|
||||
lookbackMinutes,
|
||||
scoreThreshold
|
||||
);
|
||||
|
||||
setCredentials(saved);
|
||||
setApiToken("");
|
||||
setSuccess("Configuration Graylog sauvegardée.");
|
||||
await refresh();
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestConnection() {
|
||||
if (!projectId) return;
|
||||
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const message = await testGraylogConnection(projectId);
|
||||
setSuccess(message);
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleManualPoll() {
|
||||
if (!projectId) return;
|
||||
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const triggeredCount = await manualGraylogPoll(projectId);
|
||||
setSuccess(`Polling Graylog manuel terminé: ${triggeredCount} sujet(s) déclenché(s).`);
|
||||
await refresh();
|
||||
} catch (err: unknown) {
|
||||
setError(getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">Graylog</h2>
|
||||
{projectId && (
|
||||
<Link
|
||||
to={`/projects/${projectId}`}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-700 hover:bg-gray-300"
|
||||
>
|
||||
Retour
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded border border-red-200 bg-red-50 p-3 text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="rounded border border-green-200 bg-green-50 p-3 text-sm text-green-700">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={handleSave}
|
||||
className="space-y-3 rounded-lg border border-gray-200 bg-white p-4"
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-gray-900">Configuration</h3>
|
||||
|
||||
<input
|
||||
className="w-full rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={baseUrl}
|
||||
onChange={(event) => setBaseUrl(event.target.value)}
|
||||
placeholder="https://graylog.example.com"
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
className="w-full rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={apiToken}
|
||||
onChange={(event) => setApiToken(event.target.value)}
|
||||
placeholder={
|
||||
credentials
|
||||
? "Laisser vide pour conserver le token actuel"
|
||||
: "Token API Graylog"
|
||||
}
|
||||
required={!credentials}
|
||||
/>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<select
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={analystAgentId}
|
||||
onChange={(event) => setAnalystAgentId(event.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Analyst agent</option>
|
||||
{analysts.map((agent) => (
|
||||
<option key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<select
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={developerAgentId}
|
||||
onChange={(event) => setDeveloperAgentId(event.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Developer agent</option>
|
||||
{developers.map((agent) => (
|
||||
<option key={agent.id} value={agent.id}>
|
||||
{agent.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<input
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={streamId}
|
||||
onChange={(event) => setStreamId(event.target.value)}
|
||||
placeholder="stream_id (optionnel)"
|
||||
/>
|
||||
<input
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm md:col-span-3"
|
||||
value={queryFilter}
|
||||
onChange={(event) => setQueryFilter(event.target.value)}
|
||||
placeholder="Filtre query Graylog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<input
|
||||
type="number"
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={pollingIntervalMinutes}
|
||||
onChange={(event) => setPollingIntervalMinutes(Number(event.target.value))}
|
||||
min={1}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={lookbackMinutes}
|
||||
onChange={(event) => setLookbackMinutes(Number(event.target.value))}
|
||||
min={1}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="rounded border border-gray-300 px-3 py-2 text-sm"
|
||||
value={scoreThreshold}
|
||||
onChange={(event) => setScoreThreshold(Number(event.target.value))}
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Sauvegarde..." : "Sauvegarder"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleTestConnection()}
|
||||
className="rounded bg-gray-200 px-4 py-2 text-sm text-gray-800 hover:bg-gray-300"
|
||||
>
|
||||
Tester la connexion
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleManualPoll()}
|
||||
className="rounded bg-gray-900 px-4 py-2 text-sm text-white hover:bg-black"
|
||||
>
|
||||
Poll manuel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-900">Sujets détectés</h3>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-sm text-gray-500">Chargement...</div>
|
||||
) : subjects.length === 0 ? (
|
||||
<div className="text-sm text-gray-400">Aucun sujet détecté.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{subjects.map((subject) => (
|
||||
<div key={subject.id} className="rounded border border-gray-100 p-3">
|
||||
<div className="text-sm font-medium text-gray-900">{subject.source}</div>
|
||||
<div className="text-xs text-gray-600">{subject.normalized_message}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
Score: {subject.last_score} | Statut: {subject.status} | Last seen: {" "}
|
||||
{new Date(subject.last_seen_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-900">Dernières détections</h3>
|
||||
|
||||
{detections.length === 0 ? (
|
||||
<div className="text-sm text-gray-400">Aucune détection enregistrée.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{detections.slice(0, 20).map((detection) => (
|
||||
<div
|
||||
key={detection.id}
|
||||
className="rounded border border-gray-100 p-3 text-xs text-gray-700"
|
||||
>
|
||||
score={detection.score} total={detection.total_count} triggered={" "}
|
||||
{String(detection.triggered)} at={" "}
|
||||
{new Date(detection.created_at).toLocaleString()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -72,6 +72,14 @@ export default function ProjectModules() {
|
|||
<div className="text-sm font-semibold text-gray-900">{mod.name}</div>
|
||||
<div className="mt-1 text-xs text-gray-500">{mod.description}</div>
|
||||
<div className="mt-2 font-mono text-[11px] text-gray-400">{mod.module_key}</div>
|
||||
{projectId && mod.module_key === "graylog_polling_auto_resolve" && (
|
||||
<Link
|
||||
to={`/projects/${projectId}/graylog`}
|
||||
className="mt-2 inline-flex rounded bg-gray-100 px-2 py-1 text-xs text-gray-700 hover:bg-gray-200"
|
||||
>
|
||||
Configurer
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<label className="inline-flex items-center gap-2 text-sm text-gray-700">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -300,6 +300,16 @@ export default function TicketDetail() {
|
|||
<span className="text-sm text-gray-500">Status:</span>
|
||||
<span className="ml-2 text-sm">{ticket.status}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Source:</span>
|
||||
<span className="ml-2 text-sm uppercase">{ticket.source}</span>
|
||||
</div>
|
||||
{ticket.source_ref && (
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Source ref:</span>
|
||||
<span className="ml-2 font-mono text-sm">{ticket.source_ref}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-sm text-gray-500">Detected:</span>
|
||||
<span className="ml-2 text-sm">{new Date(ticket.detected_at).toLocaleString()}</span>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ export default function TicketList() {
|
|||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-gray-400">#{ticket.artifact_id}</span>
|
||||
<span className="rounded bg-gray-100 px-2 py-0.5 text-[10px] uppercase tracking-wide text-gray-600">
|
||||
{ticket.source}
|
||||
</span>
|
||||
<span className="truncate text-sm font-medium">{ticket.artifact_title}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-400">
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import type {
|
|||
LiveMessage,
|
||||
LiveAgentExchange,
|
||||
AgentTask,
|
||||
GraylogCredentialsSafe,
|
||||
GraylogSubject,
|
||||
GraylogDetection,
|
||||
} from "./types";
|
||||
|
||||
export async function createProject(
|
||||
|
|
@ -107,6 +110,60 @@ export async function testTuleapConnection(projectId: string | null): Promise<st
|
|||
return invoke("test_tuleap_connection", { projectId });
|
||||
}
|
||||
|
||||
// Graylog credentials and polling
|
||||
export async function setGraylogCredentials(
|
||||
projectId: string,
|
||||
baseUrl: string,
|
||||
apiToken: string,
|
||||
analystAgentId: string,
|
||||
developerAgentId: string,
|
||||
streamId: string | null,
|
||||
queryFilter: string,
|
||||
pollingIntervalMinutes: number,
|
||||
lookbackMinutes: number,
|
||||
scoreThreshold: number
|
||||
): Promise<GraylogCredentialsSafe> {
|
||||
return invoke("set_graylog_credentials", {
|
||||
projectId,
|
||||
baseUrl,
|
||||
apiToken,
|
||||
analystAgentId,
|
||||
developerAgentId,
|
||||
streamId,
|
||||
queryFilter,
|
||||
pollingIntervalMinutes,
|
||||
lookbackMinutes,
|
||||
scoreThreshold,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getGraylogCredentials(projectId: string): Promise<GraylogCredentialsSafe | null> {
|
||||
return invoke("get_graylog_credentials", { projectId });
|
||||
}
|
||||
|
||||
export async function deleteGraylogCredentials(projectId: string): Promise<void> {
|
||||
return invoke("delete_graylog_credentials", { projectId });
|
||||
}
|
||||
|
||||
export async function testGraylogConnection(projectId: string): Promise<string> {
|
||||
return invoke("test_graylog_connection", { projectId });
|
||||
}
|
||||
|
||||
export async function manualGraylogPoll(projectId: string): Promise<number> {
|
||||
return invoke("manual_graylog_poll", { projectId });
|
||||
}
|
||||
|
||||
export async function listGraylogSubjects(projectId: string): Promise<GraylogSubject[]> {
|
||||
return invoke("list_graylog_subjects", { projectId });
|
||||
}
|
||||
|
||||
export async function listGraylogDetections(
|
||||
projectId: string,
|
||||
subjectId?: string
|
||||
): Promise<GraylogDetection[]> {
|
||||
return invoke("list_graylog_detections", { projectId, subjectId });
|
||||
}
|
||||
|
||||
// Trackers
|
||||
export async function addTracker(
|
||||
projectId: string,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,10 @@ export interface WatchedTracker {
|
|||
|
||||
export interface ProcessedTicket {
|
||||
id: string;
|
||||
tracker_id: string;
|
||||
tracker_id: string | null;
|
||||
project_id: string;
|
||||
source: string;
|
||||
source_ref: string | null;
|
||||
artifact_id: number;
|
||||
artifact_title: string;
|
||||
artifact_data: string;
|
||||
|
|
@ -80,6 +83,52 @@ export interface ProcessedTicket {
|
|||
processed_at: string | null;
|
||||
}
|
||||
|
||||
export interface GraylogCredentialsSafe {
|
||||
id: string;
|
||||
project_id: string;
|
||||
base_url: string;
|
||||
analyst_agent_id: string;
|
||||
developer_agent_id: string;
|
||||
stream_id: string | null;
|
||||
query_filter: string;
|
||||
polling_interval_minutes: number;
|
||||
lookback_minutes: number;
|
||||
score_threshold: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface GraylogSubject {
|
||||
id: string;
|
||||
project_id: string;
|
||||
subject_key: string;
|
||||
source: string;
|
||||
normalized_message: string;
|
||||
first_seen_at: string;
|
||||
last_seen_at: string;
|
||||
last_score: number;
|
||||
active_ticket_id: string | null;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface GraylogDetection {
|
||||
id: string;
|
||||
subject_id: string;
|
||||
window_start: string;
|
||||
window_end: string;
|
||||
critical_count: number;
|
||||
error_count: number;
|
||||
warning_count: number;
|
||||
total_count: number;
|
||||
last_seen_at: string;
|
||||
score: number;
|
||||
triggered: boolean;
|
||||
triggered_ticket_id: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ProjectThroughputStats {
|
||||
backlog_count: number;
|
||||
done_last_24h: number;
|
||||
|
|
|
|||
Loading…
Reference in a new issue