|
|
|
@@ -4,7 +4,7 @@
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
<meta charset="UTF-8" />
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
<title>Admin Operations Center</title>
|
|
|
|
<title>Admin Operations Center</title>
|
|
|
|
<link rel="stylesheet" href="/style.css?v=14" />
|
|
|
|
<link rel="stylesheet" href="/style.css?v=15" />
|
|
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
|
|
|
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
|
|
|
</head>
|
|
|
|
</head>
|
|
|
|
@@ -56,8 +56,11 @@
|
|
|
|
</header>
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<h3>Source Health</h3>
|
|
|
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('sourceHealth')">
|
|
|
|
<div class="source-health">
|
|
|
|
<h3>Source Health</h3>
|
|
|
|
|
|
|
|
<span class="panel-toggle" :class="panelState.sourceHealth ? 'panel-toggle--open' : ''">▸</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div x-show="panelState.sourceHealth">
|
|
|
|
<template x-for="src in sourceHealth" :key="src.source">
|
|
|
|
<template x-for="src in sourceHealth" :key="src.source">
|
|
|
|
<div class="health-card">
|
|
|
|
<div class="health-card">
|
|
|
|
<strong x-text="src.source"></strong>
|
|
|
|
<strong x-text="src.source"></strong>
|
|
|
|
@@ -71,11 +74,15 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<div class="panel-header">
|
|
|
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('alerts')">
|
|
|
|
<h3>Alerts</h3>
|
|
|
|
<h3>Alerts</h3>
|
|
|
|
<span x-text="`${alertSummary.total_open} open`" class="alert-open-count"></span>
|
|
|
|
<div style="display:flex;align-items:center;gap:10px;">
|
|
|
|
|
|
|
|
<span x-text="`${alertSummary.total_open} open`" class="alert-open-count"></span>
|
|
|
|
|
|
|
|
<span class="panel-toggle" :class="panelState.alerts ? 'panel-toggle--open' : ''">▸</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="alert-filters">
|
|
|
|
<div x-show="panelState.alerts">
|
|
|
|
|
|
|
|
<div class="alert-filters">
|
|
|
|
<select x-model="alertsFilter.status" @change="alertsPage = 1; loadAlerts()">
|
|
|
|
<select x-model="alertsFilter.status" @change="alertsPage = 1; loadAlerts()">
|
|
|
|
<option value="">All statuses</option>
|
|
|
|
<option value="">All statuses</option>
|
|
|
|
<option value="open">Open</option>
|
|
|
|
<option value="open">Open</option>
|
|
|
|
@@ -117,14 +124,19 @@
|
|
|
|
<span x-text="`Page ${alertsPage}`"></span>
|
|
|
|
<span x-text="`Page ${alertsPage}`"></span>
|
|
|
|
<button type="button" :disabled="alertsPage * 20 >= alertsTotal" @click="alertsPage++; loadAlerts()">Next</button>
|
|
|
|
<button type="button" :disabled="alertsPage * 20 >= alertsTotal" @click="alertsPage++; loadAlerts()">Next</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<div class="panel-header">
|
|
|
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('rules')">
|
|
|
|
<h3>Alert Rules</h3>
|
|
|
|
<h3>Alert Rules</h3>
|
|
|
|
<button type="button" class="btn--compact" @click="openRuleEditor()">+ Add rule</button>
|
|
|
|
<div style="display:flex;align-items:center;gap:10px;">
|
|
|
|
|
|
|
|
<button type="button" class="btn--compact" @click.stop="openRuleEditor()">+ Add rule</button>
|
|
|
|
|
|
|
|
<span class="panel-toggle" :class="panelState.rules ? 'panel-toggle--open' : ''">▸</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="rules-list">
|
|
|
|
<div x-show="panelState.rules">
|
|
|
|
|
|
|
|
<div class="rules-list">
|
|
|
|
<template x-for="rule in rules" :key="rule.id">
|
|
|
|
<template x-for="rule in rules" :key="rule.id">
|
|
|
|
<div class="rule-card" :class="rule.enabled ? '' : 'rule-card--disabled'">
|
|
|
|
<div class="rule-card" :class="rule.enabled ? '' : 'rule-card--disabled'">
|
|
|
|
<div class="rule-card__meta">
|
|
|
|
<div class="rule-card__meta">
|
|
|
|
@@ -151,6 +163,7 @@
|
|
|
|
<div class="rules-empty" x-show="rules.length === 0">
|
|
|
|
<div class="rules-empty" x-show="rules.length === 0">
|
|
|
|
<p>No custom rules yet. Pre-built admin-ops rules are active by default. Add your own rules to detect specific patterns.</p>
|
|
|
|
<p>No custom rules yet. Pre-built admin-ops rules are active by default. Add your own rules to detect specific patterns.</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="ruleModal" class="modal hidden" role="dialog" aria-modal="true" :class="{ 'hidden': !ruleModalOpen }">
|
|
|
|
<div id="ruleModal" class="modal hidden" role="dialog" aria-modal="true" :class="{ 'hidden': !ruleModalOpen }">
|
|
|
|
<div class="modal__content" style="max-width: 600px;">
|
|
|
|
<div class="modal__content" style="max-width: 600px;">
|
|
|
|
@@ -210,7 +223,11 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<form id="filters" class="filters" @submit.prevent="resetPagination(); loadEvents()">
|
|
|
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('filters')">
|
|
|
|
|
|
|
|
<h3>Filters</h3>
|
|
|
|
|
|
|
|
<span class="panel-toggle" :class="panelState.filters ? 'panel-toggle--open' : ''">▸</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<form id="filters" class="filters" @submit.prevent="resetPagination(); loadEvents()" x-show="panelState.filters">
|
|
|
|
<div class="filter-row">
|
|
|
|
<div class="filter-row">
|
|
|
|
<label>
|
|
|
|
<label>
|
|
|
|
User (name/UPN)
|
|
|
|
User (name/UPN)
|
|
|
|
@@ -304,8 +321,11 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel" x-show="aiFeaturesEnabled">
|
|
|
|
<section class="panel" x-show="aiFeaturesEnabled">
|
|
|
|
<h3>Ask a question</h3>
|
|
|
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('ask')">
|
|
|
|
<form class="ask-form" @submit.prevent="askQuestion()">
|
|
|
|
<h3>Ask a question</h3>
|
|
|
|
|
|
|
|
<span class="panel-toggle" :class="panelState.ask ? 'panel-toggle--open' : ''">▸</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<form class="ask-form" @submit.prevent="askQuestion()" x-show="panelState.ask">
|
|
|
|
<div class="ask-row">
|
|
|
|
<div class="ask-row">
|
|
|
|
<input
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
type="text"
|
|
|
|
@@ -347,11 +367,15 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="panel">
|
|
|
|
<section class="panel">
|
|
|
|
<div class="panel-header">
|
|
|
|
<div class="panel-header panel-header--collapsible" @click="togglePanel('events')">
|
|
|
|
<h2>Events</h2>
|
|
|
|
<h2>Events</h2>
|
|
|
|
<span id="count" x-text="countText"></span>
|
|
|
|
<div style="display:flex;align-items:center;gap:10px;">
|
|
|
|
|
|
|
|
<span id="count" x-text="countText"></span>
|
|
|
|
|
|
|
|
<span class="panel-toggle" :class="panelState.events ? 'panel-toggle--open' : ''">▸</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="status" class="status" aria-live="polite" x-text="statusText"></div>
|
|
|
|
<div x-show="panelState.events">
|
|
|
|
|
|
|
|
<div id="status" class="status" aria-live="polite" x-text="statusText"></div>
|
|
|
|
<div id="events" class="events">
|
|
|
|
<div id="events" class="events">
|
|
|
|
<template x-for="(evt, idx) in events" :key="evt._id || evt.id || idx">
|
|
|
|
<template x-for="(evt, idx) in events" :key="evt._id || evt.id || idx">
|
|
|
|
<article class="event">
|
|
|
|
<article class="event">
|
|
|
|
@@ -391,6 +415,7 @@
|
|
|
|
<span x-text="`Page ${cursorStack.length + 1}`"></span>
|
|
|
|
<span x-text="`Page ${cursorStack.length + 1}`"></span>
|
|
|
|
<button type="button" id="nextPage" :disabled="!nextCursor" @click="goNext()">Next</button>
|
|
|
|
<button type="button" id="nextPage" :disabled="!nextCursor" @click="goNext()">Next</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle" :class="{ 'hidden': !modalOpen }">
|
|
|
|
<div id="modal" class="modal hidden" role="dialog" aria-modal="true" aria-labelledby="modalTitle" :class="{ 'hidden': !modalOpen }">
|
|
|
|
@@ -452,6 +477,7 @@
|
|
|
|
filters: {
|
|
|
|
filters: {
|
|
|
|
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 24, includeTags: '', excludeTags: '',
|
|
|
|
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 24, includeTags: '', excludeTags: '',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
panelState: { sourceHealth: true, alerts: true, rules: true, filters: true, ask: true, events: true },
|
|
|
|
options: { actors: [], services: [], operations: [], results: [] },
|
|
|
|
options: { actors: [], services: [], operations: [], results: [] },
|
|
|
|
savedSearches: [],
|
|
|
|
savedSearches: [],
|
|
|
|
appVersion: '',
|
|
|
|
appVersion: '',
|
|
|
|
@@ -479,6 +505,7 @@
|
|
|
|
await this.loadVersion();
|
|
|
|
await this.loadVersion();
|
|
|
|
await this.initAuth();
|
|
|
|
await this.initAuth();
|
|
|
|
this.loadSavedFilters();
|
|
|
|
this.loadSavedFilters();
|
|
|
|
|
|
|
|
this.loadPanelState();
|
|
|
|
if (!this.authConfig?.auth_enabled || this.accessToken) {
|
|
|
|
if (!this.authConfig?.auth_enabled || this.accessToken) {
|
|
|
|
await this.loadFilterOptions();
|
|
|
|
await this.loadFilterOptions();
|
|
|
|
await this.loadSavedSearches();
|
|
|
|
await this.loadSavedSearches();
|
|
|
|
@@ -508,6 +535,27 @@
|
|
|
|
} catch {}
|
|
|
|
} catch {}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
loadPanelState() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
const saved = localStorage.getItem('aoc_panels');
|
|
|
|
|
|
|
|
if (saved) {
|
|
|
|
|
|
|
|
const parsed = JSON.parse(saved);
|
|
|
|
|
|
|
|
Object.keys(parsed).forEach((k) => { if (this.panelState[k] !== undefined) this.panelState[k] = parsed[k]; });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
savePanelState() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
localStorage.setItem('aoc_panels', JSON.stringify(this.panelState));
|
|
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
togglePanel(key) {
|
|
|
|
|
|
|
|
this.panelState[key] = !this.panelState[key];
|
|
|
|
|
|
|
|
this.savePanelState();
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async loadVersion() {
|
|
|
|
async loadVersion() {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const res = await fetch('/api/version');
|
|
|
|
const res = await fetch('/api/version');
|
|
|
|
|