updated frontend

This commit is contained in:
2026-01-14 17:35:07 +01:00
parent 5332ab9dc1
commit b8884d89e3
5 changed files with 719 additions and 46 deletions

View File

@@ -32,6 +32,9 @@ type Client struct {
// Callbacks // Callbacks
onFrequencyChange func(freqMHz float64) onFrequencyChange func(freqMHz float64)
checkTransmitAllowed func() bool // Returns true if transmit allowed (motors not moving) checkTransmitAllowed func() bool // Returns true if transmit allowed (motors not moving)
// Reconnection settings
reconnectInterval time.Duration
} }
func New(host string, port int) *Client { func New(host string, port int) *Client {
@@ -42,6 +45,7 @@ func New(host string, port int) *Client {
lastStatus: &Status{ lastStatus: &Status{
Connected: false, Connected: false,
}, },
reconnectInterval: 5 * time.Second, // Reconnect every 5 seconds if disconnected
} }
} }
@@ -68,6 +72,7 @@ func (c *Client) Connect() error {
conn, err := net.DialTimeout("tcp", addr, 5*time.Second) conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil { if err != nil {
log.Printf("FlexRadio: Connection failed: %v", err)
return fmt.Errorf("failed to connect: %w", err) return fmt.Errorf("failed to connect: %w", err)
} }
@@ -83,10 +88,14 @@ func (c *Client) Start() error {
return nil return nil
} }
if err := c.Connect(); err != nil { c.running = true
return err
}
// Try initial connection but don't fail if it doesn't work
// The messageLoop will handle reconnection
err := c.Connect()
if err != nil {
log.Printf("FlexRadio: Initial connection failed, will retry: %v", err)
} else {
// Update connected status // Update connected status
c.statusMu.Lock() c.statusMu.Lock()
if c.lastStatus != nil { if c.lastStatus != nil {
@@ -94,19 +103,22 @@ func (c *Client) Start() error {
} }
c.statusMu.Unlock() c.statusMu.Unlock()
c.running = true // Subscribe to slice updates for frequency tracking
c.subscribeToSlices()
}
// Start message listener // Start message listener (handles reconnection)
go c.messageLoop() go c.messageLoop()
// Subscribe to slice updates for frequency tracking return nil
}
func (c *Client) subscribeToSlices() {
log.Println("FlexRadio: Subscribing to slice updates...") log.Println("FlexRadio: Subscribing to slice updates...")
_, err := c.sendCommand("sub slice all") _, err := c.sendCommand("sub slice all")
if err != nil { if err != nil {
log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err) log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err)
} }
return nil
} }
func (c *Client) Stop() { func (c *Client) Stop() {
@@ -173,15 +185,52 @@ func (c *Client) sendCommand(cmd string) (string, error) {
func (c *Client) messageLoop() { func (c *Client) messageLoop() {
log.Println("FlexRadio: Message loop started") log.Println("FlexRadio: Message loop started")
reconnectTicker := time.NewTicker(c.reconnectInterval)
defer reconnectTicker.Stop()
for c.running { for c.running {
c.connMu.Lock() c.connMu.Lock()
if c.conn == nil || c.reader == nil { isConnected := c.conn != nil && c.reader != nil
c.connMu.Unlock() c.connMu.Unlock()
time.Sleep(1 * time.Second)
if !isConnected {
// Update status to disconnected
c.statusMu.Lock()
if c.lastStatus != nil {
c.lastStatus.Connected = false
}
c.statusMu.Unlock()
// Wait for reconnect interval
select {
case <-reconnectTicker.C:
log.Println("FlexRadio: Attempting to reconnect...")
if err := c.Connect(); err != nil { if err := c.Connect(); err != nil {
log.Printf("FlexRadio: Reconnect failed: %v", err) log.Printf("FlexRadio: Reconnect failed: %v", err)
continue continue
} }
// Successfully reconnected
c.statusMu.Lock()
if c.lastStatus != nil {
c.lastStatus.Connected = true
}
c.statusMu.Unlock()
// Re-subscribe to slices after reconnection
c.subscribeToSlices()
case <-c.stopChan:
log.Println("FlexRadio: Message loop stopping (stop signal received)")
return
}
continue
}
// Read from connection
c.connMu.Lock()
if c.conn == nil || c.reader == nil {
c.connMu.Unlock()
continue continue
} }
@@ -211,6 +260,8 @@ func (c *Client) messageLoop() {
c.lastStatus.Connected = false c.lastStatus.Connected = false
} }
c.statusMu.Unlock() c.statusMu.Unlock()
log.Println("FlexRadio: Connection lost, will attempt reconnection...")
continue continue
} }

View File

@@ -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 {

View File

@@ -73,7 +73,6 @@
<div class="power-bar-container"> <div class="power-bar-container">
<div class="power-bar-bg"> <div class="power-bar-bg">
<div class="power-bar-fill" style="width: {powerPercent}%"> <div class="power-bar-fill" style="width: {powerPercent}%">
<div class="power-bar-glow"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -282,16 +281,6 @@
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.power-bar-glow {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5));
animation: shimmer 2s infinite;
}
@keyframes shimmer { @keyframes shimmer {
0% { transform: translateX(-100%); } 0% { transform: translateX(-100%); }
100% { transform: translateX(100%); } 100% { transform: translateX(100%); }

View 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>

View File

@@ -67,7 +67,6 @@
<div class="power-bar-container"> <div class="power-bar-container">
<div class="power-bar-bg"> <div class="power-bar-bg">
<div class="power-bar-fill" style="width: {powerPercent}%"> <div class="power-bar-fill" style="width: {powerPercent}%">
<div class="power-bar-glow"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -265,16 +264,6 @@
transition: width 0.3s ease; transition: width 0.3s ease;
} }
.power-bar-glow {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5));
animation: shimmer 2s infinite;
}
@keyframes shimmer { @keyframes shimmer {
0% { transform: translateX(-100%); } 0% { transform: translateX(-100%); }
100% { transform: translateX(100%); } 100% { transform: translateX(100%); }