209 lines
8.1 KiB
HTML
209 lines
8.1 KiB
HTML
<div class="border-b border-border bg-card/90 px-4 py-3">
|
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-semibold text-foreground">Debug Console</span>
|
|
@if (activeTab() === 'logs') {
|
|
<span class="rounded-full bg-secondary px-2 py-0.5 text-[11px] font-medium text-muted-foreground">{{ visibleCount() }} visible</span>
|
|
} @else {
|
|
<span class="rounded-full bg-secondary px-2 py-0.5 text-[11px] font-medium text-muted-foreground"
|
|
>{{ networkSummary().clientCount }} clients · {{ networkSummary().peerConnectionCount }} links</span
|
|
>
|
|
}
|
|
</div>
|
|
<p class="mt-1 text-xs text-muted-foreground">
|
|
{{
|
|
activeTab() === 'logs'
|
|
? 'Search logs, filter by level or source, and inspect timestamps inline.'
|
|
: 'Visualize signaling, peer links, typing, speaking, streaming, and grouped traffic directly from captured debug data.'
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<button
|
|
type="button"
|
|
(click)="toggleDetached()"
|
|
class="inline-flex items-center gap-1.5 rounded-lg bg-secondary px-2.5 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
|
>
|
|
{{ getDetachLabel() }}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
(click)="toggleAutoScroll()"
|
|
class="inline-flex items-center gap-1.5 rounded-lg bg-secondary px-2.5 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
|
[attr.aria-pressed]="autoScroll()"
|
|
>
|
|
<ng-icon
|
|
[name]="autoScroll() ? 'lucidePause' : 'lucidePlay'"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
{{ autoScroll() ? 'Pause auto-scroll' : 'Resume auto-scroll' }}
|
|
</button>
|
|
|
|
<!-- Export dropdown -->
|
|
<div
|
|
class="relative"
|
|
data-export-menu
|
|
>
|
|
<button
|
|
type="button"
|
|
(click)="toggleExportMenu()"
|
|
class="inline-flex items-center gap-1.5 rounded-lg bg-secondary px-2.5 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
|
[attr.aria-expanded]="exportMenuOpen()"
|
|
aria-haspopup="true"
|
|
title="Export logs"
|
|
>
|
|
<ng-icon
|
|
name="lucideDownload"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
Export
|
|
</button>
|
|
|
|
@if (exportMenuOpen()) {
|
|
<div class="absolute right-0 top-full z-10 mt-1 min-w-[11rem] rounded-lg border border-border bg-card p-1 shadow-xl">
|
|
@if (activeTab() === 'logs') {
|
|
<p class="px-2.5 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Logs</p>
|
|
<button
|
|
type="button"
|
|
(click)="exportLogs('csv')"
|
|
class="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Export as CSV
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="exportLogs('txt')"
|
|
class="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Export as TXT
|
|
</button>
|
|
} @else {
|
|
<p class="px-2.5 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">Network</p>
|
|
<button
|
|
type="button"
|
|
(click)="exportNetwork('csv')"
|
|
class="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Export as CSV
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="exportNetwork('txt')"
|
|
class="flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-xs text-foreground transition-colors hover:bg-secondary"
|
|
>
|
|
Export as TXT
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
(click)="clear()"
|
|
class="inline-flex items-center gap-1.5 rounded-lg bg-secondary px-2.5 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
|
>
|
|
<ng-icon
|
|
name="lucideTrash2"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
Clear
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
(click)="close()"
|
|
class="inline-flex items-center gap-1.5 rounded-lg bg-secondary px-2.5 py-2 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
|
>
|
|
<ng-icon
|
|
name="lucideX"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 flex flex-wrap items-center gap-2">
|
|
@for (tab of tabs; track tab) {
|
|
<button
|
|
type="button"
|
|
(click)="setActiveTab(tab)"
|
|
[class]="getTabButtonClass(tab)"
|
|
[attr.aria-pressed]="activeTab() === tab"
|
|
>
|
|
{{ getTabLabel(tab) }}
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
@if (activeTab() === 'logs') {
|
|
<div class="mt-3 grid gap-3 xl:grid-cols-[minmax(0,1fr)_12rem]">
|
|
<label class="relative block">
|
|
<span class="sr-only">Search logs</span>
|
|
<ng-icon
|
|
name="lucideSearch"
|
|
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
|
/>
|
|
<input
|
|
type="search"
|
|
class="w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
placeholder="Search messages, payloads, timestamps, and sources"
|
|
[value]="searchTerm()"
|
|
(input)="onSearchInput($event)"
|
|
/>
|
|
</label>
|
|
|
|
<label class="relative block">
|
|
<span class="sr-only">Filter by source</span>
|
|
<select
|
|
class="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
|
[value]="selectedSource()"
|
|
(change)="onSourceChange($event)"
|
|
>
|
|
<option value="all">All sources</option>
|
|
@for (source of sourceOptions(); track source) {
|
|
<option [value]="source">{{ source }}</option>
|
|
}
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mt-3 flex flex-wrap items-center gap-2">
|
|
<div class="mr-1 inline-flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
<ng-icon
|
|
name="lucideFilter"
|
|
class="h-3.5 w-3.5"
|
|
/>
|
|
Levels
|
|
</div>
|
|
|
|
@for (level of levels; track level) {
|
|
<button
|
|
type="button"
|
|
(click)="toggleLevel(level)"
|
|
[class]="getLevelButtonClass(level)"
|
|
[attr.aria-pressed]="levelState()[level]"
|
|
>
|
|
{{ getLevelLabel(level) }}
|
|
<span class="ml-1 text-[11px] opacity-80">{{ levelCounts()[level] }}</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
} @else {
|
|
<div class="mt-3 rounded-xl border border-border/80 bg-background/70 px-3 py-3 text-xs text-muted-foreground">
|
|
<p>Traffic is grouped by edge and message type to keep signaling, voice-state, and screen-state chatter readable.</p>
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2 text-[11px]">
|
|
<span class="rounded-full border border-border bg-secondary/70 px-2 py-0.5">{{ networkSummary().typingCount }} typing</span>
|
|
<span class="rounded-full border border-border bg-secondary/70 px-2 py-0.5">{{ networkSummary().speakingCount }} speaking</span>
|
|
<span class="rounded-full border border-border bg-secondary/70 px-2 py-0.5">{{ networkSummary().streamingCount }} streaming</span>
|
|
<span class="rounded-full border border-border bg-secondary/70 px-2 py-0.5">{{ networkSummary().membershipCount }} memberships</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|