mirror of
https://github.com/smittix/intercept.git
synced 2026-07-22 00:08:10 -07:00
Refactor timeline as reusable ActivityTimeline component
- Extract signal-timeline into configurable activity-timeline.js - Add visual modes: compact, enriched, summary - Create data adapters for RF, Bluetooth, WiFi normalization - Integrate timeline into Listening Post, Bluetooth, WiFi modes - Preserve backward compatibility for existing TSCM code - Add mode-specific configuration presets via adapters Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,696 @@
|
|||||||
|
/**
|
||||||
|
* Activity Timeline Component
|
||||||
|
* Reusable, configuration-driven timeline visualization
|
||||||
|
* Supports visual modes: compact, enriched, summary
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
CSS VARIABLES (with fallbacks)
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline {
|
||||||
|
--timeline-bg: var(--bg-card, #1a1a1a);
|
||||||
|
--timeline-border: var(--border-color, #333);
|
||||||
|
--timeline-bg-secondary: var(--bg-secondary, #252525);
|
||||||
|
--timeline-bg-elevated: var(--bg-elevated, #2a2a2a);
|
||||||
|
--timeline-text-primary: var(--text-primary, #fff);
|
||||||
|
--timeline-text-secondary: var(--text-secondary, #888);
|
||||||
|
--timeline-text-dim: var(--text-dim, #666);
|
||||||
|
--timeline-accent: var(--accent-cyan, #4a9eff);
|
||||||
|
--timeline-status-new: var(--signal-new, #3b82f6);
|
||||||
|
--timeline-status-baseline: var(--signal-baseline, #6b7280);
|
||||||
|
--timeline-status-burst: var(--signal-burst, #f59e0b);
|
||||||
|
--timeline-status-flagged: var(--signal-emergency, #ef4444);
|
||||||
|
--timeline-status-gone: var(--text-dim, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TIMELINE CONTAINER
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline {
|
||||||
|
background: var(--timeline-bg);
|
||||||
|
border: 1px solid var(--timeline-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline.collapsed .activity-timeline-body {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline.collapsed .activity-timeline-header {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline.collapsed .activity-timeline-collapse-icon {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
HEADER
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-header:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-collapse-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--timeline-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-header-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-header-stat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-header-stat .stat-value {
|
||||||
|
color: var(--timeline-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
BODY
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-body {
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
|
border-top: 1px solid var(--timeline-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
CONTROLS
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-btn {
|
||||||
|
background: var(--timeline-bg-secondary);
|
||||||
|
border: 1px solid var(--timeline-border);
|
||||||
|
color: var(--timeline-text-secondary);
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-btn:hover {
|
||||||
|
background: var(--timeline-bg-elevated);
|
||||||
|
color: var(--timeline-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-btn.active {
|
||||||
|
background: var(--timeline-accent);
|
||||||
|
color: #000;
|
||||||
|
border-color: var(--timeline-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-window {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-window-select {
|
||||||
|
background: var(--timeline-bg-secondary);
|
||||||
|
border: 1px solid var(--timeline-border);
|
||||||
|
color: var(--timeline-text-primary);
|
||||||
|
font-size: 9px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TIME AXIS
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-axis {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 50px 0 140px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-axis-label {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-axis-label::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: 50%;
|
||||||
|
width: 1px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--timeline-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
LANES CONTAINER
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-lanes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lanes::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lanes::-webkit-scrollbar-track {
|
||||||
|
background: var(--timeline-bg-secondary);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lanes::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--timeline-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lanes::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--timeline-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
INDIVIDUAL LANE
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-lane {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
min-height: 32px;
|
||||||
|
background: var(--timeline-bg-secondary);
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lane:hover {
|
||||||
|
background: var(--timeline-bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lane.expanded {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lane.baseline {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lane.baseline:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status indicator strip */
|
||||||
|
.activity-timeline-status {
|
||||||
|
width: 4px;
|
||||||
|
min-width: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-status[data-status="new"] {
|
||||||
|
background: var(--timeline-status-new);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-status[data-status="baseline"] {
|
||||||
|
background: var(--timeline-status-baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-status[data-status="burst"] {
|
||||||
|
background: var(--timeline-status-burst);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-status[data-status="flagged"] {
|
||||||
|
background: var(--timeline-status-flagged);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-status[data-status="gone"] {
|
||||||
|
background: var(--timeline-status-gone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Label section */
|
||||||
|
.activity-timeline-label {
|
||||||
|
width: 130px;
|
||||||
|
min-width: 130px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1px;
|
||||||
|
border-right: 1px solid var(--timeline-border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-id {
|
||||||
|
color: var(--timeline-text-primary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-name {
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
font-size: 9px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TRACK (where bars are drawn)
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-track {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-track-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
SIGNAL BARS
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
height: 14px;
|
||||||
|
min-width: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strength variants */
|
||||||
|
.activity-timeline-bar[data-strength="1"] { height: 5px; }
|
||||||
|
.activity-timeline-bar[data-strength="2"] { height: 9px; }
|
||||||
|
.activity-timeline-bar[data-strength="3"] { height: 13px; }
|
||||||
|
.activity-timeline-bar[data-strength="4"] { height: 17px; }
|
||||||
|
.activity-timeline-bar[data-strength="5"] { height: 21px; }
|
||||||
|
|
||||||
|
/* Status colors */
|
||||||
|
.activity-timeline-bar[data-status="new"],
|
||||||
|
.activity-timeline-bar[data-status="repeated"] {
|
||||||
|
background: var(--timeline-status-new);
|
||||||
|
box-shadow: 0 0 4px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-bar[data-status="baseline"] {
|
||||||
|
background: var(--timeline-status-baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-bar[data-status="burst"] {
|
||||||
|
background: var(--timeline-status-burst);
|
||||||
|
box-shadow: 0 0 5px rgba(245, 158, 11, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-bar[data-status="flagged"] {
|
||||||
|
background: var(--timeline-status-flagged);
|
||||||
|
box-shadow: 0 0 6px rgba(239, 68, 68, 0.5);
|
||||||
|
animation: timeline-flagged-pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes timeline-flagged-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lane:hover .activity-timeline-bar {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
EXPANDED VIEW (tick marks)
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-ticks {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: 4px;
|
||||||
|
border-top: 1px solid var(--timeline-border);
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-lane.expanded .activity-timeline-ticks {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-tick {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 1px;
|
||||||
|
background: var(--timeline-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-tick[data-strength="1"] { height: 4px; }
|
||||||
|
.activity-timeline-tick[data-strength="2"] { height: 8px; }
|
||||||
|
.activity-timeline-tick[data-strength="3"] { height: 12px; }
|
||||||
|
.activity-timeline-tick[data-strength="4"] { height: 16px; }
|
||||||
|
.activity-timeline-tick[data-strength="5"] { height: 20px; }
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
STATS COLUMN
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-stats {
|
||||||
|
width: 45px;
|
||||||
|
min-width: 45px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-end;
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
border-left: 1px solid var(--timeline-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-stat-count {
|
||||||
|
color: var(--timeline-text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-stat-label {
|
||||||
|
font-size: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ANNOTATIONS
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-annotations {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid var(--timeline-border);
|
||||||
|
max-height: 80px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-annotation {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--timeline-text-secondary);
|
||||||
|
background: var(--timeline-bg-secondary);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-annotation-icon {
|
||||||
|
font-size: 10px;
|
||||||
|
width: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-annotation[data-type="new"] {
|
||||||
|
border-left: 2px solid var(--timeline-status-new);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-annotation[data-type="burst"] {
|
||||||
|
border-left: 2px solid var(--timeline-status-burst);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-annotation[data-type="pattern"] {
|
||||||
|
border-left: 2px solid var(--timeline-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-annotation[data-type="flagged"] {
|
||||||
|
border-left: 2px solid var(--timeline-status-flagged);
|
||||||
|
color: var(--timeline-status-flagged);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-annotation[data-type="gone"] {
|
||||||
|
border-left: 2px solid var(--timeline-status-gone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TOOLTIP
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
background: var(--timeline-bg-elevated);
|
||||||
|
border: 1px solid var(--timeline-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--timeline-text-primary);
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
max-width: 240px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-tooltip-header {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--timeline-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-tooltip-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
color: var(--timeline-text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-tooltip-row span:last-child {
|
||||||
|
color: var(--timeline-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
LEGEND
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-top: 1px solid var(--timeline-border);
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-legend-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-legend-dot.new { background: var(--timeline-status-new); }
|
||||||
|
.activity-timeline-legend-dot.baseline { background: var(--timeline-status-baseline); }
|
||||||
|
.activity-timeline-legend-dot.burst { background: var(--timeline-status-burst); }
|
||||||
|
.activity-timeline-legend-dot.flagged { background: var(--timeline-status-flagged); }
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
EMPTY STATE
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline-empty-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* More indicator */
|
||||||
|
.activity-timeline-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--timeline-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
VISUAL MODE: COMPACT
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline--compact .activity-timeline-lanes {
|
||||||
|
max-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-lane {
|
||||||
|
min-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-label {
|
||||||
|
width: 100px;
|
||||||
|
min-width: 100px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-id {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-name {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--timeline-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-track {
|
||||||
|
min-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-bar {
|
||||||
|
height: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-bar[data-strength="1"] { height: 4px !important; }
|
||||||
|
.activity-timeline--compact .activity-timeline-bar[data-strength="2"] { height: 6px !important; }
|
||||||
|
.activity-timeline--compact .activity-timeline-bar[data-strength="3"] { height: 8px !important; }
|
||||||
|
.activity-timeline--compact .activity-timeline-bar[data-strength="4"] { height: 10px !important; }
|
||||||
|
.activity-timeline--compact .activity-timeline-bar[data-strength="5"] { height: 12px !important; }
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-stats {
|
||||||
|
width: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-stat-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-legend {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--compact .activity-timeline-axis {
|
||||||
|
padding-left: 110px;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
VISUAL MODE: SUMMARY
|
||||||
|
============================================ */
|
||||||
|
.activity-timeline--summary .activity-timeline-lanes {
|
||||||
|
max-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-lane {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-label {
|
||||||
|
width: 80px;
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-id,
|
||||||
|
.activity-timeline--summary .activity-timeline-name {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-status {
|
||||||
|
width: 3px;
|
||||||
|
min-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-track {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-bar {
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-stats {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-ticks {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-annotations {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-legend {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-timeline--summary .activity-timeline-axis {
|
||||||
|
padding-left: 90px;
|
||||||
|
padding-right: 10px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
BACKWARD COMPATIBILITY NOTE
|
||||||
|
The old signal-timeline.css is still loaded
|
||||||
|
for existing TSCM code that uses those classes.
|
||||||
|
New code should use activity-timeline classes.
|
||||||
|
============================================ */
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* Bluetooth Timeline Adapter
|
||||||
|
* Normalizes Bluetooth device data for the Activity Timeline component
|
||||||
|
* Used by: Bluetooth mode, TSCM (Bluetooth detections)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BluetoothTimelineAdapter = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSSI to strength category mapping for Bluetooth
|
||||||
|
* Bluetooth RSSI typically ranges from -30 (very close) to -100 (far)
|
||||||
|
*/
|
||||||
|
const RSSI_THRESHOLDS = {
|
||||||
|
VERY_STRONG: -45, // 5 - device likely within 1m
|
||||||
|
STRONG: -60, // 4 - device likely within 3m
|
||||||
|
MODERATE: -75, // 3 - device likely within 10m
|
||||||
|
WEAK: -90, // 2 - device at edge of range
|
||||||
|
MINIMAL: -100 // 1 - barely detectable
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known device type patterns
|
||||||
|
*/
|
||||||
|
const DEVICE_PATTERNS = {
|
||||||
|
// Apple devices
|
||||||
|
AIRPODS: /airpods/i,
|
||||||
|
IPHONE: /iphone/i,
|
||||||
|
IPAD: /ipad/i,
|
||||||
|
MACBOOK: /macbook|mac\s*pro|imac/i,
|
||||||
|
APPLE_WATCH: /apple\s*watch/i,
|
||||||
|
AIRTAG: /airtag/i,
|
||||||
|
|
||||||
|
// Trackers
|
||||||
|
TILE: /tile/i,
|
||||||
|
CHIPOLO: /chipolo/i,
|
||||||
|
SAMSUNG_TAG: /smarttag|galaxy\s*tag/i,
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
HEADPHONES: /headphone|earphone|earbud|bose|sony|beats|jabra|sennheiser/i,
|
||||||
|
SPEAKER: /speaker|soundbar|echo|homepod|sonos/i,
|
||||||
|
|
||||||
|
// Wearables
|
||||||
|
FITBIT: /fitbit/i,
|
||||||
|
GARMIN: /garmin/i,
|
||||||
|
SMARTWATCH: /watch|band|mi\s*band|galaxy\s*fit/i,
|
||||||
|
|
||||||
|
// Input devices
|
||||||
|
KEYBOARD: /keyboard/i,
|
||||||
|
MOUSE: /mouse|trackpad|magic/i,
|
||||||
|
CONTROLLER: /controller|gamepad|xbox|playstation|dualshock/i,
|
||||||
|
|
||||||
|
// Vehicles
|
||||||
|
CAR: /car\s*kit|handsfree|obd|vehicle|toyota|honda|ford|bmw|mercedes/i
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert RSSI to strength category
|
||||||
|
*/
|
||||||
|
function rssiToStrength(rssi) {
|
||||||
|
if (rssi === null || rssi === undefined) return 3;
|
||||||
|
|
||||||
|
const r = parseFloat(rssi);
|
||||||
|
if (isNaN(r)) return 3;
|
||||||
|
|
||||||
|
if (r > RSSI_THRESHOLDS.VERY_STRONG) return 5;
|
||||||
|
if (r > RSSI_THRESHOLDS.STRONG) return 4;
|
||||||
|
if (r > RSSI_THRESHOLDS.MODERATE) return 3;
|
||||||
|
if (r > RSSI_THRESHOLDS.WEAK) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify device type from name
|
||||||
|
*/
|
||||||
|
function classifyDevice(name) {
|
||||||
|
if (!name) return { type: 'unknown', category: 'device' };
|
||||||
|
|
||||||
|
for (const [pattern, regex] of Object.entries(DEVICE_PATTERNS)) {
|
||||||
|
if (regex.test(name)) {
|
||||||
|
return {
|
||||||
|
type: pattern.toLowerCase(),
|
||||||
|
category: getCategoryForType(pattern)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'unknown', category: 'device' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get category for device type
|
||||||
|
*/
|
||||||
|
function getCategoryForType(type) {
|
||||||
|
const categories = {
|
||||||
|
AIRPODS: 'audio',
|
||||||
|
IPHONE: 'phone',
|
||||||
|
IPAD: 'tablet',
|
||||||
|
MACBOOK: 'computer',
|
||||||
|
APPLE_WATCH: 'wearable',
|
||||||
|
AIRTAG: 'tracker',
|
||||||
|
TILE: 'tracker',
|
||||||
|
CHIPOLO: 'tracker',
|
||||||
|
SAMSUNG_TAG: 'tracker',
|
||||||
|
HEADPHONES: 'audio',
|
||||||
|
SPEAKER: 'audio',
|
||||||
|
FITBIT: 'wearable',
|
||||||
|
GARMIN: 'wearable',
|
||||||
|
SMARTWATCH: 'wearable',
|
||||||
|
KEYBOARD: 'input',
|
||||||
|
MOUSE: 'input',
|
||||||
|
CONTROLLER: 'input',
|
||||||
|
CAR: 'vehicle'
|
||||||
|
};
|
||||||
|
return categories[type] || 'device';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format MAC address for display (truncated)
|
||||||
|
*/
|
||||||
|
function formatMac(mac, full = false) {
|
||||||
|
if (!mac) return 'Unknown';
|
||||||
|
if (full) return mac.toUpperCase();
|
||||||
|
return mac.substring(0, 8).toUpperCase() + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if device is a tracker type
|
||||||
|
*/
|
||||||
|
function isTracker(device) {
|
||||||
|
if (device.is_tracker) return true;
|
||||||
|
|
||||||
|
const name = device.name || '';
|
||||||
|
return /airtag|tile|chipolo|smarttag|tracker/i.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a Bluetooth device detection for the timeline
|
||||||
|
*/
|
||||||
|
function normalizeDevice(device) {
|
||||||
|
const mac = device.mac || device.address || device.id;
|
||||||
|
const name = device.name || device.device_name || formatMac(mac);
|
||||||
|
const classification = classifyDevice(name);
|
||||||
|
|
||||||
|
const tags = [device.type || 'ble'];
|
||||||
|
tags.push(classification.category);
|
||||||
|
|
||||||
|
if (isTracker(device)) tags.push('tracker');
|
||||||
|
if (device.is_beacon) tags.push('beacon');
|
||||||
|
if (device.is_connectable) tags.push('connectable');
|
||||||
|
if (device.manufacturer) tags.push('identified');
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: mac,
|
||||||
|
label: name,
|
||||||
|
strength: rssiToStrength(device.rssi),
|
||||||
|
duration: device.scan_duration || device.duration || 1000,
|
||||||
|
type: classification.type,
|
||||||
|
tags: tags,
|
||||||
|
metadata: {
|
||||||
|
mac: mac,
|
||||||
|
rssi: device.rssi,
|
||||||
|
device_type: device.type,
|
||||||
|
manufacturer: device.manufacturer,
|
||||||
|
services: device.services,
|
||||||
|
is_tracker: isTracker(device),
|
||||||
|
classification: classification
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize for TSCM context (includes threat assessment)
|
||||||
|
*/
|
||||||
|
function normalizeTscmDevice(device) {
|
||||||
|
const normalized = normalizeDevice(device);
|
||||||
|
|
||||||
|
// Add TSCM-specific tags
|
||||||
|
if (device.is_new) normalized.tags.push('new');
|
||||||
|
if (device.threat_level) normalized.tags.push(`threat-${device.threat_level}`);
|
||||||
|
if (device.baseline_known === false) normalized.tags.push('unknown');
|
||||||
|
|
||||||
|
normalized.metadata.threat_level = device.threat_level;
|
||||||
|
normalized.metadata.first_seen = device.first_seen;
|
||||||
|
normalized.metadata.appearance_count = device.appearance_count;
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch normalize multiple devices
|
||||||
|
*/
|
||||||
|
function normalizeDevices(devices, context = 'scan') {
|
||||||
|
const normalizer = context === 'tscm' ? normalizeTscmDevice : normalizeDevice;
|
||||||
|
return devices.map(normalizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create timeline configuration for Bluetooth mode
|
||||||
|
*/
|
||||||
|
function getBluetoothConfig() {
|
||||||
|
return {
|
||||||
|
title: 'Device Activity',
|
||||||
|
mode: 'bluetooth',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: false,
|
||||||
|
showAnnotations: true,
|
||||||
|
showLegend: true,
|
||||||
|
defaultWindow: '15m',
|
||||||
|
availableWindows: ['5m', '15m', '30m', '1h'],
|
||||||
|
filters: {
|
||||||
|
hideBaseline: { enabled: true, label: 'Hide Known', default: false },
|
||||||
|
showOnlyNew: { enabled: true, label: 'New Only', default: false },
|
||||||
|
showOnlyBurst: { enabled: false, label: 'Bursts', default: false }
|
||||||
|
},
|
||||||
|
customFilters: [
|
||||||
|
{
|
||||||
|
key: 'showOnlyTrackers',
|
||||||
|
label: 'Trackers Only',
|
||||||
|
default: false,
|
||||||
|
predicate: (item) => item.tags.includes('tracker')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hideWearables',
|
||||||
|
label: 'Hide Wearables',
|
||||||
|
default: false,
|
||||||
|
predicate: (item) => !item.tags.includes('wearable')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
maxItems: 75,
|
||||||
|
maxDisplayedLanes: 12,
|
||||||
|
labelGenerator: (id) => formatMac(id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create compact timeline configuration (for sidebar use)
|
||||||
|
*/
|
||||||
|
function getCompactConfig() {
|
||||||
|
return {
|
||||||
|
title: 'BT Devices',
|
||||||
|
mode: 'bluetooth',
|
||||||
|
visualMode: 'compact',
|
||||||
|
collapsed: false,
|
||||||
|
showAnnotations: false,
|
||||||
|
showLegend: false,
|
||||||
|
defaultWindow: '15m',
|
||||||
|
availableWindows: ['5m', '15m', '30m'],
|
||||||
|
filters: {
|
||||||
|
hideBaseline: { enabled: false },
|
||||||
|
showOnlyNew: { enabled: true, label: 'New', default: false },
|
||||||
|
showOnlyBurst: { enabled: false }
|
||||||
|
},
|
||||||
|
customFilters: [],
|
||||||
|
maxItems: 30,
|
||||||
|
maxDisplayedLanes: 8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
return {
|
||||||
|
// Normalization
|
||||||
|
normalizeDevice: normalizeDevice,
|
||||||
|
normalizeTscmDevice: normalizeTscmDevice,
|
||||||
|
normalizeDevices: normalizeDevices,
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
rssiToStrength: rssiToStrength,
|
||||||
|
classifyDevice: classifyDevice,
|
||||||
|
formatMac: formatMac,
|
||||||
|
isTracker: isTracker,
|
||||||
|
|
||||||
|
// Configuration presets
|
||||||
|
getBluetoothConfig: getBluetoothConfig,
|
||||||
|
getCompactConfig: getCompactConfig,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
RSSI_THRESHOLDS: RSSI_THRESHOLDS,
|
||||||
|
DEVICE_PATTERNS: DEVICE_PATTERNS
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Export for module systems
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = BluetoothTimelineAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.BluetoothTimelineAdapter = BluetoothTimelineAdapter;
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/**
|
||||||
|
* RF Signal Timeline Adapter
|
||||||
|
* Normalizes RF signal data for the Activity Timeline component
|
||||||
|
* Used by: Listening Post, TSCM
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RFTimelineAdapter = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSSI to strength category mapping
|
||||||
|
* Uses confidence-safe thresholds
|
||||||
|
*/
|
||||||
|
const RSSI_THRESHOLDS = {
|
||||||
|
VERY_STRONG: -40, // 5 - indicates likely nearby source
|
||||||
|
STRONG: -55, // 4 - probable close proximity
|
||||||
|
MODERATE: -70, // 3 - likely in proximity
|
||||||
|
WEAK: -85, // 2 - potentially distant or obstructed
|
||||||
|
MINIMAL: -100 // 1 - may be ambient noise or distant source
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frequency band categorization
|
||||||
|
*/
|
||||||
|
const FREQUENCY_BANDS = [
|
||||||
|
{ min: 2400, max: 2500, label: 'Wi-Fi 2.4GHz', type: 'wifi' },
|
||||||
|
{ min: 5150, max: 5850, label: 'Wi-Fi 5GHz', type: 'wifi' },
|
||||||
|
{ min: 5925, max: 7125, label: 'Wi-Fi 6E', type: 'wifi' },
|
||||||
|
{ min: 2402, max: 2480, label: 'Bluetooth', type: 'bluetooth' },
|
||||||
|
{ min: 433, max: 434, label: '433MHz ISM', type: 'ism' },
|
||||||
|
{ min: 868, max: 869, label: '868MHz ISM', type: 'ism' },
|
||||||
|
{ min: 902, max: 928, label: '915MHz ISM', type: 'ism' },
|
||||||
|
{ min: 315, max: 316, label: '315MHz', type: 'keyfob' },
|
||||||
|
{ min: 144, max: 148, label: 'VHF Ham', type: 'amateur' },
|
||||||
|
{ min: 420, max: 450, label: 'UHF Ham', type: 'amateur' },
|
||||||
|
{ min: 462.5625, max: 467.7125, label: 'FRS/GMRS', type: 'personal' },
|
||||||
|
{ min: 151, max: 159, label: 'VHF Business', type: 'commercial' },
|
||||||
|
{ min: 450, max: 470, label: 'UHF Business', type: 'commercial' },
|
||||||
|
{ min: 88, max: 108, label: 'FM Broadcast', type: 'broadcast' },
|
||||||
|
{ min: 118, max: 137, label: 'Airband', type: 'aviation' },
|
||||||
|
{ min: 156, max: 162, label: 'Marine VHF', type: 'marine' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert RSSI (dBm) to strength category (1-5)
|
||||||
|
*/
|
||||||
|
function rssiToStrength(rssi) {
|
||||||
|
if (rssi === null || rssi === undefined) return 3;
|
||||||
|
|
||||||
|
const r = parseFloat(rssi);
|
||||||
|
if (isNaN(r)) return 3;
|
||||||
|
|
||||||
|
if (r > RSSI_THRESHOLDS.VERY_STRONG) return 5;
|
||||||
|
if (r > RSSI_THRESHOLDS.STRONG) return 4;
|
||||||
|
if (r > RSSI_THRESHOLDS.MODERATE) return 3;
|
||||||
|
if (r > RSSI_THRESHOLDS.WEAK) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize frequency into human-readable band name
|
||||||
|
*/
|
||||||
|
function categorizeFrequency(freqMHz) {
|
||||||
|
const f = parseFloat(freqMHz);
|
||||||
|
if (isNaN(f)) return { label: String(freqMHz), type: 'unknown' };
|
||||||
|
|
||||||
|
for (const band of FREQUENCY_BANDS) {
|
||||||
|
if (f >= band.min && f <= band.max) {
|
||||||
|
return { label: band.label, type: band.type };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic labeling by range
|
||||||
|
if (f < 30) return { label: `${f.toFixed(3)} MHz HF`, type: 'hf' };
|
||||||
|
if (f < 300) return { label: `${f.toFixed(3)} MHz VHF`, type: 'vhf' };
|
||||||
|
if (f < 3000) return { label: `${f.toFixed(3)} MHz UHF`, type: 'uhf' };
|
||||||
|
return { label: `${f.toFixed(3)} MHz`, type: 'unknown' };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a scanner signal detection for the timeline
|
||||||
|
*/
|
||||||
|
function normalizeSignal(signalData) {
|
||||||
|
const freq = signalData.frequency || signalData.freq;
|
||||||
|
const category = categorizeFrequency(freq);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(freq),
|
||||||
|
label: signalData.name || category.label,
|
||||||
|
strength: rssiToStrength(signalData.rssi || signalData.signal_strength),
|
||||||
|
duration: signalData.duration || 1000,
|
||||||
|
type: category.type,
|
||||||
|
tags: buildTags(signalData, category),
|
||||||
|
metadata: {
|
||||||
|
frequency: freq,
|
||||||
|
rssi: signalData.rssi,
|
||||||
|
modulation: signalData.modulation,
|
||||||
|
bandwidth: signalData.bandwidth
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a TSCM RF detection
|
||||||
|
*/
|
||||||
|
function normalizeTscmSignal(detection) {
|
||||||
|
const freq = detection.frequency;
|
||||||
|
const category = categorizeFrequency(freq);
|
||||||
|
|
||||||
|
const tags = buildTags(detection, category);
|
||||||
|
|
||||||
|
// Add TSCM-specific tags
|
||||||
|
if (detection.is_new) tags.push('new');
|
||||||
|
if (detection.baseline_deviation) tags.push('deviation');
|
||||||
|
if (detection.threat_level) tags.push(`threat-${detection.threat_level}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: String(freq),
|
||||||
|
label: detection.name || category.label,
|
||||||
|
strength: rssiToStrength(detection.rssi),
|
||||||
|
duration: detection.duration || 1000,
|
||||||
|
type: category.type,
|
||||||
|
tags: tags,
|
||||||
|
metadata: {
|
||||||
|
frequency: freq,
|
||||||
|
rssi: detection.rssi,
|
||||||
|
threat_level: detection.threat_level,
|
||||||
|
source: detection.source
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build tags array from signal data
|
||||||
|
*/
|
||||||
|
function buildTags(data, category) {
|
||||||
|
const tags = [];
|
||||||
|
|
||||||
|
if (category.type) tags.push(category.type);
|
||||||
|
|
||||||
|
if (data.modulation) {
|
||||||
|
tags.push(data.modulation.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.is_burst) tags.push('burst');
|
||||||
|
if (data.is_continuous) tags.push('continuous');
|
||||||
|
if (data.is_periodic) tags.push('periodic');
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch normalize multiple signals
|
||||||
|
*/
|
||||||
|
function normalizeSignals(signals, type = 'scanner') {
|
||||||
|
const normalizer = type === 'tscm' ? normalizeTscmSignal : normalizeSignal;
|
||||||
|
return signals.map(normalizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create timeline configuration for Listening Post mode
|
||||||
|
*/
|
||||||
|
function getListeningPostConfig() {
|
||||||
|
return {
|
||||||
|
title: 'Signal Activity',
|
||||||
|
mode: 'listening-post',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: false,
|
||||||
|
showAnnotations: true,
|
||||||
|
showLegend: true,
|
||||||
|
defaultWindow: '15m',
|
||||||
|
availableWindows: ['5m', '15m', '30m', '1h'],
|
||||||
|
filters: {
|
||||||
|
hideBaseline: { enabled: true, label: 'Hide Known', default: false },
|
||||||
|
showOnlyNew: { enabled: true, label: 'New Only', default: false },
|
||||||
|
showOnlyBurst: { enabled: true, label: 'Bursts', default: false }
|
||||||
|
},
|
||||||
|
customFilters: [
|
||||||
|
{
|
||||||
|
key: 'hideIsm',
|
||||||
|
label: 'Hide ISM',
|
||||||
|
default: false,
|
||||||
|
predicate: (item) => !item.tags.includes('ism')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
maxItems: 50,
|
||||||
|
maxDisplayedLanes: 12
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create timeline configuration for TSCM mode
|
||||||
|
*/
|
||||||
|
function getTscmConfig() {
|
||||||
|
return {
|
||||||
|
title: 'Signal Activity Timeline',
|
||||||
|
mode: 'tscm',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: true,
|
||||||
|
showAnnotations: true,
|
||||||
|
showLegend: true,
|
||||||
|
defaultWindow: '30m',
|
||||||
|
availableWindows: ['5m', '15m', '30m', '1h', '2h'],
|
||||||
|
filters: {
|
||||||
|
hideBaseline: { enabled: true, label: 'Hide Known', default: false },
|
||||||
|
showOnlyNew: { enabled: true, label: 'New Only', default: false },
|
||||||
|
showOnlyBurst: { enabled: true, label: 'Bursts', default: false }
|
||||||
|
},
|
||||||
|
customFilters: [],
|
||||||
|
maxItems: 100,
|
||||||
|
maxDisplayedLanes: 15
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
return {
|
||||||
|
// Normalization
|
||||||
|
normalizeSignal: normalizeSignal,
|
||||||
|
normalizeTscmSignal: normalizeTscmSignal,
|
||||||
|
normalizeSignals: normalizeSignals,
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
rssiToStrength: rssiToStrength,
|
||||||
|
categorizeFrequency: categorizeFrequency,
|
||||||
|
|
||||||
|
// Configuration presets
|
||||||
|
getListeningPostConfig: getListeningPostConfig,
|
||||||
|
getTscmConfig: getTscmConfig,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
RSSI_THRESHOLDS: RSSI_THRESHOLDS,
|
||||||
|
FREQUENCY_BANDS: FREQUENCY_BANDS
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Export for module systems
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = RFTimelineAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.RFTimelineAdapter = RFTimelineAdapter;
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
/**
|
||||||
|
* WiFi Timeline Adapter
|
||||||
|
* Normalizes WiFi network data for the Activity Timeline component
|
||||||
|
* Used by: WiFi mode, TSCM (WiFi detections)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const WiFiTimelineAdapter = (function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSSI to strength category mapping for WiFi
|
||||||
|
*/
|
||||||
|
const RSSI_THRESHOLDS = {
|
||||||
|
EXCELLENT: -50, // 5 - excellent signal
|
||||||
|
GOOD: -60, // 4 - good signal
|
||||||
|
FAIR: -70, // 3 - fair signal
|
||||||
|
WEAK: -80, // 2 - weak signal
|
||||||
|
POOR: -90 // 1 - very weak
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WiFi channel to frequency band mapping
|
||||||
|
*/
|
||||||
|
const CHANNEL_BANDS = {
|
||||||
|
// 2.4 GHz (channels 1-14)
|
||||||
|
'2.4GHz': { min: 1, max: 14 },
|
||||||
|
// 5 GHz (channels 32-177)
|
||||||
|
'5GHz': { min: 32, max: 177 },
|
||||||
|
// 6 GHz (channels 1-233, WiFi 6E)
|
||||||
|
'6GHz': { min: 1, max: 233, is6e: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security type classifications
|
||||||
|
*/
|
||||||
|
const SECURITY_TYPES = {
|
||||||
|
OPEN: 'open',
|
||||||
|
WEP: 'wep',
|
||||||
|
WPA: 'wpa',
|
||||||
|
WPA2: 'wpa2',
|
||||||
|
WPA3: 'wpa3',
|
||||||
|
ENTERPRISE: 'enterprise'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert RSSI to strength category
|
||||||
|
*/
|
||||||
|
function rssiToStrength(rssi) {
|
||||||
|
if (rssi === null || rssi === undefined) return 3;
|
||||||
|
|
||||||
|
const r = parseFloat(rssi);
|
||||||
|
if (isNaN(r)) return 3;
|
||||||
|
|
||||||
|
if (r > RSSI_THRESHOLDS.EXCELLENT) return 5;
|
||||||
|
if (r > RSSI_THRESHOLDS.GOOD) return 4;
|
||||||
|
if (r > RSSI_THRESHOLDS.FAIR) return 3;
|
||||||
|
if (r > RSSI_THRESHOLDS.WEAK) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine frequency band from channel
|
||||||
|
*/
|
||||||
|
function getBandFromChannel(channel, frequency) {
|
||||||
|
if (frequency) {
|
||||||
|
const f = parseFloat(frequency);
|
||||||
|
if (f >= 5925) return '6GHz';
|
||||||
|
if (f >= 5000) return '5GHz';
|
||||||
|
if (f >= 2400) return '2.4GHz';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ch = parseInt(channel);
|
||||||
|
if (isNaN(ch)) return 'unknown';
|
||||||
|
|
||||||
|
// This is simplified - in practice 6GHz also uses channels 1+
|
||||||
|
// but typically reported with frequency
|
||||||
|
if (ch <= 14) return '2.4GHz';
|
||||||
|
if (ch >= 32 && ch <= 177) return '5GHz';
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify security type
|
||||||
|
*/
|
||||||
|
function classifySecurity(network) {
|
||||||
|
const security = (network.security || network.encryption || '').toLowerCase();
|
||||||
|
const auth = (network.auth || '').toLowerCase();
|
||||||
|
|
||||||
|
if (!security || security === 'none' || security === 'open') {
|
||||||
|
return SECURITY_TYPES.OPEN;
|
||||||
|
}
|
||||||
|
if (security.includes('wep')) return SECURITY_TYPES.WEP;
|
||||||
|
if (security.includes('wpa3')) return SECURITY_TYPES.WPA3;
|
||||||
|
if (security.includes('wpa2') || security.includes('rsn')) {
|
||||||
|
if (auth.includes('eap') || auth.includes('802.1x') || auth.includes('enterprise')) {
|
||||||
|
return SECURITY_TYPES.ENTERPRISE;
|
||||||
|
}
|
||||||
|
return SECURITY_TYPES.WPA2;
|
||||||
|
}
|
||||||
|
if (security.includes('wpa')) return SECURITY_TYPES.WPA;
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate SSID for display
|
||||||
|
*/
|
||||||
|
function formatSsid(ssid, maxLength = 20) {
|
||||||
|
if (!ssid) return '[Hidden]';
|
||||||
|
if (ssid.length <= maxLength) return ssid;
|
||||||
|
return ssid.substring(0, maxLength - 3) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identify potentially interesting network characteristics
|
||||||
|
*/
|
||||||
|
function identifyCharacteristics(network) {
|
||||||
|
const characteristics = [];
|
||||||
|
const ssid = (network.ssid || '').toLowerCase();
|
||||||
|
|
||||||
|
// Hidden network
|
||||||
|
if (!network.ssid || network.is_hidden) {
|
||||||
|
characteristics.push('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open network
|
||||||
|
if (classifySecurity(network) === SECURITY_TYPES.OPEN) {
|
||||||
|
characteristics.push('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weak security
|
||||||
|
if (classifySecurity(network) === SECURITY_TYPES.WEP) {
|
||||||
|
characteristics.push('weak-security');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potential hotspot
|
||||||
|
if (/hotspot|mobile|tether|android|iphone/i.test(ssid)) {
|
||||||
|
characteristics.push('hotspot');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guest network
|
||||||
|
if (/guest|visitor|public/i.test(ssid)) {
|
||||||
|
characteristics.push('guest');
|
||||||
|
}
|
||||||
|
|
||||||
|
// IoT device
|
||||||
|
if (/ring|nest|ecobee|smartthings|wyze|arlo|hue|lifx/i.test(ssid)) {
|
||||||
|
characteristics.push('iot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return characteristics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a WiFi network detection for the timeline
|
||||||
|
*/
|
||||||
|
function normalizeNetwork(network) {
|
||||||
|
const ssid = network.ssid || network.essid || '';
|
||||||
|
const bssid = network.bssid || network.mac || '';
|
||||||
|
const band = getBandFromChannel(network.channel, network.frequency);
|
||||||
|
const security = classifySecurity(network);
|
||||||
|
const characteristics = identifyCharacteristics(network);
|
||||||
|
|
||||||
|
const tags = [band, security, ...characteristics];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: bssid || ssid,
|
||||||
|
label: formatSsid(ssid) || formatMac(bssid),
|
||||||
|
strength: rssiToStrength(network.rssi || network.signal),
|
||||||
|
duration: network.duration || 1000,
|
||||||
|
type: 'wifi',
|
||||||
|
tags: tags.filter(Boolean),
|
||||||
|
metadata: {
|
||||||
|
ssid: ssid,
|
||||||
|
bssid: bssid,
|
||||||
|
channel: network.channel,
|
||||||
|
frequency: network.frequency,
|
||||||
|
rssi: network.rssi || network.signal,
|
||||||
|
security: security,
|
||||||
|
band: band,
|
||||||
|
characteristics: characteristics
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize for TSCM context
|
||||||
|
*/
|
||||||
|
function normalizeTscmNetwork(network) {
|
||||||
|
const normalized = normalizeNetwork(network);
|
||||||
|
|
||||||
|
// Add TSCM-specific tags
|
||||||
|
if (network.is_new) normalized.tags.push('new');
|
||||||
|
if (network.threat_level) normalized.tags.push(`threat-${network.threat_level}`);
|
||||||
|
if (network.is_rogue) normalized.tags.push('rogue');
|
||||||
|
if (network.is_deauth_target) normalized.tags.push('targeted');
|
||||||
|
|
||||||
|
normalized.metadata.threat_level = network.threat_level;
|
||||||
|
normalized.metadata.first_seen = network.first_seen;
|
||||||
|
normalized.metadata.client_count = network.client_count;
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format MAC/BSSID for display
|
||||||
|
*/
|
||||||
|
function formatMac(mac) {
|
||||||
|
if (!mac) return 'Unknown';
|
||||||
|
return mac.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch normalize multiple networks
|
||||||
|
*/
|
||||||
|
function normalizeNetworks(networks, context = 'scan') {
|
||||||
|
const normalizer = context === 'tscm' ? normalizeTscmNetwork : normalizeNetwork;
|
||||||
|
return networks.map(normalizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create timeline configuration for WiFi mode
|
||||||
|
*/
|
||||||
|
function getWiFiConfig() {
|
||||||
|
return {
|
||||||
|
title: 'Network Activity',
|
||||||
|
mode: 'wifi',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: false,
|
||||||
|
showAnnotations: true,
|
||||||
|
showLegend: true,
|
||||||
|
defaultWindow: '15m',
|
||||||
|
availableWindows: ['5m', '15m', '30m', '1h'],
|
||||||
|
filters: {
|
||||||
|
hideBaseline: { enabled: true, label: 'Hide Known', default: false },
|
||||||
|
showOnlyNew: { enabled: true, label: 'New Only', default: false },
|
||||||
|
showOnlyBurst: { enabled: false, label: 'Bursts', default: false }
|
||||||
|
},
|
||||||
|
customFilters: [
|
||||||
|
{
|
||||||
|
key: 'showOnlyOpen',
|
||||||
|
label: 'Open Only',
|
||||||
|
default: false,
|
||||||
|
predicate: (item) => item.tags.includes('open')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hideHidden',
|
||||||
|
label: 'Hide Hidden',
|
||||||
|
default: false,
|
||||||
|
predicate: (item) => !item.tags.includes('hidden')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'show5GHz',
|
||||||
|
label: '5GHz Only',
|
||||||
|
default: false,
|
||||||
|
predicate: (item) => item.tags.includes('5GHz')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
maxItems: 100,
|
||||||
|
maxDisplayedLanes: 15,
|
||||||
|
labelGenerator: (id) => formatSsid(id)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create compact configuration for sidebar
|
||||||
|
*/
|
||||||
|
function getCompactConfig() {
|
||||||
|
return {
|
||||||
|
title: 'Networks',
|
||||||
|
mode: 'wifi',
|
||||||
|
visualMode: 'compact',
|
||||||
|
collapsed: false,
|
||||||
|
showAnnotations: false,
|
||||||
|
showLegend: false,
|
||||||
|
defaultWindow: '15m',
|
||||||
|
availableWindows: ['5m', '15m', '30m'],
|
||||||
|
filters: {
|
||||||
|
hideBaseline: { enabled: false },
|
||||||
|
showOnlyNew: { enabled: true, label: 'New', default: false },
|
||||||
|
showOnlyBurst: { enabled: false }
|
||||||
|
},
|
||||||
|
customFilters: [],
|
||||||
|
maxItems: 30,
|
||||||
|
maxDisplayedLanes: 8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
return {
|
||||||
|
// Normalization
|
||||||
|
normalizeNetwork: normalizeNetwork,
|
||||||
|
normalizeTscmNetwork: normalizeTscmNetwork,
|
||||||
|
normalizeNetworks: normalizeNetworks,
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
rssiToStrength: rssiToStrength,
|
||||||
|
getBandFromChannel: getBandFromChannel,
|
||||||
|
classifySecurity: classifySecurity,
|
||||||
|
formatSsid: formatSsid,
|
||||||
|
identifyCharacteristics: identifyCharacteristics,
|
||||||
|
|
||||||
|
// Configuration presets
|
||||||
|
getWiFiConfig: getWiFiConfig,
|
||||||
|
getCompactConfig: getCompactConfig,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
RSSI_THRESHOLDS: RSSI_THRESHOLDS,
|
||||||
|
SECURITY_TYPES: SECURITY_TYPES
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Export for module systems
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = WiFiTimelineAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.WiFiTimelineAdapter = WiFiTimelineAdapter;
|
||||||
@@ -691,6 +691,25 @@ function addSignalHit(data) {
|
|||||||
|
|
||||||
const hitCount = document.getElementById('scannerHitCount');
|
const hitCount = document.getElementById('scannerHitCount');
|
||||||
if (hitCount) hitCount.textContent = `${tbody.children.length} signals found`;
|
if (hitCount) hitCount.textContent = `${tbody.children.length} signals found`;
|
||||||
|
|
||||||
|
// Feed to activity timeline if available
|
||||||
|
if (typeof addTimelineEvent === 'function') {
|
||||||
|
const normalized = typeof RFTimelineAdapter !== 'undefined'
|
||||||
|
? RFTimelineAdapter.normalizeSignal({
|
||||||
|
frequency: data.frequency,
|
||||||
|
rssi: data.rssi || data.signal_strength,
|
||||||
|
duration: data.duration || 2000,
|
||||||
|
modulation: data.modulation
|
||||||
|
})
|
||||||
|
: {
|
||||||
|
id: String(data.frequency),
|
||||||
|
label: `${data.frequency.toFixed(3)} MHz`,
|
||||||
|
strength: 3,
|
||||||
|
duration: 2000,
|
||||||
|
type: 'rf'
|
||||||
|
};
|
||||||
|
addTimelineEvent('listening', normalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearScannerLog() {
|
function clearScannerLog() {
|
||||||
@@ -700,6 +719,12 @@ function clearScannerLog() {
|
|||||||
scannerCycles = 0;
|
scannerCycles = 0;
|
||||||
recentSignalHits.clear();
|
recentSignalHits.clear();
|
||||||
|
|
||||||
|
// Clear the timeline if available
|
||||||
|
const timeline = typeof getTimeline === 'function' ? getTimeline('listening') : null;
|
||||||
|
if (timeline) {
|
||||||
|
timeline.clear();
|
||||||
|
}
|
||||||
|
|
||||||
const signalCount = document.getElementById('scannerSignalCount');
|
const signalCount = document.getElementById('scannerSignalCount');
|
||||||
if (signalCount) signalCount.textContent = '0';
|
if (signalCount) signalCount.textContent = '0';
|
||||||
|
|
||||||
|
|||||||
+135
-4
@@ -18,6 +18,7 @@
|
|||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/tscm.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/tscm.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-timeline.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-timeline.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/activity-timeline.css') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -790,6 +791,10 @@
|
|||||||
<div style="color: var(--text-dim);">Waiting for client probe requests...</div>
|
<div style="color: var(--text-dim);">Waiting for client probe requests...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Network Activity Timeline -->
|
||||||
|
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||||||
|
<div id="wifiTimelineContainer"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Right: WiFi Device Cards -->
|
<!-- Right: WiFi Device Cards -->
|
||||||
<div class="wifi-device-list" id="wifiDeviceList">
|
<div class="wifi-device-list" id="wifiDeviceList">
|
||||||
@@ -870,6 +875,10 @@
|
|||||||
FindMy-compatible devices...</div>
|
FindMy-compatible devices...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Device Activity Timeline -->
|
||||||
|
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||||||
|
<div id="bluetoothTimelineContainer"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Right: Bluetooth Device Cards -->
|
<!-- Right: Bluetooth Device Cards -->
|
||||||
<div class="wifi-device-list bt-device-list" id="btDeviceListPanel">
|
<div class="wifi-device-list bt-device-list" id="btDeviceListPanel">
|
||||||
@@ -1345,6 +1354,11 @@
|
|||||||
<div class="scanner-log-entry" style="color: var(--text-muted);">Ready</div>
|
<div class="scanner-log-entry" style="color: var(--text-muted);">Ready</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SIGNAL ACTIVITY TIMELINE -->
|
||||||
|
<div class="radio-module-box" style="grid-column: span 4; padding: 10px;">
|
||||||
|
<div id="listeningPostTimelineContainer"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Satellite Dashboard (Embedded) -->
|
<!-- Satellite Dashboard (Embedded) -->
|
||||||
@@ -1577,9 +1591,93 @@
|
|||||||
<script src="{{ url_for('static', filename='js/components/radio-knob.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/components/radio-knob.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/components/signal-cards.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/components/signal-cards.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/components/signal-timeline.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/components/signal-timeline.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/components/activity-timeline.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/components/timeline-adapters/rf-adapter.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/components/timeline-adapters/bluetooth-adapter.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/components/timeline-adapters/wifi-adapter.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/modes/listening-post.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/modes/listening-post.js') }}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// ============================================
|
||||||
|
// ACTIVITY TIMELINE MANAGEMENT
|
||||||
|
// ============================================
|
||||||
|
const modeTimelines = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize timeline for a specific mode
|
||||||
|
*/
|
||||||
|
function initializeModeTimeline(mode) {
|
||||||
|
// Skip if already initialized
|
||||||
|
if (modeTimelines[mode]) return;
|
||||||
|
|
||||||
|
const configs = {
|
||||||
|
'tscm': {
|
||||||
|
container: 'tscmTimelineContainer',
|
||||||
|
config: typeof RFTimelineAdapter !== 'undefined' ? RFTimelineAdapter.getTscmConfig() : {
|
||||||
|
title: 'Signal Activity Timeline',
|
||||||
|
mode: 'tscm',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'listening': {
|
||||||
|
container: 'listeningPostTimelineContainer',
|
||||||
|
config: typeof RFTimelineAdapter !== 'undefined' ? RFTimelineAdapter.getListeningPostConfig() : {
|
||||||
|
title: 'Signal Activity',
|
||||||
|
mode: 'listening-post',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'bluetooth': {
|
||||||
|
container: 'bluetoothTimelineContainer',
|
||||||
|
config: typeof BluetoothTimelineAdapter !== 'undefined' ? BluetoothTimelineAdapter.getBluetoothConfig() : {
|
||||||
|
title: 'Device Activity',
|
||||||
|
mode: 'bluetooth',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'wifi': {
|
||||||
|
container: 'wifiTimelineContainer',
|
||||||
|
config: typeof WiFiTimelineAdapter !== 'undefined' ? WiFiTimelineAdapter.getWiFiConfig() : {
|
||||||
|
title: 'Network Activity',
|
||||||
|
mode: 'wifi',
|
||||||
|
visualMode: 'enriched',
|
||||||
|
collapsed: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const modeConfig = configs[mode];
|
||||||
|
if (!modeConfig) return;
|
||||||
|
|
||||||
|
const container = document.getElementById(modeConfig.container);
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Create timeline using new ActivityTimeline
|
||||||
|
if (typeof ActivityTimeline !== 'undefined') {
|
||||||
|
modeTimelines[mode] = ActivityTimeline.create(modeConfig.container, modeConfig.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event to a mode's timeline
|
||||||
|
*/
|
||||||
|
function addTimelineEvent(mode, eventData) {
|
||||||
|
const timeline = modeTimelines[mode];
|
||||||
|
if (timeline) {
|
||||||
|
timeline.addEvent(eventData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get timeline instance for a mode
|
||||||
|
*/
|
||||||
|
function getTimeline(mode) {
|
||||||
|
return modeTimelines[mode] || null;
|
||||||
|
}
|
||||||
|
|
||||||
// Selected mode from welcome screen
|
// Selected mode from welcome screen
|
||||||
let selectedStartMode = 'pager';
|
let selectedStartMode = 'pager';
|
||||||
|
|
||||||
@@ -2042,14 +2140,13 @@
|
|||||||
};
|
};
|
||||||
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
|
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
|
||||||
|
|
||||||
|
// Initialize mode-specific timelines
|
||||||
|
initializeModeTimeline(mode);
|
||||||
|
|
||||||
// Initialize TSCM mode when selected
|
// Initialize TSCM mode when selected
|
||||||
if (mode === 'tscm') {
|
if (mode === 'tscm') {
|
||||||
loadTscmBaselines();
|
loadTscmBaselines();
|
||||||
refreshTscmDevices();
|
refreshTscmDevices();
|
||||||
// Initialize signal timeline if not already created
|
|
||||||
if (!document.getElementById('signalTimeline')) {
|
|
||||||
SignalTimeline.create('tscmTimelineContainer');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
|
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft/tscm)
|
||||||
@@ -5079,6 +5176,26 @@
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
if (autoScroll) output.scrollTop = 0;
|
if (autoScroll) output.scrollTop = 0;
|
||||||
|
|
||||||
|
// Feed to activity timeline if it's a new network
|
||||||
|
if (isNew && typeof addTimelineEvent === 'function') {
|
||||||
|
const normalized = typeof WiFiTimelineAdapter !== 'undefined'
|
||||||
|
? WiFiTimelineAdapter.normalizeNetwork({
|
||||||
|
ssid: net.essid,
|
||||||
|
bssid: net.bssid,
|
||||||
|
channel: net.channel,
|
||||||
|
rssi: signalStrength,
|
||||||
|
security: net.privacy
|
||||||
|
})
|
||||||
|
: {
|
||||||
|
id: net.bssid,
|
||||||
|
label: net.essid || '[Hidden]',
|
||||||
|
strength: signalBars || 3,
|
||||||
|
duration: 1500,
|
||||||
|
type: 'wifi'
|
||||||
|
};
|
||||||
|
addTimelineEvent('wifi', normalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add WiFi client card to device list
|
// Add WiFi client card to device list
|
||||||
@@ -6345,6 +6462,20 @@
|
|||||||
|
|
||||||
// Update statistics panels
|
// Update statistics panels
|
||||||
updateBtStatsPanels();
|
updateBtStatsPanels();
|
||||||
|
|
||||||
|
// Feed to activity timeline if it's a new detection
|
||||||
|
if (isNew && typeof addTimelineEvent === 'function') {
|
||||||
|
const normalized = typeof BluetoothTimelineAdapter !== 'undefined'
|
||||||
|
? BluetoothTimelineAdapter.normalizeDevice(device)
|
||||||
|
: {
|
||||||
|
id: device.mac,
|
||||||
|
label: device.name || device.mac.substring(0, 8) + '...',
|
||||||
|
strength: device.rssi ? Math.min(5, Math.max(1, Math.ceil((device.rssi + 100) / 20))) : 3,
|
||||||
|
duration: 1500,
|
||||||
|
type: 'bluetooth'
|
||||||
|
};
|
||||||
|
addTimelineEvent('bluetooth', normalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select a Bluetooth device
|
// Select a Bluetooth device
|
||||||
|
|||||||
@@ -24,40 +24,38 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<label class="inline-checkbox">
|
||||||
<input type="checkbox" id="tscmVerboseResults" style="margin: 0;">
|
<input type="checkbox" id="tscmVerboseResults">
|
||||||
<label for="tscmVerboseResults" style="flex: 1; margin: 0; font-size: 12px;">
|
Verbose results (store full device details)
|
||||||
Verbose results (store full device details)
|
</label>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
|
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
|
||||||
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 8px; color: var(--text-secondary);">Scan Sources</label>
|
<label style="display: block; font-size: 11px; font-weight: 600; margin-bottom: 8px; color: var(--text-secondary);">Scan Sources</label>
|
||||||
<div class="form-group" style="margin-bottom: 8px;">
|
<div class="form-group" style="margin-bottom: 8px;">
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<label class="inline-checkbox">
|
||||||
<input type="checkbox" id="tscmWifiEnabled" checked style="margin: 0;">
|
<input type="checkbox" id="tscmWifiEnabled" checked>
|
||||||
<label for="tscmWifiEnabled" style="flex: 1; margin: 0; font-size: 12px;">WiFi</label>
|
WiFi
|
||||||
</div>
|
</label>
|
||||||
<select id="tscmWifiInterface" style="width: 100%; margin-top: 4px; font-size: 11px;">
|
<select id="tscmWifiInterface" style="margin-top: 4px;">
|
||||||
<option value="">Select WiFi interface...</option>
|
<option value="">Select WiFi interface...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom: 8px;">
|
<div class="form-group" style="margin-bottom: 8px;">
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<label class="inline-checkbox">
|
||||||
<input type="checkbox" id="tscmBtEnabled" checked style="margin: 0;">
|
<input type="checkbox" id="tscmBtEnabled" checked>
|
||||||
<label for="tscmBtEnabled" style="flex: 1; margin: 0; font-size: 12px;">Bluetooth</label>
|
Bluetooth
|
||||||
</div>
|
</label>
|
||||||
<select id="tscmBtInterface" style="width: 100%; margin-top: 4px; font-size: 11px;">
|
<select id="tscmBtInterface" style="margin-top: 4px;">
|
||||||
<option value="">Select Bluetooth adapter...</option>
|
<option value="">Select Bluetooth adapter...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom: 8px;">
|
<div class="form-group" style="margin-bottom: 8px;">
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<label class="inline-checkbox">
|
||||||
<input type="checkbox" id="tscmRfEnabled" style="margin: 0;">
|
<input type="checkbox" id="tscmRfEnabled">
|
||||||
<label for="tscmRfEnabled" style="flex: 1; margin: 0; font-size: 12px;">RF/SDR</label>
|
RF/SDR
|
||||||
</div>
|
</label>
|
||||||
<select id="tscmSdrDevice" style="width: 100%; margin-top: 4px; font-size: 11px;">
|
<select id="tscmSdrDevice" style="margin-top: 4px;">
|
||||||
<option value="">Select SDR device...</option>
|
<option value="">Select SDR device...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user