mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Adding Spystations page and 2 small fixed for the vessel page
This commit is contained in:
@@ -93,6 +93,200 @@ STATIONS = [
|
|||||||
"schedule": "Multiple daily transmissions",
|
"schedule": "Multiple daily transmissions",
|
||||||
"source_url": "https://priyom.org/number-stations/cuba/hm01"
|
"source_url": "https://priyom.org/number-stations/cuba/hm01"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "e07",
|
||||||
|
"name": "E07",
|
||||||
|
"nickname": "7-dash",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Russia",
|
||||||
|
"country_code": "RU",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 5292, "primary": True},
|
||||||
|
{"freq_khz": 6388, "primary": False},
|
||||||
|
{"freq_khz": 7482, "primary": False},
|
||||||
|
{"freq_khz": 8576, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "USB",
|
||||||
|
"description": "Russian intelligence station using distinctive 7-dash interval signal. Female voice reading 5-figure groups in English. Part of the 'Russian 7' operator network.",
|
||||||
|
"operator": "Russian 7",
|
||||||
|
"schedule": "Irregular, typically evenings UTC",
|
||||||
|
"source_url": "https://priyom.org/number-stations/english/e07"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e11",
|
||||||
|
"name": "E11",
|
||||||
|
"nickname": "Mazielka",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Poland",
|
||||||
|
"country_code": "PL",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 4030, "primary": True},
|
||||||
|
{"freq_khz": 5240, "primary": False},
|
||||||
|
{"freq_khz": 6910, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "USB",
|
||||||
|
"description": "Polish intelligence number station. Female voice reads 5-figure groups in English. Named after distinctive melody interval signal.",
|
||||||
|
"operator": "ABW (Polish Intelligence)",
|
||||||
|
"schedule": "Weekly transmissions",
|
||||||
|
"source_url": "https://priyom.org/number-stations/english/e11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e17z",
|
||||||
|
"name": "E17z",
|
||||||
|
"nickname": "Israeli Numbers",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Israel",
|
||||||
|
"country_code": "IL",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 4779, "primary": True},
|
||||||
|
{"freq_khz": 5091, "primary": False},
|
||||||
|
{"freq_khz": 6446, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "USB",
|
||||||
|
"description": "Israeli intelligence number station. Female voice with distinctive Hebrew-accented English. Transmits 5-figure groups with phonetic alphabet.",
|
||||||
|
"operator": "Mossad (suspected)",
|
||||||
|
"schedule": "Irregular schedule",
|
||||||
|
"source_url": "https://priyom.org/number-stations/english/e17z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g06",
|
||||||
|
"name": "G06",
|
||||||
|
"nickname": "Russian German",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Russia",
|
||||||
|
"country_code": "RU",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 4310, "primary": True},
|
||||||
|
{"freq_khz": 4800, "primary": False},
|
||||||
|
{"freq_khz": 5370, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "USB+carrier",
|
||||||
|
"description": "German language mode of Russian 6 operator. Male synthesized voice reads 5-figure groups in German. Shares frequencies with E06/S06.",
|
||||||
|
"operator": "Russian 6",
|
||||||
|
"schedule": "Same schedule as E06",
|
||||||
|
"source_url": "https://priyom.org/number-stations/german/g06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "v02a",
|
||||||
|
"name": "V02a",
|
||||||
|
"nickname": "Cuban Spy Numbers",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Cuba",
|
||||||
|
"country_code": "CU",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 5855, "primary": True},
|
||||||
|
{"freq_khz": 9330, "primary": False},
|
||||||
|
{"freq_khz": 11635, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "AM",
|
||||||
|
"description": "Cuban intelligence station using AM mode. Female Spanish voice reading 4-figure groups. Related to HM01 but separate schedule.",
|
||||||
|
"operator": "DGI (Cuban Intelligence)",
|
||||||
|
"schedule": "Evening transmissions, weekdays",
|
||||||
|
"source_url": "https://priyom.org/number-stations/spanish/v02a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "v07",
|
||||||
|
"name": "V07",
|
||||||
|
"nickname": "Russian 7 Voice",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Russia",
|
||||||
|
"country_code": "RU",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 3756, "primary": True},
|
||||||
|
{"freq_khz": 4625, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "USB",
|
||||||
|
"description": "Russian voice number station. Female voice reads 5-figure groups in Russian. Part of Russian 7 operator network. Often shares 4625 kHz with UVB-76.",
|
||||||
|
"operator": "Russian 7",
|
||||||
|
"schedule": "Irregular transmissions",
|
||||||
|
"source_url": "https://priyom.org/number-stations/russian/v07"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s11a",
|
||||||
|
"name": "S11a",
|
||||||
|
"nickname": "Russian Phonetic",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Russia",
|
||||||
|
"country_code": "RU",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 4560, "primary": True},
|
||||||
|
{"freq_khz": 5200, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "USB",
|
||||||
|
"description": "Russian phonetic alphabet number station. Male voice reads 5-letter groups using Russian phonetic alphabet (Anna, Boris, etc.).",
|
||||||
|
"operator": "GRU (suspected)",
|
||||||
|
"schedule": "Weekly scheduled transmissions",
|
||||||
|
"source_url": "https://priyom.org/number-stations/russian/s11a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "v13",
|
||||||
|
"name": "V13",
|
||||||
|
"nickname": "The Pip",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Russia",
|
||||||
|
"country_code": "RU",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 3756, "primary": True},
|
||||||
|
{"freq_khz": 5448, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "USB",
|
||||||
|
"description": "Russian military channel marker known as 'The Pip'. Continuous short beep every 1 second with occasional voice messages. Sister station to UVB-76.",
|
||||||
|
"operator": "Russian Military",
|
||||||
|
"schedule": "24/7 continuous operation",
|
||||||
|
"source_url": "https://priyom.org/military-stations/russia/the-pip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "v24",
|
||||||
|
"name": "V24",
|
||||||
|
"nickname": "Air Horn",
|
||||||
|
"type": "number",
|
||||||
|
"country": "Russia",
|
||||||
|
"country_code": "RU",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 3243, "primary": True},
|
||||||
|
],
|
||||||
|
"mode": "USB",
|
||||||
|
"description": "Russian channel marker known as 'Air Horn' due to distinctive foghorn-like sound. Continuous tone with occasional voice messages in Russian.",
|
||||||
|
"operator": "Russian Military",
|
||||||
|
"schedule": "24/7 continuous operation",
|
||||||
|
"source_url": "https://priyom.org/military-stations/russia/the-air-horn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vc01",
|
||||||
|
"name": "VC01",
|
||||||
|
"nickname": "Chinese Robot",
|
||||||
|
"type": "number",
|
||||||
|
"country": "China",
|
||||||
|
"country_code": "CN",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 8300, "primary": True},
|
||||||
|
{"freq_khz": 9725, "primary": False},
|
||||||
|
{"freq_khz": 11430, "primary": False},
|
||||||
|
{"freq_khz": 13750, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "AM",
|
||||||
|
"description": "Chinese intelligence number station. Robotic female voice reading 4-figure groups in Chinese. Distinctive electronic music interval signal.",
|
||||||
|
"operator": "MSS (Chinese Intelligence)",
|
||||||
|
"schedule": "Daily transmissions",
|
||||||
|
"source_url": "https://priyom.org/number-stations/chinese/vc01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "v22",
|
||||||
|
"name": "V22",
|
||||||
|
"nickname": "Chinese Lady",
|
||||||
|
"type": "number",
|
||||||
|
"country": "China",
|
||||||
|
"country_code": "CN",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 7883, "primary": True},
|
||||||
|
{"freq_khz": 9170, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "AM",
|
||||||
|
"description": "Chinese number station using female voice. Reads 4-figure groups in Mandarin Chinese. Often reported in Southeast Asian target areas.",
|
||||||
|
"operator": "MSS (Chinese Intelligence)",
|
||||||
|
"schedule": "Evening transmissions UTC",
|
||||||
|
"source_url": "https://priyom.org/number-stations/chinese/v22"
|
||||||
|
},
|
||||||
# Diplomatic Stations
|
# Diplomatic Stations
|
||||||
{
|
{
|
||||||
"id": "bulgaria_mfa",
|
"id": "bulgaria_mfa",
|
||||||
@@ -261,6 +455,114 @@ STATIONS = [
|
|||||||
"schedule": "24/7 global network",
|
"schedule": "24/7 global network",
|
||||||
"source_url": "https://priyom.org/diplomatic/united-states"
|
"source_url": "https://priyom.org/diplomatic/united-states"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "morocco_mfa",
|
||||||
|
"name": "Morocco MFA",
|
||||||
|
"nickname": "Moroccan Diplomatic",
|
||||||
|
"type": "diplomatic",
|
||||||
|
"country": "Morocco",
|
||||||
|
"country_code": "MA",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 8010, "primary": True},
|
||||||
|
{"freq_khz": 11205, "primary": False},
|
||||||
|
{"freq_khz": 14620, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "PACTOR-II/ALE",
|
||||||
|
"description": "Moroccan Ministry of Foreign Affairs diplomatic network. Links Rabat with embassies in Europe and Africa. Uses PACTOR-II and 2G ALE.",
|
||||||
|
"operator": "Moroccan MFA",
|
||||||
|
"schedule": "Daily scheduled traffic",
|
||||||
|
"source_url": "https://priyom.org/diplomatic/morocco"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "poland_mfa",
|
||||||
|
"name": "Poland MFA",
|
||||||
|
"nickname": "Polish Diplomatic",
|
||||||
|
"type": "diplomatic",
|
||||||
|
"country": "Poland",
|
||||||
|
"country_code": "PL",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 6825, "primary": True},
|
||||||
|
{"freq_khz": 9250, "primary": False},
|
||||||
|
{"freq_khz": 13485, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "STANAG-4285/ALE",
|
||||||
|
"description": "Polish Ministry of Foreign Affairs HF network. Uses NATO STANAG-4285 modem with 2G ALE linking. Connects Warsaw with global embassies.",
|
||||||
|
"operator": "Polish MFA",
|
||||||
|
"schedule": "Regular diplomatic traffic",
|
||||||
|
"source_url": "https://priyom.org/diplomatic/poland"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "france_mfa",
|
||||||
|
"name": "France MFA",
|
||||||
|
"nickname": "French Diplomatic",
|
||||||
|
"type": "diplomatic",
|
||||||
|
"country": "France",
|
||||||
|
"country_code": "FR",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 6910, "primary": True},
|
||||||
|
{"freq_khz": 10640, "primary": False},
|
||||||
|
{"freq_khz": 13870, "primary": False},
|
||||||
|
{"freq_khz": 16840, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "MIL-STD-188-110/ALE",
|
||||||
|
"description": "French Ministry of Foreign Affairs network. Extensive global coverage with Paris hub. Uses MIL-STD-188-110 with 2G/3G ALE linking protocols.",
|
||||||
|
"operator": "French MFA",
|
||||||
|
"schedule": "24/7 network operations",
|
||||||
|
"source_url": "https://priyom.org/diplomatic/france"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "romania_mfa",
|
||||||
|
"name": "Romania MFA",
|
||||||
|
"nickname": "Romanian Diplomatic",
|
||||||
|
"type": "diplomatic",
|
||||||
|
"country": "Romania",
|
||||||
|
"country_code": "RO",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 5390, "primary": True},
|
||||||
|
{"freq_khz": 8158, "primary": False},
|
||||||
|
{"freq_khz": 11555, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "PACTOR-III/ALE",
|
||||||
|
"description": "Romanian diplomatic network linking Bucharest with embassies. Uses PACTOR-III for traffic and 2G ALE for channel establishment.",
|
||||||
|
"operator": "Romanian MFA",
|
||||||
|
"schedule": "Scheduled daily windows",
|
||||||
|
"source_url": "https://priyom.org/diplomatic/romania"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "algeria_mfa",
|
||||||
|
"name": "Algeria MFA",
|
||||||
|
"nickname": "Algerian Diplomatic",
|
||||||
|
"type": "diplomatic",
|
||||||
|
"country": "Algeria",
|
||||||
|
"country_code": "DZ",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 7706, "primary": True},
|
||||||
|
{"freq_khz": 10235, "primary": False},
|
||||||
|
{"freq_khz": 14385, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "SITOR-B/PACTOR",
|
||||||
|
"description": "Algerian Ministry of Foreign Affairs network. Links Algiers with African and European embassies. Uses SITOR-B and PACTOR modes.",
|
||||||
|
"operator": "Algerian MFA",
|
||||||
|
"schedule": "Daily scheduled transmissions",
|
||||||
|
"source_url": "https://priyom.org/diplomatic/algeria"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "egypt_mfa_m14a",
|
||||||
|
"name": "Egypt MFA M14a",
|
||||||
|
"nickname": "Egyptian Extended",
|
||||||
|
"type": "diplomatic",
|
||||||
|
"country": "Egypt",
|
||||||
|
"country_code": "EG",
|
||||||
|
"frequencies": [
|
||||||
|
{"freq_khz": 12175, "primary": True},
|
||||||
|
{"freq_khz": 16360, "primary": False},
|
||||||
|
],
|
||||||
|
"mode": "Codan 3012/SITOR",
|
||||||
|
"description": "Extended Egyptian diplomatic network frequencies. Higher frequency allocations for long-distance embassy communications to Asia and Americas.",
|
||||||
|
"operator": "Egyptian MFA",
|
||||||
|
"schedule": "Daily traffic windows",
|
||||||
|
"source_url": "https://priyom.org/diplomatic/egypt"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,38 @@ body {
|
|||||||
color: var(--accent-green);
|
color: var(--accent-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Signal quality states */
|
||||||
|
.strip-stat.signal-stat .strip-value {
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-stat.signal-stat.good {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border-color: rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-stat.signal-stat.good .strip-value {
|
||||||
|
color: var(--accent-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-stat.signal-stat.warning {
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
border-color: rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-stat.signal-stat.warning .strip-value {
|
||||||
|
color: var(--accent-orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-stat.signal-stat.poor {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strip-stat.signal-stat.poor .strip-value {
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
.strip-divider {
|
.strip-divider {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
height: 100%;
|
min-height: 0;
|
||||||
overflow: hidden;
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spy-stations-header {
|
.spy-stations-header {
|
||||||
@@ -54,9 +55,8 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
padding-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
@@ -66,8 +66,9 @@
|
|||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spy-station-card:hover {
|
.spy-station-card:hover {
|
||||||
@@ -144,6 +145,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spy-station-meta {
|
.spy-station-meta {
|
||||||
@@ -211,10 +213,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card Footer */
|
/* Card Footer */
|
||||||
@@ -225,6 +223,49 @@
|
|||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frequency Selector Group */
|
||||||
|
.spy-tune-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-freq-select {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-width: 120px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-freq-select:hover {
|
||||||
|
border-color: var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-freq-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clickable frequency items in details modal */
|
||||||
|
.spy-freq-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-freq-clickable:hover {
|
||||||
|
background: var(--accent-cyan);
|
||||||
|
color: #000;
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tune Button */
|
/* Tune Button */
|
||||||
@@ -295,6 +336,13 @@
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
MODE VISIBILITY - Ensure sidebar shows when active
|
||||||
|
============================================ */
|
||||||
|
#spystationsMode.active {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================
|
/* ============================================
|
||||||
FILTER CHECKBOX STYLING
|
FILTER CHECKBOX STYLING
|
||||||
============================================ */
|
============================================ */
|
||||||
@@ -321,6 +369,22 @@
|
|||||||
/* ============================================
|
/* ============================================
|
||||||
RESPONSIVE
|
RESPONSIVE
|
||||||
============================================ */
|
============================================ */
|
||||||
|
|
||||||
|
/* Large desktop (1200px+) */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.spy-stations-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop/Tablet landscape (1024px) */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.spy-stations-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet portrait (768px) */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.spy-stations-grid {
|
.spy-stations-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -341,7 +405,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
/* Small tablet / large phone (640px) */
|
||||||
|
@media (max-width: 640px) {
|
||||||
.spy-station-footer {
|
.spy-station-footer {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -351,5 +416,51 @@
|
|||||||
.spy-details-btn {
|
.spy-details-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-tune-group {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-freq-select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile (480px) */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.spy-stations-container {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-station-body {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-stations-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-station-desc {
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch device compliance */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.spy-tune-btn,
|
||||||
|
.spy-details-btn,
|
||||||
|
.spy-freq-select {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spy-freq-clickable {
|
||||||
|
padding: 8px 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,14 @@ const SpyStations = (function() {
|
|||||||
'EG': '\u{1F1EA}\u{1F1EC}',
|
'EG': '\u{1F1EA}\u{1F1EC}',
|
||||||
'KP': '\u{1F1F0}\u{1F1F5}',
|
'KP': '\u{1F1F0}\u{1F1F5}',
|
||||||
'TN': '\u{1F1F9}\u{1F1F3}',
|
'TN': '\u{1F1F9}\u{1F1F3}',
|
||||||
'US': '\u{1F1FA}\u{1F1F8}'
|
'US': '\u{1F1FA}\u{1F1F8}',
|
||||||
|
'PL': '\u{1F1F5}\u{1F1F1}',
|
||||||
|
'IL': '\u{1F1EE}\u{1F1F1}',
|
||||||
|
'CN': '\u{1F1E8}\u{1F1F3}',
|
||||||
|
'MA': '\u{1F1F2}\u{1F1E6}',
|
||||||
|
'FR': '\u{1F1EB}\u{1F1F7}',
|
||||||
|
'RO': '\u{1F1F7}\u{1F1F4}',
|
||||||
|
'DZ': '\u{1F1E9}\u{1F1FF}'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -121,6 +128,7 @@ const SpyStations = (function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
renderStations();
|
renderStations();
|
||||||
|
updateStats(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,6 +169,39 @@ const SpyStations = (function() {
|
|||||||
const freqList = station.frequencies.slice(0, 4).map(f => formatFrequency(f.freq_khz)).join(', ');
|
const freqList = station.frequencies.slice(0, 4).map(f => formatFrequency(f.freq_khz)).join(', ');
|
||||||
const moreFreqs = station.frequencies.length > 4 ? ` +${station.frequencies.length - 4} more` : '';
|
const moreFreqs = station.frequencies.length > 4 ? ` +${station.frequencies.length - 4} more` : '';
|
||||||
|
|
||||||
|
// Build tune button with frequency selector if multiple frequencies
|
||||||
|
let tuneSection;
|
||||||
|
if (station.frequencies.length > 1) {
|
||||||
|
const options = station.frequencies.map(f => {
|
||||||
|
const label = formatFrequency(f.freq_khz) + (f.primary ? ' (primary)' : '');
|
||||||
|
return `<option value="${f.freq_khz}">${label}</option>`;
|
||||||
|
}).join('');
|
||||||
|
tuneSection = `
|
||||||
|
<div class="spy-tune-group">
|
||||||
|
<select class="spy-freq-select" id="freq-select-${station.id}">
|
||||||
|
${options}
|
||||||
|
</select>
|
||||||
|
<button class="spy-tune-btn" onclick="SpyStations.tuneToSelectedFreq('${station.id}')">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
|
||||||
|
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
|
||||||
|
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
|
||||||
|
</svg>
|
||||||
|
Tune In
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
tuneSection = `
|
||||||
|
<button class="spy-tune-btn" onclick="SpyStations.tuneToStation('${station.id}', ${primaryFreq.freq_khz})">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
|
||||||
|
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
|
||||||
|
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
|
||||||
|
</svg>
|
||||||
|
Tune In
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="spy-station-card" data-station-id="${station.id}">
|
<div class="spy-station-card" data-station-id="${station.id}">
|
||||||
<div class="spy-station-header">
|
<div class="spy-station-header">
|
||||||
@@ -189,13 +230,7 @@ const SpyStations = (function() {
|
|||||||
<div class="spy-station-desc">${station.description}</div>
|
<div class="spy-station-desc">${station.description}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="spy-station-footer">
|
<div class="spy-station-footer">
|
||||||
<button class="spy-tune-btn" onclick="SpyStations.tuneToStation('${station.id}', ${primaryFreq.freq_khz})">
|
${tuneSection}
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 14px; height: 14px;">
|
|
||||||
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
|
|
||||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
|
|
||||||
</svg>
|
|
||||||
Tune In
|
|
||||||
</button>
|
|
||||||
<button class="spy-details-btn" onclick="SpyStations.showDetails('${station.id}')">
|
<button class="spy-details-btn" onclick="SpyStations.showDetails('${station.id}')">
|
||||||
Details
|
Details
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 12px; height: 12px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 12px; height: 12px;">
|
||||||
@@ -217,20 +252,34 @@ const SpyStations = (function() {
|
|||||||
return freqKhz + ' kHz';
|
return freqKhz + ' kHz';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get appropriate SDR mode from station mode string
|
||||||
|
*/
|
||||||
|
function getModeFromStation(stationMode) {
|
||||||
|
const mode = stationMode.toLowerCase();
|
||||||
|
if (mode.includes('am') || mode.includes('ofdm')) return 'am';
|
||||||
|
if (mode.includes('lsb')) return 'lsb';
|
||||||
|
if (mode.includes('fm')) return 'fm';
|
||||||
|
// Default to USB for most number stations and digital modes
|
||||||
|
return 'usb';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tune to a station frequency
|
* Tune to a station frequency
|
||||||
*/
|
*/
|
||||||
function tuneToStation(stationId, freqKhz) {
|
function tuneToStation(stationId, freqKhz) {
|
||||||
const freqMhz = freqKhz / 1000;
|
const freqMhz = freqKhz / 1000;
|
||||||
sessionStorage.setItem('tuneFrequency', freqMhz.toString());
|
sessionStorage.setItem('tuneFrequency', freqMhz.toString());
|
||||||
sessionStorage.setItem('tuneMode', 'usb'); // Most number stations use USB
|
|
||||||
|
|
||||||
// Find the station for notification
|
// Find the station and determine mode
|
||||||
const station = stations.find(s => s.id === stationId);
|
const station = stations.find(s => s.id === stationId);
|
||||||
|
const tuneMode = station ? getModeFromStation(station.mode) : 'usb';
|
||||||
|
sessionStorage.setItem('tuneMode', tuneMode);
|
||||||
|
|
||||||
const stationName = station ? station.name : 'Station';
|
const stationName = station ? station.name : 'Station';
|
||||||
|
|
||||||
if (typeof showNotification === 'function') {
|
if (typeof showNotification === 'function') {
|
||||||
showNotification('Tuning to ' + stationName, formatFrequency(freqKhz));
|
showNotification('Tuning to ' + stationName, formatFrequency(freqKhz) + ' (' + tuneMode.toUpperCase() + ')');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switch to listening post mode
|
// Switch to listening post mode
|
||||||
@@ -241,6 +290,17 @@ const SpyStations = (function() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tune to selected frequency from dropdown
|
||||||
|
*/
|
||||||
|
function tuneToSelectedFreq(stationId) {
|
||||||
|
const select = document.getElementById('freq-select-' + stationId);
|
||||||
|
if (select) {
|
||||||
|
const freqKhz = parseInt(select.value, 10);
|
||||||
|
tuneToStation(stationId, freqKhz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we arrived from another page with a tune request
|
* Check if we arrived from another page with a tune request
|
||||||
*/
|
*/
|
||||||
@@ -266,7 +326,7 @@ const SpyStations = (function() {
|
|||||||
const flag = countryFlags[station.country_code] || '';
|
const flag = countryFlags[station.country_code] || '';
|
||||||
const allFreqs = station.frequencies.map(f => {
|
const allFreqs = station.frequencies.map(f => {
|
||||||
const label = f.primary ? ' (primary)' : '';
|
const label = f.primary ? ' (primary)' : '';
|
||||||
return `<span class="spy-freq-item">${formatFrequency(f.freq_khz)}${label}</span>`;
|
return `<span class="spy-freq-item spy-freq-clickable" onclick="SpyStations.tuneToStation('${station.id}', ${f.freq_khz}); SpyStations.closeDetails();">${formatFrequency(f.freq_khz)}${label}</span>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
@@ -425,12 +485,14 @@ const SpyStations = (function() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update sidebar stats
|
* Update sidebar stats
|
||||||
|
* @param {boolean} useFiltered - If true, use filtered stations instead of all stations
|
||||||
*/
|
*/
|
||||||
function updateStats() {
|
function updateStats(useFiltered) {
|
||||||
const numberCount = stations.filter(s => s.type === 'number').length;
|
const stationList = useFiltered ? filteredStations : stations;
|
||||||
const diplomaticCount = stations.filter(s => s.type === 'diplomatic').length;
|
const numberCount = stationList.filter(s => s.type === 'number').length;
|
||||||
const countryCount = new Set(stations.map(s => s.country_code)).size;
|
const diplomaticCount = stationList.filter(s => s.type === 'diplomatic').length;
|
||||||
const freqCount = stations.reduce((sum, s) => sum + s.frequencies.length, 0);
|
const countryCount = new Set(stationList.map(s => s.country_code)).size;
|
||||||
|
const freqCount = stationList.reduce((sum, s) => sum + s.frequencies.length, 0);
|
||||||
|
|
||||||
const numberEl = document.getElementById('spyStatsNumber');
|
const numberEl = document.getElementById('spyStatsNumber');
|
||||||
const diplomaticEl = document.getElementById('spyStatsDiplomatic');
|
const diplomaticEl = document.getElementById('spyStatsDiplomatic');
|
||||||
@@ -441,6 +503,12 @@ const SpyStations = (function() {
|
|||||||
if (diplomaticEl) diplomaticEl.textContent = diplomaticCount;
|
if (diplomaticEl) diplomaticEl.textContent = diplomaticCount;
|
||||||
if (countriesEl) countriesEl.textContent = countryCount;
|
if (countriesEl) countriesEl.textContent = countryCount;
|
||||||
if (freqsEl) freqsEl.textContent = freqCount;
|
if (freqsEl) freqsEl.textContent = freqCount;
|
||||||
|
|
||||||
|
// Update visible count in header if element exists
|
||||||
|
const visibleCountEl = document.getElementById('spyStationsVisibleCount');
|
||||||
|
if (visibleCountEl) {
|
||||||
|
visibleCountEl.textContent = stationList.length + ' stations';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
@@ -448,6 +516,7 @@ const SpyStations = (function() {
|
|||||||
init,
|
init,
|
||||||
applyFilters,
|
applyFilters,
|
||||||
tuneToStation,
|
tuneToStation,
|
||||||
|
tuneToSelectedFreq,
|
||||||
showDetails,
|
showDetails,
|
||||||
closeDetails,
|
closeDetails,
|
||||||
showHelp,
|
showHelp,
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
<span class="strip-label">NEAR NM</span>
|
<span class="strip-label">NEAR NM</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="strip-divider"></div>
|
<div class="strip-divider"></div>
|
||||||
|
<div class="strip-stat signal-stat" title="Signal quality (messages/sec)">
|
||||||
|
<span class="strip-value" id="stripSignal">--</span>
|
||||||
|
<span class="strip-label">SIGNAL</span>
|
||||||
|
</div>
|
||||||
<div class="strip-stat session-stat">
|
<div class="strip-stat session-stat">
|
||||||
<span class="strip-value" id="stripSession">00:00:00</span>
|
<span class="strip-value" id="stripSession">00:00:00</span>
|
||||||
<span class="strip-label">SESSION</span>
|
<span class="strip-label">SESSION</span>
|
||||||
@@ -155,9 +159,16 @@
|
|||||||
maxRange: 0,
|
maxRange: 0,
|
||||||
fastestSpeed: 0,
|
fastestSpeed: 0,
|
||||||
closestDistance: Infinity,
|
closestDistance: Infinity,
|
||||||
sessionStart: null
|
sessionStart: null,
|
||||||
|
messagesReceived: 0,
|
||||||
|
messagesPerSecond: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Session timer
|
||||||
|
let sessionTimerInterval = null;
|
||||||
|
let messageRateInterval = null;
|
||||||
|
let lastMessageCount = 0;
|
||||||
|
|
||||||
// Ship type to icon mapping
|
// Ship type to icon mapping
|
||||||
const SHIP_ICONS = {
|
const SHIP_ICONS = {
|
||||||
30: '🐟', // Fishing
|
30: '🐟', // Fishing
|
||||||
@@ -374,11 +385,11 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.status === 'started' || data.status === 'already_running') {
|
if (data.status === 'started' || data.status === 'already_running') {
|
||||||
isTracking = true;
|
isTracking = true;
|
||||||
stats.sessionStart = Date.now();
|
|
||||||
document.getElementById('startBtn').textContent = 'STOP';
|
document.getElementById('startBtn').textContent = 'STOP';
|
||||||
document.getElementById('startBtn').classList.add('active');
|
document.getElementById('startBtn').classList.add('active');
|
||||||
document.getElementById('trackingDot').classList.add('active');
|
document.getElementById('trackingDot').classList.add('active');
|
||||||
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
||||||
|
startSessionTimer();
|
||||||
startSSE();
|
startSSE();
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || 'Failed to start');
|
alert(data.message || 'Failed to start');
|
||||||
@@ -396,6 +407,8 @@
|
|||||||
document.getElementById('startBtn').classList.remove('active');
|
document.getElementById('startBtn').classList.remove('active');
|
||||||
document.getElementById('trackingDot').classList.remove('active');
|
document.getElementById('trackingDot').classList.remove('active');
|
||||||
document.getElementById('trackingStatus').textContent = 'STANDBY';
|
document.getElementById('trackingStatus').textContent = 'STANDBY';
|
||||||
|
stopSessionTimer();
|
||||||
|
updateSignalQuality();
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
eventSource = null;
|
eventSource = null;
|
||||||
@@ -429,6 +442,7 @@
|
|||||||
|
|
||||||
vessels[mmsi] = data;
|
vessels[mmsi] = data;
|
||||||
stats.totalVesselsSeen.add(mmsi);
|
stats.totalVesselsSeen.add(mmsi);
|
||||||
|
stats.messagesReceived++;
|
||||||
|
|
||||||
// Update statistics
|
// Update statistics
|
||||||
if (data.speed && data.speed > stats.fastestSpeed) {
|
if (data.speed && data.speed > stats.fastestSpeed) {
|
||||||
@@ -630,15 +644,6 @@
|
|||||||
document.getElementById('stripMaxRange').textContent = stats.maxRange.toFixed(1);
|
document.getElementById('stripMaxRange').textContent = stats.maxRange.toFixed(1);
|
||||||
document.getElementById('stripFastest').textContent = stats.fastestSpeed > 0 ? stats.fastestSpeed.toFixed(1) : '-';
|
document.getElementById('stripFastest').textContent = stats.fastestSpeed > 0 ? stats.fastestSpeed.toFixed(1) : '-';
|
||||||
document.getElementById('stripClosest').textContent = stats.closestDistance < Infinity ? stats.closestDistance.toFixed(1) : '-';
|
document.getElementById('stripClosest').textContent = stats.closestDistance < Infinity ? stats.closestDistance.toFixed(1) : '-';
|
||||||
|
|
||||||
if (stats.sessionStart) {
|
|
||||||
const elapsed = Math.floor((Date.now() - stats.sessionStart) / 1000);
|
|
||||||
const h = Math.floor(elapsed / 3600);
|
|
||||||
const m = Math.floor((elapsed % 3600) / 60);
|
|
||||||
const s = elapsed % 60;
|
|
||||||
document.getElementById('stripSession').textContent =
|
|
||||||
`${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupStaleVessels() {
|
function cleanupStaleVessels() {
|
||||||
@@ -682,6 +687,77 @@
|
|||||||
document.getElementById('utcTime').textContent = utc + ' UTC';
|
document.getElementById('utcTime').textContent = utc + ' UTC';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Session timer functions
|
||||||
|
function startSessionTimer() {
|
||||||
|
if (!stats.sessionStart) {
|
||||||
|
stats.sessionStart = Date.now();
|
||||||
|
}
|
||||||
|
if (sessionTimerInterval) clearInterval(sessionTimerInterval);
|
||||||
|
sessionTimerInterval = setInterval(updateSessionTimer, 1000);
|
||||||
|
|
||||||
|
// Start message rate tracking
|
||||||
|
if (messageRateInterval) clearInterval(messageRateInterval);
|
||||||
|
lastMessageCount = stats.messagesReceived;
|
||||||
|
messageRateInterval = setInterval(updateMessageRate, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSessionTimer() {
|
||||||
|
if (sessionTimerInterval) {
|
||||||
|
clearInterval(sessionTimerInterval);
|
||||||
|
sessionTimerInterval = null;
|
||||||
|
}
|
||||||
|
if (messageRateInterval) {
|
||||||
|
clearInterval(messageRateInterval);
|
||||||
|
messageRateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSessionTimer() {
|
||||||
|
if (!stats.sessionStart) return;
|
||||||
|
const elapsed = Date.now() - stats.sessionStart;
|
||||||
|
const hours = Math.floor(elapsed / 3600000);
|
||||||
|
const mins = Math.floor((elapsed % 3600000) / 60000);
|
||||||
|
const secs = Math.floor((elapsed % 60000) / 1000);
|
||||||
|
document.getElementById('stripSession').textContent =
|
||||||
|
`${hours.toString().padStart(2,'0')}:${mins.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMessageRate() {
|
||||||
|
const currentCount = stats.messagesReceived;
|
||||||
|
stats.messagesPerSecond = currentCount - lastMessageCount;
|
||||||
|
lastMessageCount = currentCount;
|
||||||
|
updateSignalQuality();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal quality display
|
||||||
|
function updateSignalQuality() {
|
||||||
|
const msgRate = stats.messagesPerSecond;
|
||||||
|
const el = document.getElementById('stripSignal');
|
||||||
|
const stat = el.closest('.strip-stat');
|
||||||
|
|
||||||
|
if (!isTracking || msgRate === 0) {
|
||||||
|
el.textContent = '--';
|
||||||
|
stat.classList.remove('good', 'warning', 'poor');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal quality based on message rate
|
||||||
|
// Good: >5 msg/s, Warning: 1-5, Poor: <1
|
||||||
|
if (msgRate >= 5) {
|
||||||
|
el.textContent = '●●●';
|
||||||
|
stat.classList.remove('warning', 'poor');
|
||||||
|
stat.classList.add('good');
|
||||||
|
} else if (msgRate >= 1) {
|
||||||
|
el.textContent = '●●○';
|
||||||
|
stat.classList.remove('good', 'poor');
|
||||||
|
stat.classList.add('warning');
|
||||||
|
} else {
|
||||||
|
el.textContent = '●○○';
|
||||||
|
stat.classList.remove('good', 'warning');
|
||||||
|
stat.classList.add('poor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', initMap);
|
document.addEventListener('DOMContentLoaded', initMap);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- SPY STATIONS MODE -->
|
<!-- SPY STATIONS MODE -->
|
||||||
<div id="spystationsMode" class="mode-content" style="display: none;">
|
<div id="spystationsMode" class="mode-content">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Spy Stations</h3>
|
<h3>Spy Stations</h3>
|
||||||
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
<p style="color: var(--text-secondary); font-size: 11px; line-height: 1.5; margin-bottom: 15px;">
|
||||||
|
|||||||
Reference in New Issue
Block a user