miroir/crates/miroir-proxy/admin-ui/dist/index.html
jedarden 9d29d757c7 feat(admin-ui): add 2PC settings preview endpoint and UI integration
Implements P5.19.b §13.19 - Indexes + Aliases sections with LIVE 2PC preview.

Backend changes:
- Add POST /indexes/{index}/settings preview endpoint
- Returns current vs proposed settings with SHA256 fingerprints
- Shows node targets, version info, and diff summary
- Displays full two-phase flow (propose/verify/commit) details
- Export compute_settings_diff for testing

Frontend changes:
- Update previewSettingsChanges() to call new preview endpoint
- Display current/proposed fingerprints, version info
- Show node targets and two-phase flow steps
- Render structured diff (added/removed/modified)

Tests:
- Add p13_19_admin_ui_2pc_preview.rs acceptance tests
- Verify fingerprint computation, diff detection, node targets

Closes: miroir-uhj.19.2
2026-05-25 00:03:35 -04:00

1071 lines
65 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Miroir Admin</title>
<link rel="stylesheet" href="/_miroir/admin/styles.css">
</head>
<body>
<div class="app">
<nav class="sidebar">
<div class="sidebar-header">
<h1>Miroir</h1>
<span class="subtitle">Admin Console</span>
</div>
<ul class="nav-links">
<li><a href="#overview" data-section="overview" class="active">
<span class="icon">📊</span> Overview
</a></li>
<li><a href="#topology" data-section="topology">
<span class="icon">🔷</span> Topology
</a></li>
<li><a href="#indexes" data-section="indexes">
<span class="icon">📚</span> Indexes
</a></li>
<li><a href="#aliases" data-section="aliases">
<span class="icon">🔖</span> Aliases
</a></li>
<li><a href="#documents" data-section="documents">
<span class="icon">📄</span> Documents
</a></li>
<li><a href="#query" data-section="query">
<span class="icon">🔍</span> Query Sandbox
</a></li>
<li><a href="#tasks" data-section="tasks">
<span class="icon">⚙️</span> Tasks
</a></li>
<li><a href="#canaries" data-section="canaries">
<span class="icon">🐤</span> Canaries
</a></li>
<li><a href="#shadow" data-section="shadow">
<span class="icon">👻</span> Shadow Diff
</a></li>
<li><a href="#cdc" data-section="cdc">
<span class="icon">📡</span> CDC Inspector
</a></li>
<li><a href="#metrics" data-section="metrics">
<span class="icon">📈</span> Metrics
</a></li>
<li><a href="#settings" data-section="settings">
<span class="icon">⚙️</span> Settings
</a></li>
</ul>
<div class="sidebar-footer">
<div class="connection-status" id="connectionStatus">
<span class="status-dot"></span> Connected
</div>
</div>
</nav>
<main class="main-content">
<header class="top-bar">
<button class="mobile-menu-toggle" id="mobileMenuToggle"></button>
<h2 id="sectionTitle">Overview</h2>
<div class="top-bar-actions">
<button class="refresh-btn" id="refreshBtn" title="Refresh data">🔄</button>
</div>
</header>
<div class="content-area">
<!-- Overview Section -->
<section id="overview" class="section active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">Cluster Status</div>
<div class="stat-value" id="clusterStatus">Loading...</div>
<div class="stat-sub" id="clusterStatusSub"></div>
</div>
<div class="stat-card">
<div class="stat-label">Total Shards</div>
<div class="stat-value" id="totalShards">-</div>
<div class="stat-sub"><span id="replicationFactor"></span>x replication</div>
</div>
<div class="stat-card">
<div class="stat-label">Nodes</div>
<div class="stat-value" id="totalNodes">-</div>
<div class="stat-sub"><span id="degradedNodes">0</span> degraded</div>
</div>
<div class="stat-card">
<div class="stat-label">Replica Groups</div>
<div class="stat-value" id="totalGroups">-</div>
<div class="stat-sub">Fully covered</div>
</div>
</div>
<div class="card">
<h3>Active Operations</h3>
<div id="activeOperations">
<p class="placeholder">No active operations</p>
</div>
</div>
<div class="card">
<h3>Recent Canary Failures</h3>
<div id="canaryFailures">
<p class="placeholder">Loading canary status...</p>
</div>
</div>
<div class="card">
<h3>CDC Backlog</h3>
<div id="cdcBacklog">
<p class="placeholder">Loading CDC status...</p>
</div>
</div>
</section>
<!-- Topology Section -->
<section id="topology" class="section">
<div class="card">
<h3>Node Health</h3>
<div class="table-container">
<table class="data-table" id="nodeTable">
<thead>
<tr>
<th>Node ID</th>
<th>Address</th>
<th>Status</th>
<th>Replica Group</th>
<th>Shards</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody id="nodeTableBody">
<tr><td colspan="6" class="loading">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<h3>Shard Coverage Map</h3>
<p class="card-subtitle">Visual representation of shard distribution across nodes</p>
<div id="shardCoverageMap" class="shard-coverage-map">
<p class="placeholder">Loading...</p>
</div>
</div>
<div class="card">
<h3>Rebalance Progress</h3>
<div id="rebalanceProgress">
<p class="placeholder">No rebalance in progress</p>
</div>
</div>
</section>
<!-- Indexes Section -->
<section id="indexes" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3>Index Management</h3>
<button class="btn btn-primary" id="createIndexBtn">Create Index</button>
</div>
<p class="card-subtitle">Manage indexes and their settings with two-phase commit (2PC) preview</p>
<div class="table-container">
<table class="data-table" id="indexesTable">
<thead>
<tr>
<th>UID</th>
<th>Primary Key</th>
<th>Documents</th>
<th>Settings Version</th>
<th>Fingerprint</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="indexesTableBody">
<tr><td colspan="6" class="loading">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Settings Editor Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="settingsModalTitle">Index Settings</h3>
<button class="modal-close" id="settingsModalClose">&times;</button>
</div>
<div class="modal-body">
<div class="settings-preview" id="settingsPreview">
<h4>Current Settings</h4>
<pre id="currentSettingsJson" class="settings-json"></pre>
</div>
<div class="settings-editor">
<h4>Edit Settings (JSON)</h4>
<textarea id="settingsEditor" class="settings-textarea" rows="15"></textarea>
</div>
<div class="settings-diff" id="settingsDiff" style="display: none;">
<h4>2PC Preview — What Will Change</h4>
<div class="diff-summary" id="diffSummary"></div>
<div class="diff-fingerprint">
<strong>Current Fingerprint:</strong> <code id="currentFingerprint"></code>
</div>
<div class="diff-fingerprint">
<strong>Proposed Fingerprint:</strong> <code id="newFingerprint"></code>
</div>
<div class="diff-fingerprint" id="settingsVersionInfo"></div>
<div class="diff-fingerprint" id="nodeTargetsInfo"></div>
<div class="two-phase-flow" id="twoPhaseFlowInfo"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="settingsPreviewBtn">Preview Changes</button>
<button class="btn btn-primary" id="settingsApplyBtn" style="display: none;">Apply Changes</button>
<button class="btn btn-secondary" id="settingsCancelBtn">Cancel</button>
</div>
</div>
</div>
<!-- Create Index Modal -->
<div id="createIndexModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>Create Index</h3>
<button class="modal-close" id="createIndexModalClose">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="indexUid">Index UID *</label>
<input type="text" id="indexUid" class="form-input" placeholder="my-index" required>
</div>
<div class="form-group">
<label for="indexPrimaryKey">Primary Key</label>
<input type="text" id="indexPrimaryKey" class="form-input" placeholder="id">
<small class="form-hint">Required for sharding. If omitted, Meilisearch will infer it from the first document.</small>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="createIndexCancelBtn">Cancel</button>
<button class="btn btn-primary" id="createIndexConfirmBtn">Create</button>
</div>
</div>
</div>
<!-- Delete Index Confirmation Modal -->
<div id="deleteIndexModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>Delete Index</h3>
<button class="modal-close" id="deleteIndexModalClose">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete index <strong id="deleteIndexName"></strong>?</p>
<p class="warning">This action cannot be undone. All documents in this index will be permanently deleted.</p>
<div class="form-group">
<label for="deleteIndexConfirm">Type the index name to confirm</label>
<input type="text" id="deleteIndexConfirm" class="form-input">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="deleteIndexCancelBtn">Cancel</button>
<button class="btn btn-danger" id="deleteIndexConfirmBtn" disabled>Delete</button>
</div>
</div>
</div>
</section>
<!-- Aliases Section -->
<section id="aliases" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3>Alias Management</h3>
<button class="btn btn-primary" id="createAliasBtn">Create Alias</button>
</div>
<p class="card-subtitle">Atomic index aliases for blue-green reindexing and zero-downtime migrations</p>
<div class="table-container">
<table class="data-table" id="aliasesTable">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Current Target</th>
<th>Version</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="aliasesTableBody">
<tr><td colspan="6" class="loading">Loading...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Alias History Timeline -->
<div id="aliasHistoryCard" class="card" style="display: none;">
<h3>Alias History: <span id="aliasHistoryName"></span></h3>
<div id="aliasHistoryTimeline" class="timeline"></div>
</div>
<!-- Create Alias Modal -->
<div id="createAliasModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>Create Alias</h3>
<button class="modal-close" id="createAliasModalClose">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="aliasName">Alias Name *</label>
<input type="text" id="aliasName" class="form-input" placeholder="products" required>
</div>
<div class="form-group">
<label>Alias Type *</label>
<select id="aliasType" class="form-input">
<option value="single">Single Target (for blue-green deployments)</option>
<option value="multi">Multi Target (for ILM rollups)</option>
</select>
</div>
<div class="form-group" id="singleTargetGroup">
<label for="aliasTarget">Target Index *</label>
<input type="text" id="aliasTarget" class="form-input" placeholder="products_v3">
</div>
<div class="form-group" id="multiTargetGroup" style="display: none;">
<label for="aliasTargets">Target Indexes (comma-separated) *</label>
<input type="text" id="aliasTargets" class="form-input" placeholder="logs-2026-01-01, logs-2026-01-02">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="createAliasCancelBtn">Cancel</button>
<button class="btn btn-primary" id="createAliasConfirmBtn">Create</button>
</div>
</div>
</div>
<!-- Flip Alias Modal -->
<div id="flipAliasModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>Flip Alias</h3>
<button class="modal-close" id="flipAliasModalClose">&times;</button>
</div>
<div class="modal-body">
<p>Flip alias <strong id="flipAliasName"></strong> to a new target:</p>
<div class="form-group">
<label>Current Target</label>
<input type="text" id="flipAliasCurrent" class="form-input" disabled>
</div>
<div class="form-group">
<label for="flipAliasNew">New Target *</label>
<input type="text" id="flipAliasNew" class="form-input" placeholder="products_v4">
</div>
<p class="info">This atomically updates the alias. All queries will immediately resolve to the new target.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="flipAliasCancelBtn">Cancel</button>
<button class="btn btn-primary" id="flipAliasConfirmBtn">Flip</button>
</div>
</div>
</div>
<!-- Delete Alias Confirmation Modal -->
<div id="deleteAliasModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>Delete Alias</h3>
<button class="modal-close" id="deleteAliasModalClose">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete alias <strong id="deleteAliasName"></strong>?</p>
<p class="warning">This action cannot be undone. Queries using this alias will fail after deletion.</p>
<div class="form-group">
<label for="deleteAliasConfirm">Type the alias name to confirm</label>
<input type="text" id="deleteAliasConfirm" class="form-input">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="deleteAliasCancelBtn">Cancel</button>
<button class="btn btn-danger" id="deleteAliasConfirmBtn" disabled>Delete</button>
</div>
</div>
</div>
</section>
<!-- Documents Section -->
<section id="documents" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem;">
<div style="display: flex; gap: 0.5rem; align-items: center; flex: 1; min-width: 200px;">
<select id="documentIndexSelect" class="form-input" style="flex: 1;">
<option value="">Loading indexes...</option>
</select>
</div>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-secondary" id="importDocumentsBtn">📥 Import</button>
<button class="btn btn-secondary" id="exportDocumentsBtn">📤 Export</button>
</div>
</div>
<div class="card">
<h4>Filter Builder</h4>
<div id="documentFilterBuilder" class="filter-builder">
<div class="filter-row">
<select class="form-input filter-field">
<option value="">Select field...</option>
</select>
<select class="form-input filter-operator">
<option value="equals">Equals</option>
<option value="notEquals">Not Equals</option>
<option value="gt">Greater Than</option>
<option value="gte">Greater Than or Equal</option>
<option value="lt">Less Than</option>
<option value="lte">Less Than or Equal</option>
<option value="in">In</option>
<option value="exists">Exists</option>
</select>
<input type="text" class="form-input filter-value" placeholder="Value">
<button class="btn btn-secondary btn-sm" onclick="addFilterRow('documentFilterBuilder')">+</button>
<button class="btn btn-secondary btn-sm" onclick="removeFilterRow(this)"></button>
</div>
</div>
<button class="btn btn-primary" id="applyDocumentFilterBtn" style="margin-top: 0.5rem;">Apply Filters</button>
<button class="btn btn-secondary" id="clearDocumentFilterBtn" style="margin-top: 0.5rem;">Clear</button>
</div>
<div class="table-container" style="margin-top: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
<span id="documentCount" class="text-secondary">0 documents</span>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<label>Limit:</label>
<select id="documentLimit" class="form-input" style="width: 80px;">
<option value="20">20</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="200">200</option>
</select>
<label>Offset:</label>
<input type="number" id="documentOffset" class="form-input" value="0" min="0" style="width: 80px;">
</div>
</div>
<table class="data-table" id="documentsTable">
<thead id="documentsTableHead">
<tr><th class="loading">Loading...</th></tr>
</thead>
<tbody id="documentsTableBody">
<tr><td class="loading">Select an index to browse documents</td></tr>
</tbody>
</table>
<div id="documentPagination" class="pagination" style="margin-top: 1rem; display: none;">
<button class="btn btn-secondary btn-sm" id="documentPrevPage">← Previous</button>
<span id="documentPageInfo" style="margin: 0 1rem;">Page 1</span>
<button class="btn btn-secondary btn-sm" id="documentNextPage">Next →</button>
</div>
</div>
</div>
<!-- Import Documents Modal -->
<div id="importDocumentsModal" class="modal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h3>Import Documents</h3>
<button class="modal-close" id="importDocumentsModalClose">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Import Method</label>
<select id="importMethod" class="form-input">
<option value="stream">Streaming Import (recommended for large files)</option>
<option value="batch">Batch Import (for smaller files)</option>
</select>
</div>
<div class="form-group">
<label>File Format</label>
<select id="importFormat" class="form-input">
<option value="ndjson">NDJSON (newline-delimited JSON)</option>
<option value="csv">CSV</option>
<option value="json">JSON Array</option>
</select>
</div>
<div class="dropzone" id="importDropzone">
<p>Drag & drop files here, or click to select</p>
<input type="file" id="importFileInput" multiple accept=".json,.ndjson,.csv" style="display: none;">
</div>
<div id="importPreview" style="display: none; margin-top: 1rem;">
<h4>Preview</h4>
<pre id="importPreviewContent" style="max-height: 200px; overflow: auto; background: var(--bg-secondary); padding: 0.5rem; border-radius: 4px;"></pre>
<p id="importFileCount" class="text-secondary" style="margin-top: 0.5rem;"></p>
</div>
<div id="importProgress" style="display: none; margin-top: 1rem;">
<h4>Import Progress</h4>
<div class="progress-bar">
<div class="progress-fill" id="importProgressBar" style="width: 0%"></div>
</div>
<p id="importStatus" class="text-secondary" style="margin-top: 0.5rem;">Initializing...</p>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="importCancelBtn">Cancel</button>
<button class="btn btn-primary" id="importConfirmBtn" disabled>Import</button>
</div>
</div>
</div>
</section>
<!-- Query Sandbox Section -->
<section id="query" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; gap: 0.5rem;">
<div style="display: flex; gap: 0.5rem; align-items: center; flex: 1; min-width: 200px;">
<select id="queryIndexSelect" class="form-input" style="flex: 1;">
<option value="">Loading indexes...</option>
</select>
</div>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-primary" id="runQueryBtn">▶ Run Query</button>
<button class="btn btn-secondary" id="explainQueryBtn"> Explain</button>
<button class="btn btn-secondary" id="shadowDiffBtn">👻 Shadow Diff</button>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
<!-- Query Builder -->
<div class="card" style="margin: 0;">
<h4>Query Builder</h4>
<div class="form-group">
<label for="queryQ">Query Text (q)</label>
<input type="text" id="queryQ" class="form-input" placeholder="Enter search query...">
</div>
<div class="card" style="margin: 0.5rem 0;">
<h4>Filter Builder</h4>
<div id="queryFilterBuilder" class="filter-builder">
<div class="filter-row">
<select class="form-input filter-field">
<option value="">Select field...</option>
</select>
<select class="form-input filter-operator">
<option value="equals">Equals</option>
<option value="notEquals">Not Equals</option>
<option value="gt">Greater Than</option>
<option value="gte">Greater Than or Equal</option>
<option value="lt">Less Than</option>
<option value="lte">Less Than or Equal</option>
<option value="in">In</option>
<option value="exists">Exists</option>
</select>
<input type="text" class="form-input filter-value" placeholder="Value">
<button class="btn btn-secondary btn-sm" onclick="addFilterRow('queryFilterBuilder')">+</button>
<button class="btn btn-secondary btn-sm" onclick="removeFilterRow(this)"></button>
</div>
</div>
<button class="btn btn-secondary btn-sm" id="addQueryFilterBtn" style="margin-top: 0.5rem;">+ Add Filter</button>
</div>
<div class="card" style="margin: 0.5rem 0;">
<h4>Sort Builder</h4>
<div id="sortBuilder">
<div class="sort-row">
<select class="form-input sort-field">
<option value="">Select field...</option>
</select>
<select class="form-input sort-direction">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
<button class="btn btn-secondary btn-sm" onclick="removeSortRow(this)"></button>
</div>
</div>
<button class="btn btn-secondary btn-sm" id="addSortBtn" style="margin-top: 0.5rem;">+ Add Sort</button>
</div>
<div class="card" style="margin: 0.5rem 0;">
<h4>Facet Request Builder</h4>
<div id="facetBuilder">
<div class="facet-row">
<select class="form-input facet-field">
<option value="">Select field...</option>
</select>
<button class="btn btn-secondary btn-sm" onclick="removeFacetRow(this)"></button>
</div>
</div>
<button class="btn btn-secondary btn-sm" id="addFacetBtn" style="margin-top: 0.5rem;">+ Add Facet</button>
</div>
<div class="form-group" style="margin-top: 1rem;">
<label for="queryLimit">Limit</label>
<input type="number" id="queryLimit" class="form-input" value="20" min="1" max="1000">
</div>
<div class="form-group">
<label for="queryOffset">Offset</label>
<input type="number" id="queryOffset" class="form-input" value="0" min="0">
</div>
</div>
<!-- Query Results -->
<div class="card" style="margin: 0;">
<h4>Results</h4>
<div id="queryResults" class="query-results">
<p class="placeholder">Run a query to see results</p>
</div>
<div id="queryStats" class="query-stats" style="display: none; margin-top: 1rem;">
<div class="stat-row">
<span class="stat-label">Hits:</span>
<span class="stat-value" id="queryHits">-</span>
</div>
<div class="stat-row">
<span class="stat-label">Processing Time:</span>
<span class="stat-value" id="queryProcessingTime">-</span>
</div>
<div class="stat-row">
<span class="stat-label">Shards Queried:</span>
<span class="stat-value" id="queryShardsQueried">-</span>
</div>
</div>
</div>
</div>
<!-- Per-Shard Latency Breakdown -->
<div id="latencyBreakdown" class="card" style="display: none;">
<h4>Per-Shard Latency Breakdown</h4>
<div id="shardLatencyTable" class="table-container"></div>
</div>
<!-- Explain Results -->
<div id="explainResults" class="card" style="display: none;">
<h4>Query Plan Explanation</h4>
<pre id="explainJson" style="max-height: 400px; overflow: auto; background: var(--bg-secondary); padding: 1rem; border-radius: 4px;"></pre>
</div>
<!-- Shadow Diff Results -->
<div id="shadowDiffResults" class="card" style="display: none;">
<h4>Shadow Diff Results</h4>
<div id="shadowDiffContent"></div>
</div>
</div>
</section>
<!-- Tasks Section -->
<section id="tasks" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3>Active Tasks</h3>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<label for="taskFilter">Filter:</label>
<select id="taskFilter" class="form-input">
<option value="all">All Tasks</option>
<option value="active">Active Only</option>
<option value="pending">Pending</option>
<option value="succeeded">Succeeded</option>
<option value="failed">Failed</option>
</select>
<label for="taskIndex">Index:</label>
<select id="taskIndex" class="form-input">
<option value="">All Indexes</option>
</select>
<label for="taskType">Type:</label>
<select id="taskType" class="form-input">
<option value="">All Types</option>
<option value="documentAddition">Document Addition</option>
<option value="documentUpdate">Document Update</option>
<option value="documentDeletion">Document Deletion</option>
<option value="settingsUpdate">Settings Update</option>
<option value="indexCreation">Index Creation</option>
<option value="indexDeletion">Index Deletion</option>
<option value="indexUpdate">Index Update</option>
<option value="indexSwap">Index Swap</option>
</select>
</div>
</div>
<div class="table-container">
<table class="data-table" id="tasksTable">
<thead>
<tr>
<th>Task UID</th>
<th>Type</th>
<th>Index</th>
<th>Status</th>
<th>Progress</th>
<th>Started</th>
<th>Finished</th>
<th>Duration</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="tasksTableBody">
<tr><td colspan="9" class="loading">Loading tasks...</td></tr>
</tbody>
</table>
</div>
<div id="taskPagination" class="pagination" style="margin-top: 1rem; display: flex; justify-content: center; align-items: center; gap: 1rem;">
<button class="btn btn-secondary btn-sm" id="taskPrevPage" disabled>← Previous</button>
<span id="taskPageInfo">Showing 0-0 of 0 tasks</span>
<button class="btn btn-secondary btn-sm" id="taskNextPage" disabled>Next →</button>
</div>
</div>
<!-- Task Details Modal -->
<div id="taskDetailsModal" class="modal">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h3>Task Details</h3>
<button class="modal-close" id="taskDetailsModalClose">&times;</button>
</div>
<div class="modal-body">
<div id="taskDetailsContent"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="taskDetailsCloseBtn">Close</button>
</div>
</div>
</div>
</section>
<section id="canaries" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3>Canary Tests</h3>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-secondary" id="startCaptureBtn">🎯 Capture Traffic</button>
<button class="btn btn-primary" id="createCanaryBtn">+ Create Canary</button>
</div>
</div>
<p class="card-subtitle">Automated query assertions with pass-fail tracking over time</p>
<div class="table-container">
<table class="data-table" id="canariesTable">
<thead>
<tr>
<th>Name</th>
<th>Index</th>
<th>Interval</th>
<th>Status</th>
<th>Last Run</th>
<th>Pass Rate (24h)</th>
<th>Heatmap</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="canariesTableBody">
<tr><td colspan="8" class="loading">Loading canaries...</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Canary Details Modal -->
<div id="canaryDetailsModal" class="modal">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h3 id="canaryDetailsTitle">Canary Details</h3>
<button class="modal-close" id="canaryDetailsModalClose">&times;</button>
</div>
<div class="modal-body">
<div id="canaryDetailsContent"></div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="canaryDetailsCloseBtn">Close</button>
</div>
</div>
</div>
<!-- Create/Edit Canary Modal -->
<div id="canaryEditModal" class="modal">
<div class="modal-content" style="max-width: 600px;">
<div class="modal-header">
<h3 id="canaryEditTitle">Create Canary</h3>
<button class="modal-close" id="canaryEditModalClose">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="canaryName">Name *</label>
<input type="text" id="canaryName" class="form-input" placeholder="my-canary" required>
</div>
<div class="form-group">
<label for="canaryIndex">Index *</label>
<select id="canaryIndex" class="form-input" required>
<option value="">Select index...</option>
</select>
</div>
<div class="form-group">
<label for="canaryInterval">Interval (seconds) *</label>
<input type="number" id="canaryInterval" class="form-input" value="3600" min="60" required>
<small class="form-hint">How often to run this canary (default: 3600s = 1 hour)</small>
</div>
<div class="card" style="margin: 1rem 0;">
<h4>Query</h4>
<div class="form-group">
<label for="canaryQuery">Query (q)</label>
<input type="text" id="canaryQuery" class="form-input" placeholder="*">
</div>
<div class="form-group">
<label for="canaryLimit">Limit</label>
<input type="number" id="canaryLimit" class="form-input" value="10" min="1">
</div>
</div>
<div class="card" style="margin: 1rem 0;">
<h4>Assertions</h4>
<div id="canaryAssertions">
<div class="assertion-row">
<select class="form-input assertion-type">
<option value="minHits">Min Hits</option>
<option value="maxHits">Max Hits</option>
<option value="maxP95Ms">Max P95 Latency</option>
<option value="containsAny">Contains Any Result</option>
</select>
<input type="number" class="form-input assertion-value" placeholder="Value">
<button class="btn btn-secondary btn-sm" onclick="addAssertionRow()">+</button>
<button class="btn btn-secondary btn-sm" onclick="removeAssertionRow(this)"></button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="canaryEditCancelBtn">Cancel</button>
<button class="btn btn-primary" id="canaryEditSaveBtn">Save Canary</button>
</div>
</div>
</div>
<!-- Capture Traffic Modal -->
<div id="captureModal" class="modal">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3>Capture Traffic for Canary</h3>
<button class="modal-close" id="captureModalClose">&times;</button>
</div>
<div class="modal-body">
<p>Capture live queries to create a new canary from real traffic patterns.</p>
<div class="form-group">
<label for="captureMaxQueries">Max Queries to Capture</label>
<input type="number" id="captureMaxQueries" class="form-input" value="100" min="1" max="1000">
</div>
<div class="form-group">
<label for="captureIndex">Index</label>
<select id="captureIndex" class="form-input">
<option value="">Select index...</option>
</select>
</div>
<div id="captureStatus" style="display: none;">
<h4>Capture Status</h4>
<p id="captureStatusText">Capturing...</p>
<p id="captureCount">0 queries captured</p>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="captureCancelBtn">Cancel</button>
<button class="btn btn-primary" id="captureStartBtn">Start Capture</button>
<button class="btn btn-success" id="captureCreateBtn" style="display: none;">Create Canary</button>
</div>
</div>
</div>
</section>
<section id="shadow" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3>Shadow Diff</h3>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<label for="shadowAutoRefresh">
<input type="checkbox" id="shadowAutoRefresh"> Auto-refresh (5s)
</label>
<button class="btn btn-secondary" id="refreshShadowBtn">🔄 Refresh</button>
</div>
</div>
<p class="card-subtitle">Live stream and aggregated summary from shadow traffic comparisons</p>
<div class="stats-grid" style="margin-bottom: 1rem;">
<div class="stat-card">
<div class="stat-label">Total Shadowed</div>
<div class="stat-value" id="shadowTotal">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Errors</div>
<div class="stat-value" id="shadowErrors">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Error Rate</div>
<div class="stat-value" id="shadowErrorRate">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Recent Diffs</div>
<div class="stat-value" id="shadowRecentDiffs">-</div>
</div>
</div>
<div class="table-container">
<table class="data-table" id="shadowDiffsTable">
<thead>
<tr>
<th>Timestamp</th>
<th>Target</th>
<th>Diff Kind</th>
<th>Details</th>
</tr>
</thead>
<tbody id="shadowDiffsTableBody">
<tr><td colspan="4" class="loading">Loading shadow diffs...</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<section id="cdc" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3>CDC Inspector</h3>
<div style="display: flex; gap: 0.5rem;">
<button class="btn btn-secondary" id="cdcPauseBtn">⏸ Pause</button>
<button class="btn btn-secondary" id="cdcClearBtn">🗑 Clear</button>
</div>
</div>
<p class="card-subtitle">Live tail of change events with filter by index/operation</p>
<div class="card" style="margin: 1rem 0;">
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<label for="cdcIndexFilter">Index:</label>
<select id="cdcIndexFilter" class="form-input" style="flex: 1; min-width: 150px;">
<option value="">All Indexes</option>
</select>
<label for="cdcOperationFilter">Operation:</label>
<select id="cdcOperationFilter" class="form-input" style="flex: 1; min-width: 150px;">
<option value="">All Operations</option>
<option value="documentAddition">Add</option>
<option value="documentUpdate">Update</option>
<option value="documentDeletion">Delete</option>
<option value="settingsUpdate">Settings</option>
</select>
</div>
</div>
<div id="cdcEventsContainer" class="cdc-events-container">
<div id="cdcEvents" class="cdc-events">
<p class="placeholder">Waiting for CDC events...</p>
</div>
</div>
<div style="margin-top: 1rem; font-size: 0.875rem; color: var(--text-secondary);">
<span>Current Sequence: <strong id="cdcCurrentSequence">0</strong></span>
<span style="margin-left: 1rem;">Events Buffered: <strong id="cdcEventCount">0</strong></span>
</div>
</div>
</section>
<section id="metrics" class="section">
<div class="card">
<h3>Metrics Dashboard</h3>
<p class="card-subtitle">System metrics and performance indicators</p>
<div class="metrics-container">
<div class="metrics-tabs">
<button class="metrics-tab active" data-metrics-tab="prometheus">Prometheus</button>
<button class="metrics-tab" data-metrics-tab="grafana">Grafana</button>
</div>
<div id="metricsPrometheus" class="metrics-tab-content active">
<div class="card" style="margin: 0;">
<h4>Prometheus Metrics</h4>
<p style="margin-bottom: 1rem;">Direct access to Prometheus metrics endpoint</p>
<div style="display: flex; gap: 0.5rem; margin-bottom: 1rem;">
<input type="text" id="prometheusQuery" class="form-input" style="flex: 1;" placeholder="Enter query (e.g., miroir_request_duration_seconds)">
<button class="btn btn-primary" id="prometheusQueryBtn">Query</button>
</div>
<div id="prometheusResults" class="prometheus-results">
<p class="placeholder">Enter a query to see results</p>
</div>
</div>
<div class="card" style="margin: 1rem 0 0 0;">
<h4>Quick Metrics</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
<button class="btn btn-secondary btn-sm quick-metric-btn" data-metric="miroir_requests_total">Total Requests</button>
<button class="btn btn-secondary btn-sm quick-metric-btn" data-metric="miroir_request_duration_seconds">Request Duration</button>
<button class="btn btn-secondary btn-sm quick-metric-btn" data-metric="miroir_rebalance_in_progress">Rebalance Status</button>
<button class="btn btn-secondary btn-sm quick-metric-btn" data-metric="miroir_degraded_shards">Degraded Shards</button>
<button class="btn btn-secondary btn-sm quick-metric-btn" data-metric="miroir_antientropy_mismatches_found">AE Mismatches</button>
<button class="btn btn-secondary btn-sm quick-metric-btn" data-metric="miroir_replica_selection_score">Replica Scores</button>
</div>
</div>
</div>
<div id="metricsGrafana" class="metrics-tab-content" style="display: none;">
<div class="card" style="margin: 0;">
<h4>Grafana Dashboard</h4>
<div class="form-group">
<label for="grafanaUrl">Grafana Dashboard URL</label>
<input type="text" id="grafanaUrl" class="form-input" placeholder="https://grafana.example.com/d/...">
<small class="form-hint">Configure in Miroir settings to persist</small>
</div>
<button class="btn btn-primary" id="grafanaLoadBtn">Load Dashboard</button>
<div id="grafanaFrame" class="grafana-frame" style="margin-top: 1rem;">
<iframe id="grafanaIframe" style="width: 100%; height: 600px; border: none; display: none;"></iframe>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="settings" class="section">
<div class="card">
<h3>Miroir Settings</h3>
<p class="card-subtitle">Read and edit Miroir's own configuration</p>
<div id="settingsContent">
<div class="settings-category" id="settingsGeneral">
<h4>General Settings</h4>
<table class="data-table">
<tbody id="settingsGeneralTable">
<tr><td colspan="2" class="loading">Loading settings...</td></tr>
</tbody>
</table>
</div>
<div class="settings-category" id="settingsAdvanced">
<h4>Advanced Settings</h4>
<div class="alert info" style="margin-bottom: 1rem;">
<strong>Restart Required:</strong> Settings marked with <span class="badge warning">Restart</span> require a pod restart to take effect.
</div>
<table class="data-table">
<tbody id="settingsAdvancedTable">
<tr><td colspan="2" class="loading">Loading settings...</td></tr>
</tbody>
</table>
</div>
</div>
<div style="margin-top: 1rem; text-align: right;">
<button class="btn btn-secondary" id="settingsReloadBtn">Reload from File</button>
<button class="btn btn-primary" id="settingsEditBtn">Edit Settings</button>
</div>
</div>
<!-- Edit Settings Modal -->
<div id="settingsEditModal" class="modal">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h3>Edit Miroir Settings</h3>
<button class="modal-close" id="settingsEditModalClose">&times;</button>
</div>
<div class="modal-body">
<div class="alert warning" style="margin-bottom: 1rem;">
<strong>Warning:</strong> Editing settings directly can cause instability. Changes marked with <span class="badge warning">Restart</span> require a pod restart.
</div>
<div class="form-group">
<label for="settingsEditYaml">Settings (YAML)</label>
<textarea id="settingsEditYaml" class="settings-textarea" rows="20" spellcheck="false"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="settingsEditCancelBtn">Cancel</button>
<button class="btn btn-primary" id="settingsEditApplyBtn">Apply Changes</button>
</div>
</div>
</div>
</section>
</div>
</main>
</div>
<script src="/_miroir/admin/app.js"></script>
</body>
</html>