first commit

This commit is contained in:
2026-05-22 11:46:43 +02:00
commit 2ebd8fb16b
29 changed files with 3356 additions and 0 deletions
+963
View File
@@ -0,0 +1,963 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>QA Automation Test Plan Report</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--color-automated: #22c55e;
--color-partial: #f59e0b;
--color-manual: #ef4444;
--color-core: #3b82f6;
--color-pbx: #8b5cf6;
--color-check: #64748b;
--color-write: #0ea5e9;
--bg-header: #0f172a;
--bg-subheader: #1e293b;
--bg-body: #f8fafc;
--bg-card: #ffffff;
--bg-row-alt: #f1f5f9;
--border: #e2e8f0;
--text-primary: #0f172a;
--text-secondary: #475569;
--text-muted: #94a3b8;
--radius: 8px;
--shadow: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.06);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg-body);
color: var(--text-primary);
font-size: 14px;
line-height: 1.5;
}
/* ── HEADER ── */
.site-header {
background: var(--bg-header);
padding: 32px 48px 28px;
border-bottom: 3px solid #334155;
}
.site-header h1 {
font-size: 26px;
font-weight: 700;
color: #f1f5f9;
letter-spacing: -0.4px;
}
.site-header .subtitle {
font-size: 14px;
color: #94a3b8;
margin-top: 4px;
}
.site-header .meta {
display: flex;
align-items: center;
gap: 20px;
margin-top: 14px;
flex-wrap: wrap;
}
.meta-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #cbd5e1;
background: #1e293b;
border: 1px solid #334155;
border-radius: 20px;
padding: 3px 12px;
}
.meta-chip .dot {
width: 7px; height: 7px; border-radius: 50%;
background: #22c55e;
}
/* ── MAIN ── */
main {
max-width: 1500px;
margin: 0 auto;
padding: 32px 32px 60px;
}
/* ── SECTION TITLES ── */
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: .7px;
margin-bottom: 14px;
}
/* ── SUMMARY CARDS ── */
.cards-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 32px;
}
@media (max-width: 900px) { .cards-row { grid-template-columns: repeat(2,1fr); } }
@media (max-width: 500px) { .cards-row { grid-template-columns: 1fr; } }
.card {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
padding: 22px 24px 18px;
border-top: 4px solid var(--card-accent, #94a3b8);
position: relative;
overflow: hidden;
}
.card::after {
content: '';
position: absolute;
top: -30px; right: -30px;
width: 100px; height: 100px;
border-radius: 50%;
background: var(--card-accent, #94a3b8);
opacity: .05;
}
.card-label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .8px;
color: var(--text-secondary);
margin-bottom: 8px;
}
.card-number {
font-size: 42px;
font-weight: 800;
line-height: 1;
color: var(--card-accent, var(--text-primary));
}
.card-pct {
font-size: 13px;
color: var(--text-muted);
margin-top: 4px;
font-weight: 500;
}
.card-bar-track {
height: 6px;
background: #e2e8f0;
border-radius: 3px;
margin-top: 14px;
overflow: hidden;
}
.card-bar-fill {
height: 100%;
border-radius: 3px;
background: var(--card-accent, #94a3b8);
transition: width .6s ease;
}
/* ── CHARTS ROW ── */
.charts-row {
display: grid;
grid-template-columns: 380px 1fr;
gap: 20px;
margin-bottom: 32px;
}
@media (max-width: 900px) { .charts-row { grid-template-columns: 1fr; } }
.chart-card {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
padding: 24px;
}
.chart-card h3 {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: .6px;
margin-bottom: 18px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.chart-wrap {
position: relative;
}
.chart-wrap-doughnut {
max-width: 280px;
margin: 0 auto;
}
.doughnut-legend {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 18px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-secondary);
}
.legend-dot {
width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0;
}
.legend-val { margin-left: auto; font-weight: 600; color: var(--text-primary); }
/* ── FILTER BAR ── */
.filter-bar {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 16px 20px;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.filter-bar label {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
margin-right: 4px;
}
.filter-group {
display: flex;
align-items: center;
gap: 6px;
}
.filter-bar select, .filter-bar input {
font-size: 13px;
padding: 6px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg-body);
color: var(--text-primary);
outline: none;
transition: border-color .15s;
}
.filter-bar select:focus, .filter-bar input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,.12);
}
.filter-bar input[type="text"] { min-width: 220px; }
.filter-divider { width: 1px; height: 24px; background: var(--border); }
.btn-reset {
font-size: 12px;
font-weight: 600;
padding: 6px 14px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--bg-body);
color: var(--text-secondary);
cursor: pointer;
transition: background .15s, color .15s;
}
.btn-reset:hover { background: #e2e8f0; color: var(--text-primary); }
.result-count {
margin-left: auto;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
}
.result-count span { color: var(--text-primary); font-size: 14px; }
/* ── TABLE ── */
.table-wrap {
background: var(--bg-card);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.table-scroll { overflow-x: auto; }
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead th {
background: var(--bg-subheader);
color: #94a3b8;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .7px;
padding: 12px 14px;
text-align: left;
white-space: nowrap;
border-bottom: 2px solid #334155;
position: sticky;
top: 0;
z-index: 1;
cursor: pointer;
user-select: none;
transition: color .15s;
}
thead th:hover { color: #e2e8f0; }
thead th.sorted-asc::after { content: ' ↑'; color: #3b82f6; }
thead th.sorted-desc::after { content: ' ↓'; color: #3b82f6; }
tbody tr {
border-left: 3px solid transparent;
transition: background .1s;
}
tbody tr:nth-child(even) { background: var(--bg-row-alt); }
tbody tr:hover { background: #e0f2fe !important; }
tbody tr.status-automated { border-left-color: var(--color-automated); }
tbody tr.status-partial { border-left-color: var(--color-partial); }
tbody tr.status-manual { border-left-color: var(--color-manual); }
td {
padding: 10px 14px;
color: var(--text-primary);
vertical-align: middle;
border-bottom: 1px solid var(--border);
}
td.num {
color: var(--text-muted);
font-size: 12px;
font-weight: 500;
width: 42px;
text-align: center;
}
td.td-id {
font-family: "SF Mono", "Consolas", "Liberation Mono", monospace;
font-size: 11.5px;
color: #334155;
max-width: 200px;
}
.id-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 190px;
}
td.td-desc {
min-width: 260px;
max-width: 380px;
color: var(--text-primary);
}
td.td-file {
font-family: "SF Mono", "Consolas", monospace;
font-size: 11px;
color: var(--text-muted);
max-width: 200px;
}
.file-text {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── BADGES ── */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .4px;
padding: 2px 9px;
border-radius: 20px;
white-space: nowrap;
}
.badge-automated { background: #dcfce7; color: #15803d; }
.badge-partial { background: #fef3c7; color: #b45309; }
.badge-manual { background: #fee2e2; color: #b91c1c; }
.badge-core { background: #dbeafe; color: #1d4ed8; }
.badge-pbx { background: #ede9fe; color: #6d28d9; }
.badge-check { background: #f1f5f9; color: #475569; }
.badge-write { background: #e0f2fe; color: #0369a1; }
.badge-dot {
width: 6px; height: 6px; border-radius: 50%;
}
.badge-automated .badge-dot { background: #15803d; }
.badge-partial .badge-dot { background: #b45309; }
.badge-manual .badge-dot { background: #b91c1c; }
/* ── COPY BUTTON ── */
.copy-btn {
display: none;
font-size: 10px;
font-weight: 600;
padding: 2px 7px;
border-radius: 4px;
border: 1px solid var(--border);
background: #fff;
color: var(--text-secondary);
cursor: pointer;
margin-left: 4px;
transition: background .1s, color .1s;
vertical-align: middle;
}
.copy-btn:hover { background: #3b82f6; color: #fff; border-color: #3b82f6; }
tbody tr:hover .copy-btn { display: inline; }
.copy-btn.copied { background: #22c55e; color: #fff; border-color: #22c55e; }
/* ── NO RESULTS ── */
.no-results {
padding: 48px;
text-align: center;
color: var(--text-muted);
font-size: 14px;
display: none;
}
.no-results svg { display: block; margin: 0 auto 12px; opacity: .3; }
/* ── FOOTER ── */
footer {
text-align: center;
padding: 24px 32px;
font-size: 12px;
color: var(--text-muted);
border-top: 1px solid var(--border);
margin-top: 40px;
}
footer strong { color: var(--text-secondary); }
</style>
</head>
<body>
<!-- ══════════════════════════════════ HEADER ══════════════════════════════════ -->
<header class="site-header">
<h1>QA Automation Test Plan Report</h1>
<p class="subtitle">PBX / Core Systems</p>
<div class="meta">
<span class="meta-chip"><span class="dot"></span> Generated 2026-05-22</span>
<span class="meta-chip">Playwright + TypeScript + MCP</span>
<span class="meta-chip">86 Test Cases</span>
</div>
</header>
<!-- ══════════════════════════════════ MAIN ══════════════════════════════════ -->
<main>
<!-- SUMMARY CARDS -->
<p class="section-title">Overview</p>
<div class="cards-row">
<div class="card" style="--card-accent:#3b82f6;">
<div class="card-label">Total Tests</div>
<div class="card-number">86</div>
<div class="card-pct">Across 18 groups</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:100%;"></div></div>
</div>
<div class="card" style="--card-accent:#22c55e;">
<div class="card-label">Automated</div>
<div class="card-number">34</div>
<div class="card-pct">39.5% of total</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:39.5%;"></div></div>
</div>
<div class="card" style="--card-accent:#f59e0b;">
<div class="card-label">Partial</div>
<div class="card-number">16</div>
<div class="card-pct">18.6% of total</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:18.6%;"></div></div>
</div>
<div class="card" style="--card-accent:#ef4444;">
<div class="card-label">Manual</div>
<div class="card-number">36</div>
<div class="card-pct">41.9% of total</div>
<div class="card-bar-track"><div class="card-bar-fill" style="width:41.9%;"></div></div>
</div>
</div>
<!-- CHARTS -->
<p class="section-title">Charts</p>
<div class="charts-row">
<!-- Doughnut -->
<div class="chart-card">
<h3>Automation Distribution</h3>
<div class="chart-wrap chart-wrap-doughnut">
<canvas id="chartDoughnut"></canvas>
</div>
<div class="doughnut-legend">
<div class="legend-item"><span class="legend-dot" style="background:#22c55e;"></span> Automated <span class="legend-val">34</span></div>
<div class="legend-item"><span class="legend-dot" style="background:#f59e0b;"></span> Partial <span class="legend-val">16</span></div>
<div class="legend-item"><span class="legend-dot" style="background:#ef4444;"></span> Manual <span class="legend-val">36</span></div>
</div>
</div>
<!-- Horizontal stacked bar -->
<div class="chart-card">
<h3>Automation by Group</h3>
<div class="chart-wrap" style="height:520px; position:relative;">
<canvas id="chartBar"></canvas>
</div>
</div>
</div>
<!-- FILTER BAR -->
<div class="filter-bar">
<div class="filter-group">
<label for="fModule">Module</label>
<select id="fModule">
<option value="">All</option>
<option value="core">Core</option>
<option value="pbx">PBX</option>
</select>
</div>
<div class="filter-group">
<label for="fGroup">Group</label>
<select id="fGroup">
<option value="">All</option>
<option value="core-repository">core-repository</option>
<option value="core-documentation">core-documentation</option>
<option value="core-features">core-features</option>
<option value="core-virtualization">core-virtualization</option>
<option value="core-security">core-security</option>
<option value="core-installation">core-installation</option>
<option value="core-upgrade">core-upgrade</option>
<option value="core-interoperability">core-interoperability</option>
<option value="pbx-configuration">pbx-configuration</option>
<option value="pbx-extensions">pbx-extensions</option>
<option value="pbx-calls">pbx-calls</option>
<option value="pbx-codecs">pbx-codecs</option>
<option value="pbx-calls-security">pbx-calls-security</option>
<option value="pbx-performance">pbx-performance</option>
<option value="pbx-availability">pbx-availability</option>
<option value="pbx-management">pbx-management</option>
<option value="pbx-logs">pbx-logs</option>
<option value="pbx-regression">pbx-regression</option>
</select>
</div>
<div class="filter-group">
<label for="fAction">Action</label>
<select id="fAction">
<option value="">All</option>
<option value="check">check</option>
<option value="write">write</option>
</select>
</div>
<div class="filter-group">
<label for="fStatus">Status</label>
<select id="fStatus">
<option value="">All</option>
<option value="automated">automated</option>
<option value="partial">partial</option>
<option value="manual">manual</option>
</select>
</div>
<div class="filter-divider"></div>
<div class="filter-group">
<label for="fSearch">Search</label>
<input type="text" id="fSearch" placeholder="ID, description, group, file…" />
</div>
<button class="btn-reset" id="btnReset">Clear</button>
<div class="result-count" id="resultCount"><span id="visibleCount">86</span> / 86 tests</div>
</div>
<!-- TABLE -->
<div class="table-wrap">
<div class="table-scroll">
<table id="testTable">
<thead>
<tr>
<th data-col="idx">#</th>
<th data-col="id">Test ID</th>
<th data-col="group">Group</th>
<th data-col="module">Module</th>
<th data-col="action">Action</th>
<th data-col="description">Description</th>
<th data-col="status">Status</th>
<th data-col="file">Spec File</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
<div class="no-results" id="noResults">
<svg width="40" height="40" fill="none" viewBox="0 0 24 24" stroke="currentColor"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
No tests match the current filters.
</div>
</div>
</div>
</main>
<footer>
Generated automatically by <strong>QA Automation Framework</strong> &middot; Playwright + TypeScript + MCP
</footer>
<script>
/* ══════════════════════════════ DATA ══════════════════════════════ */
const TESTS = [
// === CORE-REPOSITORY (6) ===
{ id: "core-repository-check-presence_of_release_notes_and_changelog", group: "core-repository", module: "core", action: "check", description: "Verify Release Notes and Changelog are present in the software repository", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_trouble_shooting_guide", group: "core-repository", module: "core", action: "check", description: "Verify Troubleshooting Guide is present in the repository", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_all_mandatory_core-documentation_files", group: "core-repository", module: "core", action: "check", description: "Verify all mandatory core documentation files are listed in the repository", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_main_software_version_all_hypervisors", group: "core-repository", module: "core", action: "check", description: "Verify main software version packages are available for all supported hypervisors", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_patch_version_and_its_compatibility", group: "core-repository", module: "core", action: "check", description: "Verify patch version packages and compatibility matrices are present", status: "automated", file: "tests/core/repository.spec.ts" },
{ id: "core-repository-check-presence_of_companion_tools_and_supporting_scripts", group: "core-repository", module: "core", action: "check", description: "Verify companion tools and supporting scripts are available in the repository", status: "automated", file: "tests/core/repository.spec.ts" },
// === CORE-DOCUMENTATION (4) ===
{ id: "core-documentation-write-release_notes_feature_changes_and_known_issues", group: "core-documentation", module: "core", action: "write", description: "Extract and record Release Notes, feature changes, and known issues", status: "automated", file: "tests/core/documentation.spec.ts" },
{ id: "core-documentation-check-accuracy_installation_and_configuration_guide", group: "core-documentation", module: "core", action: "check", description: "Verify accuracy of the Installation and Configuration Guide against actual system behaviour", status: "manual", manualReason: "Requires expert human review: QA engineer must follow the guide step-by-step on a clean system", file: "tests/core/documentation.spec.ts" },
{ id: "core-documentation-check-accuracy_trouble_shooting_guide", group: "core-documentation", module: "core", action: "check", description: "Verify the Troubleshooting Guide procedures are accurate and complete", status: "manual", manualReason: "Requires expert human review to validate each troubleshooting scenario on a real system", file: "tests/core/documentation.spec.ts" },
{ id: "core-documentation-write-changed_cli_configuration_commands", group: "core-documentation", module: "core", action: "write", description: "Extract and record changed CLI configuration commands for this release", status: "automated", file: "tests/core/documentation.spec.ts" },
// === CORE-FEATURES (7) ===
{ id: "core-features-write-summary_of_each_new_feature_per_release", group: "core-features", module: "core", action: "write", description: "Extract and record a summary of each new feature included in this release", status: "automated", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-integration_of_new_features_with_existing_call_processing_and_modules", group: "core-features", module: "core", action: "check", description: "Verify new features integrate correctly with existing call processing and system modules", status: "partial", partialNotes: "UI config checks automated; actual call-flow integration requires active telephony endpoints", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-backward_compatibility_of_new_features", group: "core-features", module: "core", action: "check", description: "Verify new features are backward-compatible with existing configurations and older endpoint firmware", status: "partial", partialNotes: "Configuration import checks automated; functional compatibility with older endpoints requires manual testing", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-performance_and_resource_impact_assessment_of_new_features", group: "core-features", module: "core", action: "check", description: "Assess CPU/memory/network resource impact of new features under representative call load", status: "manual", manualReason: "Requires SIPp load generator, performance monitoring infrastructure, and sustained load testing", file: "tests/core/features.spec.ts" },
{ id: "core-features-write-analysis_of_new_security_risks", group: "core-features", module: "core", action: "write", description: "Identify, analyse, and document new security risks introduced by features in this release", status: "manual", manualReason: "Security risk analysis requires expert threat modelling, source code review, and human judgment", file: "tests/core/features.spec.ts" },
{ id: "core-features-write-ui_ux_and_admin_portal_changes_validation", group: "core-features", module: "core", action: "write", description: "Capture and record all UI/UX changes in the admin portal for this release", status: "automated", file: "tests/core/features.spec.ts" },
{ id: "core-features-check-provisioning_changes_for_new_features", group: "core-features", module: "core", action: "check", description: "Verify provisioning settings for new features are accessible in the admin portal", status: "automated", file: "tests/core/features.spec.ts" },
// === CORE-VIRTUALIZATION (8) ===
{ id: "core-virtualization-write-supported_hypervisors_deployment", group: "core-virtualization", module: "core", action: "write", description: "Record the list of supported hypervisors and deployment methods from the download portal", status: "automated", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-guest_agent_integration", group: "core-virtualization", module: "core", action: "write", description: "Record guest agent integration status for each supported hypervisor", status: "manual", manualReason: "Requires direct access to hypervisor console (vCenter, Hyper-V Manager, virsh)", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-resource_allocation_vcpu_vram_vdisk", group: "core-virtualization", module: "core", action: "write", description: "Record vCPU, vRAM, and vDisk resource allocation for deployment templates", status: "manual", manualReason: "Hypervisor management plane access required; not exposed in PBX admin portal", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-virtual_networking_configuration", group: "core-virtualization", module: "core", action: "write", description: "Record virtual networking configuration: virtual switches, VLANs, NIC assignments", status: "manual", manualReason: "Requires access to hypervisor networking configuration and virtual switch management", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-check-live_migration_and_active_call_preservation", group: "core-virtualization", module: "core", action: "check", description: "Verify VM live migration preserves active calls without interruption", status: "manual", manualReason: "Requires HA infrastructure with 2+ hypervisor hosts and active SIP call sessions during migration", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-check-snapshot_backup_restore_and_consistency", group: "core-virtualization", module: "core", action: "check", description: "Verify VM snapshot and restore operations maintain full system consistency", status: "partial", partialNotes: "Post-restore consistency checks via admin portal automated; snapshot/restore operation needs hypervisor access", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-check-hypervisor_level_high_availability", group: "core-virtualization", module: "core", action: "check", description: "Verify hypervisor-level HA restarts the PBX VM after a host failure", status: "manual", manualReason: "Requires physical HA cluster with multiple hypervisor hosts; host failure must be simulated", file: "tests/core/virtualization.spec.ts" },
{ id: "core-virtualization-write-storage_thin_vs_thick_provisioning", group: "core-virtualization", module: "core", action: "write", description: "Record storage provisioning type and performance implications for each hypervisor", status: "manual", manualReason: "Storage details only visible in hypervisor storage management interface", file: "tests/core/virtualization.spec.ts" },
// === CORE-SECURITY (11) ===
{ id: "core-security-write-strong_password_policy", group: "core-security", module: "core", action: "write", description: "Navigate to security settings and record the current password policy configuration", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-sip_tls_and_srtp_encryption_enabled_by_default", group: "core-security", module: "core", action: "check", description: "Verify SIP TLS and SRTP media encryption are enabled by default", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-ip_access_control_lists", group: "core-security", module: "core", action: "check", description: "Verify IP Access Control Lists are properly configured and enforced", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-unnecessary_services_and_ports_are_disabled", group: "core-security", module: "core", action: "check", description: "Verify unnecessary services and network ports are disabled", status: "partial", partialNotes: "Service status via admin portal automated; port scanning requires nmap from external host", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-security_event_logging_and_audit_trail_is_active", group: "core-security", module: "core", action: "check", description: "Verify security event logging and audit trail are active and recording events", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-auto_enrollment_certificate_management_process", group: "core-security", module: "core", action: "check", description: "Verify auto-enrollment certificate management (ACME/Let's Encrypt) is configured and functional", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-known_vulnerable_dependencies_in_release_artifacts", group: "core-security", module: "core", action: "check", description: "Verify release artifacts do not contain known vulnerable dependencies (CVE scan)", status: "partial", partialNotes: "Vulnerability report page in portal automated; actual CVE scanning requires Trivy/OWASP-DC in CI", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-previous_security_findings_have_been_remediated_before_release", group: "core-security", module: "core", action: "check", description: "Verify all previously identified security findings are remediated before release", status: "manual", manualReason: "Requires access to security issue tracker and cross-referencing CVEs with the release changelog", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-obtain_formal_security_team_sign_off_for_the_new_version", group: "core-security", module: "core", action: "check", description: "Obtain formal written sign-off from the Security team approving the release", status: "manual", manualReason: "Governance gate requiring human decision and formal sign-off document; cannot be automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-verify_role_based_access_control_and_least_privilege_principle", group: "core-security", module: "core", action: "check", description: "Verify RBAC roles are correctly defined and least-privilege principle is enforced", status: "automated", file: "tests/core/security.spec.ts" },
{ id: "core-security-check-encryption_of_sensitive_data_at_rest_and_in_transit", group: "core-security", module: "core", action: "check", description: "Verify sensitive data is encrypted at rest (DB) and in transit (HTTPS/TLS)", status: "partial", partialNotes: "HTTPS/TLS configuration verified via portal; DB-level at-rest encryption requires SSH/CLI access", file: "tests/core/security.spec.ts" },
// === CORE-INSTALLATION (4) ===
{ id: "core-installation-check-fresh_installation", group: "core-installation", module: "core", action: "check", description: "Verify complete fresh installation from scratch succeeds on each supported hypervisor", status: "manual", manualReason: "Requires provisioning a clean VM, mounting ISO/OVA, and running the installation wizard", file: "tests/core/installation.spec.ts" },
{ id: "core-installation-check-post_install_service_status_and_port_verification", group: "core-installation", module: "core", action: "check", description: "Verify all required services are running and expected ports are open after installation", status: "partial", partialNotes: "Service status via admin portal automated; TCP port verification requires nmap from external host", file: "tests/core/installation.spec.ts" },
{ id: "core-installation-check-initial_configuration", group: "core-installation", module: "core", action: "check", description: "Verify the initial configuration wizard completes successfully", status: "automated", file: "tests/core/installation.spec.ts" },
{ id: "core-installation-check-license_activation", group: "core-installation", module: "core", action: "check", description: "Verify license activation completes successfully and licensed features are unlocked", status: "automated", file: "tests/core/installation.spec.ts" },
// === CORE-UPGRADE (6) ===
{ id: "core-upgrade-check-in_place_upgrade_from_previous_major_version", group: "core-upgrade", module: "core", action: "check", description: "Verify in-place upgrade from the previous major version completes successfully without data loss", status: "manual", manualReason: "Requires a running instance of the previous version and dedicated upgrade test environment", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-configuration_and_database_migration_validation", group: "core-upgrade", module: "core", action: "check", description: "Verify configuration objects and database content migrate correctly after upgrade", status: "partial", partialNotes: "UI configuration checks automated; DB schema integrity validation requires direct database access", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-schema_upgrade_and_data_integrity_check", group: "core-upgrade", module: "core", action: "check", description: "Verify database schema is correctly upgraded and all data maintains integrity", status: "manual", manualReason: "Requires direct database access (psql/mysql) and schema comparison tooling", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-rollback_procedure_execution_and_verification", group: "core-upgrade", module: "core", action: "check", description: "Verify the rollback procedure successfully reverts the system to the previous version", status: "manual", manualReason: "Requires VM snapshot revert or package downgrade; needs physical/hypervisor-level access", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-post_upgrade_regression_of_core_features", group: "core-upgrade", module: "core", action: "check", description: "Run automated regression checks on core portal features after upgrade", status: "automated", file: "tests/core/upgrade.spec.ts" },
{ id: "core-upgrade-check-zero_downtime_upgrade_where_supported", group: "core-upgrade", module: "core", action: "check", description: "Verify zero-downtime upgrade keeps the service available throughout the upgrade process", status: "manual", manualReason: "Requires active call sessions monitored during the upgrade window; calls cannot be managed by Playwright", file: "tests/core/upgrade.spec.ts" },
// === CORE-INTEROPERABILITY (2) ===
{ id: "core-interoperability-check-addm", group: "core-interoperability", module: "core", action: "check", description: "Verify ADDM integration is configured and reporting discovery data correctly", status: "automated", file: "tests/core/interoperability.spec.ts" },
{ id: "core-interoperability-check-siem", group: "core-interoperability", module: "core", action: "check", description: "Verify SIEM integration is configured and forwarding security events", status: "automated", file: "tests/core/interoperability.spec.ts" },
// === PBX-CONFIGURATION (4) ===
{ id: "pbx-configuration-check-initial_system_setup_and_network_settings", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify initial PBX system setup and network settings are correctly configured", status: "automated", file: "tests/pbx/configuration.spec.ts" },
{ id: "pbx-configuration-check-dialplan_route_and_outbound_rule_creation", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify creation and validation of dial plan rules and outbound routes", status: "automated", file: "tests/pbx/configuration.spec.ts" },
{ id: "pbx-configuration-check-backup_restore_full_system_configuration", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify full system configuration backup and restore operations work correctly", status: "automated", file: "tests/pbx/configuration.spec.ts" },
{ id: "pbx-configuration-check-bulk_csv_import_export_of_settings", group: "pbx-configuration", module: "pbx", action: "check", description: "Verify bulk CSV import and export of settings", status: "automated", file: "tests/pbx/configuration.spec.ts" },
// === PBX-EXTENSIONS (3) ===
{ id: "pbx-extensions-check-create_modify_delete_sip_extensions", group: "pbx-extensions", module: "pbx", action: "check", description: "Verify full CRUD lifecycle for SIP extensions in the admin portal", status: "automated", file: "tests/pbx/extensions.spec.ts" },
{ id: "pbx-extensions-check-extension_registration_with_deskphones_softphones", group: "pbx-extensions", module: "pbx", action: "check", description: "Verify SIP extension registers successfully from a desk phone and a softphone client", status: "manual", manualReason: "Requires physical SIP desk phone (Yealink, Polycom) or softphone (Zoiper, MicroSIP)", file: "tests/pbx/extensions.spec.ts" },
{ id: "pbx-extensions-check-device_provisioning", group: "pbx-extensions", module: "pbx", action: "check", description: "Verify automatic device provisioning: profile generation and server URL accessible", status: "partial", partialNotes: "Provisioning profile creation via admin portal automated; actual device provisioning requires physical phone", file: "tests/pbx/extensions.spec.ts" },
// === PBX-CALLS (7) ===
{ id: "pbx-calls-check-internal_extension_to_extension_audio_call", group: "pbx-calls", module: "pbx", action: "check", description: "Verify internal extension-to-extension audio call connects with bi-directional audio", status: "manual", manualReason: "Requires two active SIP endpoints and human verification of two-way audio quality", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-inbound_call_from_sip_trunk_pstn", group: "pbx-calls", module: "pbx", action: "check", description: "Verify inbound call from a SIP trunk/PSTN routes correctly to the configured destination", status: "manual", manualReason: "Requires live SIP trunk connectivity to PSTN and a real DID number", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-outbound_call_to_sip_trunk_pstn_with_caller_id", group: "pbx-calls", module: "pbx", action: "check", description: "Verify outbound call via SIP trunk presents the correct Caller ID on the receiving end", status: "manual", manualReason: "Requires live SIP trunk and verification of Caller ID at receiving PSTN party", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-caller_id_presentation_restriction_and_pai", group: "pbx-calls", module: "pbx", action: "check", description: "Verify Caller ID presentation, Anonymous restriction (CLIR), and P-Asserted-Identity header", status: "manual", manualReason: "Requires active call sessions and SIP packet capture to verify PAI header values", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-call_hold_resume_and_music_on_hold", group: "pbx-calls", module: "pbx", action: "check", description: "Verify call hold, resume, and Music on Hold playback work correctly", status: "manual", manualReason: "Requires an active two-party call; MoH requires audio monitoring with real SIP phone hardware", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-call_waiting_notification_and_switching", group: "pbx-calls", module: "pbx", action: "check", description: "Verify call waiting notification and switching between active and waiting calls", status: "manual", manualReason: "Requires concurrent call sessions on the same extension with physical/softphone capable of call-waiting", file: "tests/pbx/calls.spec.ts" },
{ id: "pbx-calls-check-three_way_conference_and_multi_party_bridge", group: "pbx-calls", module: "pbx", action: "check", description: "Verify three-way conference calling and multi-party conference bridge functionality", status: "manual", manualReason: "Requires three or more active SIP endpoints and audio verification by human listeners", file: "tests/pbx/calls.spec.ts" },
// === PBX-CODECS (3) ===
{ id: "pbx-codecs-check-codec_negotiation_g711_g729_opus_g722", group: "pbx-codecs", module: "pbx", action: "check", description: "Verify successful codec negotiation for G.711, G.729, Opus, and G.722", status: "manual", manualReason: "Requires SIP call traffic with codec-constrained endpoints and Wireshark packet capture to verify SDP negotiation", file: "tests/pbx/codecs.spec.ts" },
{ id: "pbx-codecs-check-dtmf_transmission_rfc2833_inband_sip_info", group: "pbx-codecs", module: "pbx", action: "check", description: "Verify DTMF digit transmission via RFC2833, in-band audio, and SIP INFO methods", status: "manual", manualReason: "Requires active call sessions with DTMF generation, packet capture, and IVR system for verification", file: "tests/pbx/codecs.spec.ts" },
{ id: "pbx-codecs-check-t38_fax_passthrough_and_error_correction", group: "pbx-codecs", module: "pbx", action: "check", description: "Verify T.38 fax passthrough and error correction (ECM) work correctly", status: "manual", manualReason: "Requires fax hardware or fax emulator (HylaFAX) and T.38 stack support", file: "tests/pbx/codecs.spec.ts" },
// === PBX-CALLS-SECURITY (3) ===
{ id: "pbx-calls-security-check-srtp_sdes_or_dtls_media_encryption", group: "pbx-calls-security", module: "pbx", action: "check", description: "Verify SRTP with SDES or DTLS-SRTP is enabled for media encryption on calls", status: "partial", partialNotes: "Encryption config verified via admin portal; confirming live RTP is encrypted requires Wireshark during active call", file: "tests/pbx/calls-security.spec.ts" },
{ id: "pbx-calls-security-check-acl_ip_whitelisting_and_rate_limiting", group: "pbx-calls-security", module: "pbx", action: "check", description: "Verify ACL IP whitelisting rules and SIP rate limiting are configured and enforced", status: "partial", partialNotes: "ACL/rate-limit config via portal automated; functional enforcement testing requires network-level testing from non-whitelisted IP", file: "tests/pbx/calls-security.spec.ts" },
{ id: "pbx-calls-security-check-toll_fraud_prevention_rules_and_alerting", group: "pbx-calls-security", module: "pbx", action: "check", description: "Verify toll fraud prevention rules and alert notifications are configured", status: "automated", file: "tests/pbx/calls-security.spec.ts" },
// === PBX-PERFORMANCE (3) ===
{ id: "pbx-performance-check-cpu_memory_disk_io_utilization_under_sustained_load", group: "pbx-performance", module: "pbx", action: "check", description: "Verify CPU, memory, and disk I/O remain within limits under sustained call load", status: "manual", manualReason: "Requires SIPp load generator, server-side performance monitoring, and dedicated test environment", file: "tests/pbx/performance.spec.ts" },
{ id: "pbx-performance-check-long_duration_stability_72h_test", group: "pbx-performance", module: "pbx", action: "check", description: "Verify system stability under continuous call load over a 72-hour period", status: "manual", manualReason: "72-hour test requires dedicated environment, SIPp continuous load, and automated monitoring with alerting", file: "tests/pbx/performance.spec.ts" },
{ id: "pbx-performance-check-memory_leak_and_resource_cleanup_detection", group: "pbx-performance", module: "pbx", action: "check", description: "Detect memory leaks and verify proper resource cleanup over extended operation", status: "manual", manualReason: "Requires extended monitoring with active call load and memory profiling tools (Valgrind, heap dumps)", file: "tests/pbx/performance.spec.ts" },
// === PBX-AVAILABILITY (4) ===
{ id: "pbx-availability-check-active_passive_or_active_active_failover", group: "pbx-availability", module: "pbx", action: "check", description: "Verify Active-Passive or Active-Active failover transitions correctly on node failure", status: "manual", manualReason: "Requires HA cluster with 2+ nodes; failover must be triggered by stopping the active node", file: "tests/pbx/availability.spec.ts" },
{ id: "pbx-availability-check-database_replication_and_sync_integrity", group: "pbx-availability", module: "pbx", action: "check", description: "Verify database replication between HA nodes maintains consistency and acceptable lag", status: "manual", manualReason: "Requires HA database cluster; verifying replication lag needs direct database access", file: "tests/pbx/availability.spec.ts" },
{ id: "pbx-availability-check-automatic_failover_and_call_preservation", group: "pbx-availability", module: "pbx", action: "check", description: "Verify automatic failover preserves active calls (no drops, no audio interruption > 200ms)", status: "manual", manualReason: "Requires active call sessions during failover and simultaneous call-state monitoring", file: "tests/pbx/availability.spec.ts" },
{ id: "pbx-availability-check-geographic_redundancy_and_geo_distribution", group: "pbx-availability", module: "pbx", action: "check", description: "Verify geo-distributed deployment handles site failover without service interruption", status: "manual", manualReason: "Requires multi-site infrastructure across geographic locations or WAN simulation", file: "tests/pbx/availability.spec.ts" },
// === PBX-MANAGEMENT (2) ===
{ id: "pbx-management-check-cli_management", group: "pbx-management", module: "pbx", action: "check", description: "Verify CLI management interface is accessible and key management commands execute correctly", status: "partial", partialNotes: "Web console (if present) verified via Playwright; full CLI coverage requires SSH automation integrated in CI pipeline", file: "tests/pbx/management.spec.ts" },
{ id: "pbx-management-check-gui_management", group: "pbx-management", module: "pbx", action: "check", description: "Verify all main admin portal sections load correctly and management actions are functional", status: "automated", file: "tests/pbx/management.spec.ts" },
// === PBX-LOGS (5) ===
{ id: "pbx-logs-check-cdr_generation_accuracy_completeness_and_export", group: "pbx-logs", module: "pbx", action: "check", description: "Verify CDR records contain all required fields and can be exported to CSV/PDF", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-real_time_active_call_and_registration_monitoring", group: "pbx-logs", module: "pbx", action: "check", description: "Verify real-time monitoring dashboard shows active calls and SIP registrations", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-detailed_logging_levels_rotation_and_search", group: "pbx-logs", module: "pbx", action: "check", description: "Verify logging levels, log rotation, and log search/filter functionality", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-threshold_alarms_cpu_channels_disk_memory", group: "pbx-logs", module: "pbx", action: "check", description: "Verify threshold-based alarms for CPU, active channels, disk, and memory usage", status: "automated", file: "tests/pbx/logs.spec.ts" },
{ id: "pbx-logs-check-call_quality_metrics_mos_jitter_loss_per_call", group: "pbx-logs", module: "pbx", action: "check", description: "Verify per-call quality metrics (MOS score, jitter, packet loss) are collected and displayed", status: "partial", partialNotes: "QoS metrics display UI automated; actual metric generation requires active call sessions with network impairment", file: "tests/pbx/logs.spec.ts" },
// === PBX-REGRESSION (4) ===
{ id: "pbx-regression-check-core_call_features_after_any_code_change", group: "pbx-regression", module: "pbx", action: "check", description: "Run regression checks on core features after any code change to detect regressions", status: "partial", partialNotes: "Admin portal regression automated; call-flow regression (inbound/outbound/hold/transfer) requires SIP endpoints", file: "tests/pbx/regression.spec.ts" },
{ id: "pbx-regression-check-previously_fixed_bugs_and_edge_cases_reverification", group: "pbx-regression", module: "pbx", action: "check", description: "Verify all previously fixed bugs and known edge cases have not regressed", status: "partial", partialNotes: "UI bug regressions automated; telephony-layer bug regressions require SIP client or SSH verification", file: "tests/pbx/regression.spec.ts" },
{ id: "pbx-regression-check-performance_baselines_comparison_against_previous_version", group: "pbx-regression", module: "pbx", action: "check", description: "Compare key performance metrics against baselines from the previous version", status: "manual", manualReason: "Requires SIPp load tests and statistical comparison of results against previous version baseline", file: "tests/pbx/regression.spec.ts" },
{ id: "pbx-regression-check-security_hardening_and_vulnerability_scan_baseline", group: "pbx-regression", module: "pbx", action: "check", description: "Verify security hardening settings and compare vulnerability scan baseline against previous version", status: "partial", partialNotes: "Security config checks via portal automated; full CVE scan baseline requires Trivy/OpenVAS in CI pipeline", file: "tests/pbx/regression.spec.ts" },
];
/* ══════════════════════════════ STATE ══════════════════════════════ */
let sortCol = null;
let sortDir = 1; // 1 = asc, -1 = desc
/* ══════════════════════════════ CHARTS ══════════════════════════════ */
// Doughnut
new Chart(document.getElementById('chartDoughnut'), {
type: 'doughnut',
data: {
labels: ['Automated', 'Partial', 'Manual'],
datasets: [{
data: [34, 16, 36],
backgroundColor: ['#22c55e', '#f59e0b', '#ef4444'],
borderColor: ['#16a34a', '#d97706', '#dc2626'],
borderWidth: 2,
hoverOffset: 6
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => {
const val = ctx.raw;
const pct = ((val / 86) * 100).toFixed(1);
return ` ${ctx.label}: ${val} (${pct}%)`;
}
}
}
},
cutout: '68%'
}
});
// Compute group counts
const GROUP_ORDER = [
'core-repository','core-documentation','core-features','core-virtualization',
'core-security','core-installation','core-upgrade','core-interoperability',
'pbx-configuration','pbx-extensions','pbx-calls','pbx-codecs',
'pbx-calls-security','pbx-performance','pbx-availability','pbx-management',
'pbx-logs','pbx-regression'
];
const groupCounts = {};
GROUP_ORDER.forEach(g => { groupCounts[g] = { automated: 0, partial: 0, manual: 0 }; });
TESTS.forEach(t => { if (groupCounts[t.group]) groupCounts[t.group][t.status]++; });
const barLabels = GROUP_ORDER.map(g => g);
const barAuto = GROUP_ORDER.map(g => groupCounts[g].automated);
const barPartial = GROUP_ORDER.map(g => groupCounts[g].partial);
const barManual = GROUP_ORDER.map(g => groupCounts[g].manual);
new Chart(document.getElementById('chartBar'), {
type: 'bar',
data: {
labels: barLabels,
datasets: [
{ label: 'Automated', data: barAuto, backgroundColor: '#22c55e', borderRadius: 2 },
{ label: 'Partial', data: barPartial, backgroundColor: '#f59e0b', borderRadius: 2 },
{ label: 'Manual', data: barManual, backgroundColor: '#ef4444', borderRadius: 2 }
]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
labels: {
font: { size: 11 },
color: '#475569',
boxWidth: 12,
padding: 16
}
},
tooltip: {
mode: 'index',
intersect: false
}
},
scales: {
x: {
stacked: true,
grid: { color: '#f1f5f9' },
ticks: {
font: { size: 11 },
color: '#94a3b8',
stepSize: 1
}
},
y: {
stacked: true,
grid: { display: false },
ticks: {
font: { size: 11, family: 'monospace' },
color: '#475569'
}
}
}
}
});
/* ══════════════════════════════ TABLE RENDERING ══════════════════════════════ */
function escHtml(s) {
return String(s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;');
}
function renderRows(list) {
const tbody = document.getElementById('tableBody');
const noRes = document.getElementById('noResults');
const countEl = document.getElementById('visibleCount');
if (list.length === 0) {
tbody.innerHTML = '';
noRes.style.display = 'block';
countEl.textContent = '0';
return;
}
noRes.style.display = 'none';
countEl.textContent = list.length;
const rows = list.map((t, i) => {
const statusBadge = `<span class="badge badge-${t.status}"><span class="badge-dot"></span>${t.status}</span>`;
const moduleBadge = `<span class="badge badge-${t.module}">${t.module.toUpperCase()}</span>`;
const actionBadge = `<span class="badge badge-${t.action}">${t.action}</span>`;
const shortId = t.id.length > 40 ? t.id.slice(0, 38) + '…' : t.id;
return `<tr class="status-${t.status}" data-id="${escHtml(t.id)}">
<td class="num">${i + 1}</td>
<td class="td-id">
<span class="id-text" title="${escHtml(t.id)}">${escHtml(shortId)}</span>
<button class="copy-btn" onclick="copyId(this,'${escHtml(t.id)}')" title="Copy test ID">Copy</button>
</td>
<td>${escHtml(t.group)}</td>
<td>${moduleBadge}</td>
<td>${actionBadge}</td>
<td class="td-desc">${escHtml(t.description)}</td>
<td>${statusBadge}</td>
<td class="td-file"><span class="file-text" title="${escHtml(t.file)}">${escHtml(t.file)}</span></td>
</tr>`;
});
tbody.innerHTML = rows.join('');
}
function getFiltered() {
const mod = document.getElementById('fModule').value;
const grp = document.getElementById('fGroup').value;
const act = document.getElementById('fAction').value;
const stat = document.getElementById('fStatus').value;
const search = document.getElementById('fSearch').value.trim().toLowerCase();
let list = TESTS.filter(t => {
if (mod && t.module !== mod) return false;
if (grp && t.group !== grp) return false;
if (act && t.action !== act) return false;
if (stat && t.status !== stat) return false;
if (search) {
const hay = (t.id + ' ' + t.description + ' ' + t.group + ' ' + t.file).toLowerCase();
if (!hay.includes(search)) return false;
}
return true;
});
if (sortCol) {
list = [...list].sort((a, b) => {
let va = a[sortCol] || '';
let vb = b[sortCol] || '';
if (sortCol === 'idx') { va = TESTS.indexOf(a); vb = TESTS.indexOf(b); }
if (va < vb) return -1 * sortDir;
if (va > vb) return 1 * sortDir;
return 0;
});
}
return list;
}
function refresh() { renderRows(getFiltered()); }
/* ══════════════════════════════ EVENTS ══════════════════════════════ */
['fModule','fGroup','fAction','fStatus','fSearch'].forEach(id => {
document.getElementById(id).addEventListener('input', refresh);
});
document.getElementById('btnReset').addEventListener('click', () => {
document.getElementById('fModule').value = '';
document.getElementById('fGroup').value = '';
document.getElementById('fAction').value = '';
document.getElementById('fStatus').value = '';
document.getElementById('fSearch').value = '';
sortCol = null;
sortDir = 1;
document.querySelectorAll('thead th').forEach(th => th.classList.remove('sorted-asc','sorted-desc'));
refresh();
});
// Sorting
document.querySelectorAll('thead th').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.col;
if (sortCol === col) {
sortDir *= -1;
} else {
sortCol = col;
sortDir = 1;
}
document.querySelectorAll('thead th').forEach(h => h.classList.remove('sorted-asc','sorted-desc'));
th.classList.add(sortDir === 1 ? 'sorted-asc' : 'sorted-desc');
refresh();
});
});
/* ══════════════════════════════ COPY ID ══════════════════════════════ */
function copyId(btn, id) {
navigator.clipboard.writeText(id).then(() => {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Copy';
btn.classList.remove('copied');
}, 1800);
}).catch(() => {
// fallback for non-secure contexts
const ta = document.createElement('textarea');
ta.value = id;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Copy';
btn.classList.remove('copied');
}, 1800);
});
}
/* ══════════════════════════════ INIT ══════════════════════════════ */
refresh();
</script>
</body>
</html>