Files
ShackMaster/web/src/App.svelte
2026-01-11 15:33:44 +01:00

296 lines
6.5 KiB
Svelte

<script>
import { onMount, onDestroy } from 'svelte';
import { wsService, connected, systemStatus } from './lib/websocket.js';
import { api } from './lib/api.js';
import WebSwitch from './components/WebSwitch.svelte';
import PowerGenius from './components/PowerGenius.svelte';
import TunerGenius from './components/TunerGenius.svelte';
import AntennaGenius from './components/AntennaGenius.svelte';
import RotatorGenius from './components/RotatorGenius.svelte';
import Ultrabeam from './components/Ultrabeam.svelte';
let status = null;
let isConnected = false;
let currentTime = new Date();
let callsign = 'F4BPO'; // Default
const unsubscribeStatus = systemStatus.subscribe(value => {
status = value;
});
const unsubscribeConnected = connected.subscribe(value => {
isConnected = value;
});
// Solar data from status
$: solarData = status?.solar || {
sfi: 0,
sunspots: 0,
a_index: 0,
k_index: 0,
geomag: 'Unknown'
};
onMount(async () => {
wsService.connect();
// Fetch config to get callsign
try {
const config = await api.getConfig();
if (config.callsign) {
callsign = config.callsign;
}
} catch (err) {
console.error('Failed to fetch config:', err);
}
// Update clock every second
const clockInterval = setInterval(() => {
currentTime = new Date();
}, 1000);
return () => {
clearInterval(clockInterval);
};
});
onDestroy(() => {
wsService.disconnect();
unsubscribeStatus();
unsubscribeConnected();
});
function formatTime(date) {
return date.toTimeString().slice(0, 8);
}
// Weather data from status
$: weatherData = status?.weather || {
wind_speed: 0,
wind_gust: 0,
temp: 0,
feels_like: 0
};
</script>
<div class="app">
<header>
<div class="header-left">
<h1>{callsign} Shack</h1>
<div class="connection-status">
<span class="status-indicator" class:status-online={isConnected} class:status-offline={!isConnected}></span>
{isConnected ? 'Connected' : 'Disconnected'}
</div>
</div>
<div class="header-center">
<div class="solar-info">
<span class="solar-item">SFI <span class="value">{solarData.sfi}</span></span>
<span class="solar-item">Spots <span class="value">{solarData.sunspots}</span></span>
<span class="solar-item">A <span class="value">{solarData.a_index}</span></span>
<span class="solar-item">K <span class="value">{solarData.k_index}</span></span>
<span class="solar-item">G <span class="value">{solarData.geomag}</span></span>
</div>
</div>
<div class="header-right">
<div class="weather-info">
<span title="Wind">🌬️ {weatherData.wind_speed.toFixed(1)}m/s</span>
<span title="Gust">💨 {weatherData.wind_gust.toFixed(1)}m/s</span>
<span title="Temperature">🌡️ {weatherData.temp.toFixed(1)}°C</span>
<span title="Feels like">{weatherData.feels_like.toFixed(1)}°C</span>
</div>
<div class="clock">
<span class="time">{formatTime(currentTime)}</span>
<span class="date">{currentTime.toLocaleDateString()}</span>
</div>
</div>
</header>
<main>
<div class="dashboard-grid">
<div class="row">
<WebSwitch status={status?.webswitch} />
<PowerGenius status={status?.power_genius} />
<TunerGenius status={status?.tuner_genius} flexradio={status?.flexradio} />
</div>
<div class="row">
<AntennaGenius status={status?.antenna_genius} />
<Ultrabeam status={status?.ultrabeam} flexradio={status?.flexradio} />
<RotatorGenius status={status?.rotator_genius} ultrabeam={status?.ultrabeam} />
</div>
</div>
</main>
</div>
<style>
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
flex-wrap: wrap;
gap: 16px;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
h1 {
font-size: 24px;
font-weight: 500;
margin: 0;
color: white;
}
.connection-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
padding: 6px 12px;
background: rgba(0, 0, 0, 0.3);
border-radius: 16px;
}
.header-center {
flex: 1;
display: flex;
justify-content: center;
}
.solar-info {
display: flex;
gap: 20px;
font-size: 14px;
}
.solar-item {
color: rgba(255, 255, 255, 0.9);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
}
.solar-item .value {
font-weight: 700;
margin-left: 4px;
font-size: 14px;
}
.solar-item:nth-child(1) .value { /* SFI */
color: #ffa726;
text-shadow: 0 0 8px rgba(255, 167, 38, 0.5);
}
.solar-item:nth-child(2) .value { /* Spots */
color: #66bb6a;
text-shadow: 0 0 8px rgba(102, 187, 106, 0.5);
}
.solar-item:nth-child(3) .value { /* A */
color: #42a5f5;
text-shadow: 0 0 8px rgba(66, 165, 245, 0.5);
}
.solar-item:nth-child(4) .value { /* K */
color: #ef5350;
text-shadow: 0 0 8px rgba(239, 83, 80, 0.5);
}
.solar-item:nth-child(5) .value { /* G */
color: #ab47bc;
text-shadow: 0 0 8px rgba(171, 71, 188, 0.5);
}
.header-right {
display: flex;
gap: 20px;
align-items: center;
}
.weather-info {
display: flex;
gap: 12px;
font-size: 14px;
color: rgba(255, 255, 255, 0.9);
}
.clock {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.time {
font-size: 18px;
font-weight: 500;
color: white;
}
.date {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
main {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.dashboard-grid {
display: flex;
flex-direction: column;
gap: 24px;
max-width: 1800px;
margin: 0 auto;
}
.row {
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.row > :global(*) {
flex: 1;
min-width: 300px;
}
@media (max-width: 1200px) {
.row {
flex-direction: column;
}
}
@media (max-width: 768px) {
header {
flex-direction: column;
align-items: flex-start;
}
.header-center,
.header-right {
width: 100%;
justify-content: flex-start;
}
.solar-info {
flex-wrap: wrap;
}
}
</style>