mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 09:50:44 +08:00
feat(ume): key alarm forward to OClaw via WSS and AI monitor UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
9a39ddfcc7
commit
1f1b42e8bf
18 changed files with 1079 additions and 32 deletions
|
|
@ -9,9 +9,13 @@ import {
|
|||
clearLocalUmeAlarmSubscription,
|
||||
establishUmeAlarmSubscription,
|
||||
fetchUmeAlarmSubscriptionStatus,
|
||||
fetchUmeKeyAlertMonitor,
|
||||
fetchUmeNotificationIds,
|
||||
fetchUmeSyncStatus,
|
||||
fetchUmeTokenStatus,
|
||||
refreshUmeToken,
|
||||
upsertUmeKeyAlertRule,
|
||||
deleteUmeKeyAlertRule,
|
||||
} from "../services/api";
|
||||
import { HelpHint } from "../components/HelpHint";
|
||||
import { queryKeys } from "../constants/queryKeys";
|
||||
|
|
@ -42,6 +46,10 @@ export function UmePage() {
|
|||
const [curPage, setCurPage] = useState(1);
|
||||
const [curPageSize, setCurPageSize] = useState(50);
|
||||
const [alarmsPanelOpen, setAlarmsPanelOpen] = useState(false);
|
||||
const [keyAlertNotificationId, setKeyAlertNotificationId] = useState("");
|
||||
const [keyAlertLabel, setKeyAlertLabel] = useState("");
|
||||
const [keyAlertForwardOnClear, setKeyAlertForwardOnClear] = useState(false);
|
||||
const [keyAlertOpError, setKeyAlertOpError] = useState("");
|
||||
|
||||
const syncMutation = useMutation({
|
||||
mutationFn: async (domains: string[]) => apiPost<{ ok: boolean; jobs: unknown[] }>("/v1/ume/sync", { domains }),
|
||||
|
|
@ -91,6 +99,18 @@ export function UmePage() {
|
|||
staleTime: 3000,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const keyAlertMonitorQuery = useQuery({
|
||||
queryKey: queryKeys.umeKeyAlertMonitor,
|
||||
queryFn: fetchUmeKeyAlertMonitor,
|
||||
staleTime: 3000,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const notificationIdsQuery = useQuery({
|
||||
queryKey: ["umeNotificationIds"],
|
||||
queryFn: () => fetchUmeNotificationIds(200),
|
||||
staleTime: 10000,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const confirmClearLocalSubscription = (hint?: string) =>
|
||||
window.confirm(hint || t("ume.subscription.confirmClearDefault"));
|
||||
|
|
@ -273,6 +293,44 @@ export function UmePage() {
|
|||
onError: (err) => setRuntimeTaskError(String(err)),
|
||||
});
|
||||
|
||||
const keyAlertAddMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
upsertUmeKeyAlertRule({
|
||||
notification_id: keyAlertNotificationId.trim(),
|
||||
label: keyAlertLabel.trim(),
|
||||
enabled: true,
|
||||
forward_on_clear: keyAlertForwardOnClear,
|
||||
}),
|
||||
onMutate: () => setKeyAlertOpError(""),
|
||||
onSuccess: async () => {
|
||||
setKeyAlertNotificationId("");
|
||||
setKeyAlertLabel("");
|
||||
showOk(t("ume.keyAlert.addOk"));
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitor });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus });
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = String(err);
|
||||
setKeyAlertOpError(msg);
|
||||
showError(msg);
|
||||
},
|
||||
});
|
||||
|
||||
const keyAlertDeleteMutation = useMutation({
|
||||
mutationFn: (notificationId: string) => deleteUmeKeyAlertRule(notificationId),
|
||||
onMutate: () => setKeyAlertOpError(""),
|
||||
onSuccess: async () => {
|
||||
showOk(t("ume.keyAlert.deleteOk"));
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitor });
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.integrationsStatus });
|
||||
},
|
||||
onError: (err) => {
|
||||
const msg = String(err);
|
||||
setKeyAlertOpError(msg);
|
||||
showError(msg);
|
||||
},
|
||||
});
|
||||
|
||||
const syncTotal = Number(syncStatusQuery.data?.total || 0);
|
||||
const syncPages = pageCount(syncTotal, syncPageSize);
|
||||
const neTotal = Number(neQuery.data?.total || 0);
|
||||
|
|
@ -280,6 +338,15 @@ export function UmePage() {
|
|||
const curTotal = Number(currentQuery.data?.total || 0);
|
||||
const curPages = pageCount(curTotal, curPageSize);
|
||||
|
||||
const keyAlertForwarder = keyAlertMonitorQuery.data?.forwarder;
|
||||
const keyAlertRules = keyAlertMonitorQuery.data?.rules || [];
|
||||
const oclawWsPill =
|
||||
!keyAlertForwarder?.enabled
|
||||
? "unknown"
|
||||
: keyAlertForwarder.connected
|
||||
? "up"
|
||||
: "down";
|
||||
|
||||
const perPage = (n: number) => t("common.perPage", { n: String(n) });
|
||||
|
||||
return (
|
||||
|
|
@ -511,6 +578,139 @@ export function UmePage() {
|
|||
</div>
|
||||
</article>
|
||||
|
||||
<article className="card card--full">
|
||||
<h3 className="card-title-with-hint">
|
||||
{t("ume.keyAlert.title")}
|
||||
<HelpHint text={t("ume.keyAlert.help")} ariaLabel={t("common.help")} />
|
||||
</h3>
|
||||
<div className="actions-row actions-row--inline">
|
||||
<span className={`conn-pill conn-pill--${oclawWsPill}`}>
|
||||
{t("ume.keyAlert.ws")}:{" "}
|
||||
{!keyAlertForwarder?.enabled
|
||||
? t("ume.keyAlert.wsDisabled")
|
||||
: keyAlertForwarder.connected
|
||||
? t("ume.keyAlert.wsConnected")
|
||||
: t("ume.keyAlert.wsDisconnected")}
|
||||
</span>
|
||||
{keyAlertForwarder?.enabled ? (
|
||||
<>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.publishedOk")}: {Number(keyAlertForwarder.published_ok || 0)}
|
||||
</span>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.publishedFail")}: {Number(keyAlertForwarder.published_fail || 0)}
|
||||
</span>
|
||||
<span className="conn-pill">
|
||||
{t("ume.keyAlert.queue")}: {Number(keyAlertForwarder.queue_size || 0)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="actions-row actions-row--inline" style={{ marginTop: 10, flexWrap: "wrap" }}>
|
||||
<input
|
||||
className="input"
|
||||
style={{ minWidth: 180 }}
|
||||
placeholder={t("ume.keyAlert.notificationIdPh")}
|
||||
value={keyAlertNotificationId}
|
||||
onChange={(e) => setKeyAlertNotificationId(e.target.value)}
|
||||
list="ume-notification-id-options"
|
||||
/>
|
||||
<datalist id="ume-notification-id-options">
|
||||
{(notificationIdsQuery.data?.items || []).map((x) => (
|
||||
<option key={x.notification_id} value={x.notification_id}>
|
||||
{x.native_probable_cause_sample || x.notification_id}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
<input
|
||||
className="input"
|
||||
style={{ minWidth: 140 }}
|
||||
placeholder={t("ume.keyAlert.labelPh")}
|
||||
value={keyAlertLabel}
|
||||
onChange={(e) => setKeyAlertLabel(e.target.value)}
|
||||
/>
|
||||
<label className="muted" style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={keyAlertForwardOnClear}
|
||||
onChange={(e) => setKeyAlertForwardOnClear(e.target.checked)}
|
||||
/>
|
||||
{t("ume.keyAlert.forwardOnClear")}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => notificationIdsQuery.refetch()}
|
||||
disabled={notificationIdsQuery.isFetching}
|
||||
>
|
||||
{t("ume.keyAlert.pickFromAlarms")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => keyAlertAddMutation.mutate()}
|
||||
disabled={keyAlertAddMutation.isPending || !keyAlertNotificationId.trim()}
|
||||
>
|
||||
{keyAlertAddMutation.isPending ? t("ume.keyAlert.adding") : t("ume.keyAlert.add")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeKeyAlertMonitor })}
|
||||
disabled={keyAlertMonitorQuery.isFetching}
|
||||
>
|
||||
{keyAlertMonitorQuery.isFetching ? t("common.refreshing") : t("common.refresh")}
|
||||
</button>
|
||||
</div>
|
||||
{keyAlertOpError ? (
|
||||
<div className="pill pill--high" style={{ marginTop: 8 }}>
|
||||
{t("common.opFailed")}: {keyAlertOpError}
|
||||
</div>
|
||||
) : null}
|
||||
<table style={{ marginTop: 12 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("ume.keyAlert.colId")}</th>
|
||||
<th>{t("ume.keyAlert.colLabel")}</th>
|
||||
<th>{t("ume.keyAlert.colPublished")}</th>
|
||||
<th>{t("ume.keyAlert.colAttempts")}</th>
|
||||
<th>{t("ume.keyAlert.colLast")}</th>
|
||||
<th>{t("ume.keyAlert.colActions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keyAlertRules.map((rule) => (
|
||||
<tr key={rule.notification_id}>
|
||||
<td>{rule.notification_id}</td>
|
||||
<td>{rule.label || t("common.empty")}</td>
|
||||
<td>{Number(rule.forward_stats?.published_ok || 0)}</td>
|
||||
<td>{Number(rule.forward_stats?.attempts || 0)}</td>
|
||||
<td>
|
||||
{rule.forward_stats?.last_forwarded_at
|
||||
? formatSystemTime(rule.forward_stats.last_forwarded_at)
|
||||
: t("common.empty")}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (window.confirm(t("ume.keyAlert.confirmDelete"))) {
|
||||
keyAlertDeleteMutation.mutate(rule.notification_id);
|
||||
}
|
||||
}}
|
||||
disabled={keyAlertDeleteMutation.isPending}
|
||||
>
|
||||
{t("ume.keyAlert.delete")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!keyAlertMonitorQuery.isLoading && keyAlertRules.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6}>{t("ume.keyAlert.emptyRules")}</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
|
||||
<article className="card card--full">
|
||||
<h3>{t("ume.sync.title")}</h3>
|
||||
<div className="actions-row actions-row--inline">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue