working
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { wsService, connected, systemStatus } from './lib/websocket.js';
|
import { wsService, connected, systemStatus } from './lib/websocket.js';
|
||||||
import { api } from './lib/api.js';
|
import { api } from './lib/api.js';
|
||||||
|
import StatusBanner from './components/StatusBanner.svelte';
|
||||||
import WebSwitch from './components/WebSwitch.svelte';
|
import WebSwitch from './components/WebSwitch.svelte';
|
||||||
import PowerGenius from './components/PowerGenius.svelte';
|
import PowerGenius from './components/PowerGenius.svelte';
|
||||||
import TunerGenius from './components/TunerGenius.svelte';
|
import TunerGenius from './components/TunerGenius.svelte';
|
||||||
@@ -13,6 +14,8 @@
|
|||||||
let isConnected = false;
|
let isConnected = false;
|
||||||
let currentTime = new Date();
|
let currentTime = new Date();
|
||||||
let callsign = 'F4BPO'; // Default
|
let callsign = 'F4BPO'; // Default
|
||||||
|
let latitude = null;
|
||||||
|
let longitude = null;
|
||||||
|
|
||||||
const unsubscribeStatus = systemStatus.subscribe(value => {
|
const unsubscribeStatus = systemStatus.subscribe(value => {
|
||||||
status = value;
|
status = value;
|
||||||
@@ -40,6 +43,10 @@
|
|||||||
if (config.callsign) {
|
if (config.callsign) {
|
||||||
callsign = config.callsign;
|
callsign = config.callsign;
|
||||||
}
|
}
|
||||||
|
if (config.location) {
|
||||||
|
latitude = config.location.latitude;
|
||||||
|
longitude = config.location.longitude;
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch config:', err);
|
console.error('Failed to fetch config:', err);
|
||||||
}
|
}
|
||||||
@@ -107,6 +114,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- ✅ NOUVEAU : Bandeau de statut avec fréquence FlexRadio et alertes météo -->
|
||||||
|
<StatusBanner
|
||||||
|
flexradio={status?.flexradio}
|
||||||
|
weather={status?.weather}
|
||||||
|
{latitude}
|
||||||
|
{longitude}
|
||||||
|
windWarningThreshold={30}
|
||||||
|
gustWarningThreshold={50}
|
||||||
|
/>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div class="dashboard-grid">
|
<div class="dashboard-grid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -132,12 +149,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
padding: 16px 24px;
|
padding: 8px 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
||||||
|
border-bottom: 1px solid rgba(79, 195, 247, 0.2);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
@@ -243,6 +261,7 @@
|
|||||||
.date {
|
.date {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
@@ -292,4 +311,4 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
625
web/src/components/StatusBanner.svelte
Normal file
625
web/src/components/StatusBanner.svelte
Normal file
@@ -0,0 +1,625 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
export let flexradio = null;
|
||||||
|
export let weather = null;
|
||||||
|
export let latitude = null;
|
||||||
|
export let longitude = null;
|
||||||
|
export let windWarningThreshold = 30; // km/h
|
||||||
|
export let gustWarningThreshold = 50; // km/h
|
||||||
|
export let graylineWindow = 30; // minutes avant/après sunrise/sunset
|
||||||
|
|
||||||
|
// FlexRadio frequency and mode
|
||||||
|
$: frequency = flexradio?.frequency || 0;
|
||||||
|
$: mode = flexradio?.mode || '';
|
||||||
|
$: txEnabled = flexradio?.tx || false;
|
||||||
|
$: connected = flexradio?.connected || false;
|
||||||
|
|
||||||
|
// Grayline calculation
|
||||||
|
let sunrise = null;
|
||||||
|
let sunset = null;
|
||||||
|
let isGrayline = false;
|
||||||
|
let graylineType = ''; // 'sunrise' ou 'sunset'
|
||||||
|
let timeToNextEvent = '';
|
||||||
|
let currentTime = new Date();
|
||||||
|
let clockInterval;
|
||||||
|
|
||||||
|
// Update time every minute for grayline check
|
||||||
|
onMount(() => {
|
||||||
|
calculateSunTimes();
|
||||||
|
clockInterval = setInterval(() => {
|
||||||
|
currentTime = new Date();
|
||||||
|
checkGrayline();
|
||||||
|
updateTimeToNextEvent();
|
||||||
|
}, 10000); // Update every 10 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (clockInterval) clearInterval(clockInterval);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recalculate when location changes
|
||||||
|
$: if (latitude && longitude) {
|
||||||
|
calculateSunTimes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// SunCalc algorithm (simplified version)
|
||||||
|
function calculateSunTimes() {
|
||||||
|
if (!latitude || !longitude) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const times = getSunTimes(now, latitude, longitude);
|
||||||
|
sunrise = times.sunrise;
|
||||||
|
sunset = times.sunset;
|
||||||
|
checkGrayline();
|
||||||
|
updateTimeToNextEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified sun calculation (based on NOAA algorithm)
|
||||||
|
function getSunTimes(date, lat, lon) {
|
||||||
|
const rad = Math.PI / 180;
|
||||||
|
const dayOfYear = getDayOfYear(date);
|
||||||
|
|
||||||
|
// Fractional year
|
||||||
|
const gamma = (2 * Math.PI / 365) * (dayOfYear - 1 + (date.getHours() - 12) / 24);
|
||||||
|
|
||||||
|
// Equation of time (minutes)
|
||||||
|
const eqTime = 229.18 * (0.000075 + 0.001868 * Math.cos(gamma) - 0.032077 * Math.sin(gamma)
|
||||||
|
- 0.014615 * Math.cos(2 * gamma) - 0.040849 * Math.sin(2 * gamma));
|
||||||
|
|
||||||
|
// Solar declination (radians)
|
||||||
|
const decl = 0.006918 - 0.399912 * Math.cos(gamma) + 0.070257 * Math.sin(gamma)
|
||||||
|
- 0.006758 * Math.cos(2 * gamma) + 0.000907 * Math.sin(2 * gamma)
|
||||||
|
- 0.002697 * Math.cos(3 * gamma) + 0.00148 * Math.sin(3 * gamma);
|
||||||
|
|
||||||
|
// Hour angle for sunrise/sunset
|
||||||
|
const latRad = lat * rad;
|
||||||
|
const zenith = 90.833 * rad; // Official zenith for sunrise/sunset
|
||||||
|
|
||||||
|
const cosHA = (Math.cos(zenith) / (Math.cos(latRad) * Math.cos(decl)))
|
||||||
|
- Math.tan(latRad) * Math.tan(decl);
|
||||||
|
|
||||||
|
// Check for polar day/night
|
||||||
|
if (cosHA > 1 || cosHA < -1) {
|
||||||
|
return { sunrise: null, sunset: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ha = Math.acos(cosHA) / rad; // Hour angle in degrees
|
||||||
|
|
||||||
|
// Sunrise and sunset times in minutes from midnight UTC
|
||||||
|
const sunriseMinutes = 720 - 4 * (lon + ha) - eqTime;
|
||||||
|
const sunsetMinutes = 720 - 4 * (lon - ha) - eqTime;
|
||||||
|
|
||||||
|
// Convert to local Date objects
|
||||||
|
const sunriseDate = new Date(date);
|
||||||
|
sunriseDate.setUTCHours(0, 0, 0, 0);
|
||||||
|
sunriseDate.setUTCMinutes(sunriseMinutes);
|
||||||
|
|
||||||
|
const sunsetDate = new Date(date);
|
||||||
|
sunsetDate.setUTCHours(0, 0, 0, 0);
|
||||||
|
sunsetDate.setUTCMinutes(sunsetMinutes);
|
||||||
|
|
||||||
|
return { sunrise: sunriseDate, sunset: sunsetDate };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDayOfYear(date) {
|
||||||
|
const start = new Date(date.getFullYear(), 0, 0);
|
||||||
|
const diff = date - start;
|
||||||
|
const oneDay = 1000 * 60 * 60 * 24;
|
||||||
|
return Math.floor(diff / oneDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkGrayline() {
|
||||||
|
if (!sunrise || !sunset) {
|
||||||
|
isGrayline = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = currentTime.getTime();
|
||||||
|
const windowMs = graylineWindow * 60 * 1000;
|
||||||
|
|
||||||
|
const nearSunrise = Math.abs(now - sunrise.getTime()) <= windowMs;
|
||||||
|
const nearSunset = Math.abs(now - sunset.getTime()) <= windowMs;
|
||||||
|
|
||||||
|
isGrayline = nearSunrise || nearSunset;
|
||||||
|
graylineType = nearSunrise ? 'sunrise' : (nearSunset ? 'sunset' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimeToNextEvent() {
|
||||||
|
if (!sunrise || !sunset) {
|
||||||
|
timeToNextEvent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = currentTime.getTime();
|
||||||
|
let nextEvent = null;
|
||||||
|
let eventName = '';
|
||||||
|
|
||||||
|
if (now < sunrise.getTime()) {
|
||||||
|
nextEvent = sunrise;
|
||||||
|
eventName = 'Sunrise';
|
||||||
|
} else if (now < sunset.getTime()) {
|
||||||
|
nextEvent = sunset;
|
||||||
|
eventName = 'Sunset';
|
||||||
|
} else {
|
||||||
|
// After sunset, calculate tomorrow's sunrise
|
||||||
|
const tomorrow = new Date(currentTime);
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||||
|
const tomorrowTimes = getSunTimes(tomorrow, latitude, longitude);
|
||||||
|
nextEvent = tomorrowTimes.sunrise;
|
||||||
|
eventName = 'Sunrise';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextEvent) {
|
||||||
|
const diffMs = nextEvent.getTime() - now;
|
||||||
|
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
timeToNextEvent = `${eventName} in ${hours}h${minutes.toString().padStart(2, '0')}m`;
|
||||||
|
} else {
|
||||||
|
timeToNextEvent = `${eventName} in ${minutes}m`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(date) {
|
||||||
|
if (!date) return '--:--';
|
||||||
|
return date.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format frequency for display (MHz with appropriate decimals)
|
||||||
|
function formatFrequency(freqMHz) {
|
||||||
|
if (!freqMHz || freqMHz === 0) return '---';
|
||||||
|
if (freqMHz < 10) {
|
||||||
|
return freqMHz.toFixed(4);
|
||||||
|
} else if (freqMHz < 100) {
|
||||||
|
return freqMHz.toFixed(3);
|
||||||
|
} else {
|
||||||
|
return freqMHz.toFixed(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get band from frequency
|
||||||
|
function getBand(freqMHz) {
|
||||||
|
if (!freqMHz || freqMHz === 0) return '';
|
||||||
|
if (freqMHz >= 1.8 && freqMHz <= 2.0) return '160M';
|
||||||
|
if (freqMHz >= 3.5 && freqMHz <= 4.0) return '80M';
|
||||||
|
if (freqMHz >= 5.3 && freqMHz <= 5.4) return '60M';
|
||||||
|
if (freqMHz >= 7.0 && freqMHz <= 7.3) return '40M';
|
||||||
|
if (freqMHz >= 10.1 && freqMHz <= 10.15) return '30M';
|
||||||
|
if (freqMHz >= 14.0 && freqMHz <= 14.35) return '20M';
|
||||||
|
if (freqMHz >= 18.068 && freqMHz <= 18.168) return '17M';
|
||||||
|
if (freqMHz >= 21.0 && freqMHz <= 21.45) return '15M';
|
||||||
|
if (freqMHz >= 24.89 && freqMHz <= 24.99) return '12M';
|
||||||
|
if (freqMHz >= 28.0 && freqMHz <= 29.7) return '10M';
|
||||||
|
if (freqMHz >= 50.0 && freqMHz <= 54.0) return '6M';
|
||||||
|
if (freqMHz >= 144.0 && freqMHz <= 148.0) return '2M';
|
||||||
|
if (freqMHz >= 430.0 && freqMHz <= 440.0) return '70CM';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather alerts
|
||||||
|
$: windSpeed = weather?.wind_speed || 0;
|
||||||
|
$: windGust = weather?.wind_gust || 0;
|
||||||
|
$: hasWindWarning = windSpeed >= windWarningThreshold;
|
||||||
|
$: hasGustWarning = windGust >= gustWarningThreshold;
|
||||||
|
$: hasAnyWarning = hasWindWarning || hasGustWarning;
|
||||||
|
|
||||||
|
// Band colors
|
||||||
|
function getBandColor(band) {
|
||||||
|
const colors = {
|
||||||
|
'160M': '#9c27b0',
|
||||||
|
'80M': '#673ab7',
|
||||||
|
'60M': '#3f51b5',
|
||||||
|
'40M': '#2196f3',
|
||||||
|
'30M': '#00bcd4',
|
||||||
|
'20M': '#009688',
|
||||||
|
'17M': '#4caf50',
|
||||||
|
'15M': '#8bc34a',
|
||||||
|
'12M': '#cddc39',
|
||||||
|
'10M': '#ffeb3b',
|
||||||
|
'6M': '#ff9800',
|
||||||
|
'2M': '#ff5722',
|
||||||
|
'70CM': '#f44336'
|
||||||
|
};
|
||||||
|
return colors[band] || '#4fc3f7';
|
||||||
|
}
|
||||||
|
|
||||||
|
$: currentBand = getBand(frequency);
|
||||||
|
$: bandColor = getBandColor(currentBand);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="status-banner" class:has-warning={hasAnyWarning}>
|
||||||
|
<!-- FlexRadio Section -->
|
||||||
|
<div class="flex-section">
|
||||||
|
<div class="flex-icon" class:connected={connected} class:disconnected={!connected}>
|
||||||
|
📻
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if connected && frequency > 0}
|
||||||
|
<div class="frequency-display">
|
||||||
|
<span class="frequency" style="--band-color: {bandColor}">
|
||||||
|
{formatFrequency(frequency)}
|
||||||
|
</span>
|
||||||
|
<span class="unit">MHz</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if currentBand}
|
||||||
|
<span class="band-badge" style="background-color: {bandColor}">
|
||||||
|
{currentBand}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if mode}
|
||||||
|
<span class="mode-badge">
|
||||||
|
{mode}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if txEnabled}
|
||||||
|
<span class="tx-indicator">
|
||||||
|
TX
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<span class="no-signal">FlexRadio non connecté</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<div class="separator"></div>
|
||||||
|
|
||||||
|
<!-- Grayline Section -->
|
||||||
|
<div class="grayline-section">
|
||||||
|
{#if latitude && longitude}
|
||||||
|
<div class="sun-times">
|
||||||
|
<span class="sun-item" title="Sunrise">
|
||||||
|
<svg class="sun-icon sunrise-icon" width="18" height="18" viewBox="0 0 24 24">
|
||||||
|
<!-- Horizon line -->
|
||||||
|
<line x1="2" y1="18" x2="22" y2="18" stroke="currentColor" stroke-width="1.5" opacity="0.5"/>
|
||||||
|
<!-- Sun (half visible) -->
|
||||||
|
<path d="M12 18 A6 6 0 0 1 12 6 A6 6 0 0 1 12 18" fill="#fbbf24"/>
|
||||||
|
<!-- Rays -->
|
||||||
|
<path d="M12 2v3M5.6 5.6l2.1 2.1M2 12h3M19 12h3M18.4 5.6l-2.1 2.1" stroke="#fbbf24" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<!-- Up arrow -->
|
||||||
|
<path d="M12 14l-3 3M12 14l3 3M12 14v5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
{formatTime(sunrise)}
|
||||||
|
</span>
|
||||||
|
<span class="sun-item" title="Sunset">
|
||||||
|
<svg class="sun-icon sunset-icon" width="18" height="18" viewBox="0 0 24 24">
|
||||||
|
<!-- Horizon line -->
|
||||||
|
<line x1="2" y1="18" x2="22" y2="18" stroke="currentColor" stroke-width="1.5" opacity="0.5"/>
|
||||||
|
<!-- Sun (half visible, setting) -->
|
||||||
|
<path d="M12 18 A6 6 0 0 1 12 6 A6 6 0 0 1 12 18" fill="#f97316"/>
|
||||||
|
<!-- Rays (dimmer) -->
|
||||||
|
<path d="M12 2v3M5.6 5.6l2.1 2.1M2 12h3M19 12h3M18.4 5.6l-2.1 2.1" stroke="#f97316" stroke-width="2" stroke-linecap="round" opacity="0.7"/>
|
||||||
|
<!-- Down arrow -->
|
||||||
|
<path d="M12 22l-3-3M12 22l3-3M12 22v-5" stroke="#ef4444" stroke-width="2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
{formatTime(sunset)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if isGrayline}
|
||||||
|
<span class="grayline-badge" class:sunrise={graylineType === 'sunrise'} class:sunset={graylineType === 'sunset'}>
|
||||||
|
✨ GRAYLINE
|
||||||
|
</span>
|
||||||
|
{:else if timeToNextEvent}
|
||||||
|
<span class="next-event">
|
||||||
|
{timeToNextEvent}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<span class="no-location">📍 Position non configurée</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Separator -->
|
||||||
|
<div class="separator"></div>
|
||||||
|
|
||||||
|
<!-- Weather Alerts Section -->
|
||||||
|
<div class="weather-section">
|
||||||
|
{#if hasWindWarning}
|
||||||
|
<div class="alert wind-alert">
|
||||||
|
<span class="alert-icon">⚠️</span>
|
||||||
|
<span class="alert-text">
|
||||||
|
Vent: <strong>{windSpeed.toFixed(0)} km/h</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if hasGustWarning}
|
||||||
|
<div class="alert gust-alert">
|
||||||
|
<span class="alert-icon">🌪️</span>
|
||||||
|
<span class="alert-text">
|
||||||
|
Rafales: <strong>{windGust.toFixed(0)} km/h</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !hasAnyWarning}
|
||||||
|
<div class="status-ok">
|
||||||
|
<span class="ok-icon">✓</span>
|
||||||
|
<span class="ok-text">Météo OK</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.status-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 24px;
|
||||||
|
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||||
|
border-bottom: 1px solid rgba(79, 195, 247, 0.15);
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-banner.has-warning {
|
||||||
|
background: linear-gradient(135deg, #1f1a12 0%, #29201b 50%, #2d1b0c 100%);
|
||||||
|
border-bottom-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FlexRadio Section */
|
||||||
|
.flex-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-icon.connected {
|
||||||
|
opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 4px rgba(79, 195, 247, 0.6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-icon.disconnected {
|
||||||
|
opacity: 0.4;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frequency-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frequency {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-family: 'Roboto Mono', 'Consolas', monospace;
|
||||||
|
color: var(--band-color, #4fc3f7);
|
||||||
|
text-shadow: 0 0 15px var(--band-color, rgba(79, 195, 247, 0.5));
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.band-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: rgba(156, 39, 176, 0.3);
|
||||||
|
border: 1px solid rgba(156, 39, 176, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ce93d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-indicator {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: rgba(244, 67, 54, 0.3);
|
||||||
|
border: 1px solid #f44336;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f44336;
|
||||||
|
text-shadow: 0 0 10px rgba(244, 67, 54, 0.8);
|
||||||
|
animation: txPulse 0.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes txPulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.6; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-signal {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Separator */
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 30px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grayline Section */
|
||||||
|
.grayline-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sun-times {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sun-item {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sun-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sunrise-icon {
|
||||||
|
color: rgba(251, 191, 36, 0.6);
|
||||||
|
filter: drop-shadow(0 0 4px rgba(251, 191, 36, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sunset-icon {
|
||||||
|
color: rgba(249, 115, 22, 0.6);
|
||||||
|
filter: drop-shadow(0 0 4px rgba(249, 115, 22, 0.4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayline-badge {
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
animation: graylinePulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayline-badge.sunrise {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 183, 77, 0.3) 0%, rgba(255, 138, 101, 0.3) 100%);
|
||||||
|
border: 1px solid rgba(255, 183, 77, 0.6);
|
||||||
|
color: #ffcc80;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 183, 77, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grayline-badge.sunset {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 138, 101, 0.3) 0%, rgba(239, 83, 80, 0.3) 100%);
|
||||||
|
border: 1px solid rgba(255, 138, 101, 0.6);
|
||||||
|
color: #ffab91;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 138, 101, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes graylinePulse {
|
||||||
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.85; transform: scale(1.02); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-event {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-location {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Weather Section */
|
||||||
|
.weather-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
animation: alertPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind-alert {
|
||||||
|
background: rgba(245, 158, 11, 0.2);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gust-alert {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes alertPulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-text strong {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gust-alert .alert-text strong {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-ok {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok-icon {
|
||||||
|
color: #22c55e;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ok-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(34, 197, 94, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.status-banner {
|
||||||
|
padding: 8px 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frequency {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-section,
|
||||||
|
.grayline-section,
|
||||||
|
.weather-section {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user