Merge branch 'accius:main' into main

pull/106/head
trancen 2 days ago committed by GitHub
commit 144e7959a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -67,6 +67,13 @@ LAYOUT=modern
# Only uncomment if you have your own proxy running
# DXSPIDER_PROXY_URL=https://your-dxspider-proxy.com
# DX Cluster source: auto | proxy | hamqth | dxspider
# auto = tries proxy first, then HamQTH, then direct telnet
# proxy = use DX Spider Proxy (set DXSPIDER_PROXY_URL above)
# hamqth = HamQTH CSV feed (HTTP, works everywhere)
# dxspider = direct telnet to DX Spider nodes (works locally/Pi)
# DX_CLUSTER_SOURCE=auto
# OpenWeatherMap API key (for local weather display)
# Get a free key at https://openweathermap.org/api
# OPENWEATHER_API_KEY=your_api_key_here

@ -225,6 +225,9 @@ else
CHROME_CMD="chromium-browser"
fi
# Trap Ctrl+Q to exit kiosk cleanly
trap 'pkill -f "chromium.*kiosk"; exit 0' SIGTERM SIGINT
$CHROME_CMD \
--kiosk \
--noerrdialogs \
@ -237,7 +240,17 @@ $CHROME_CMD \
--overscroll-history-navigation=0 \
--disable-pinch \
--incognito \
http://localhost:3000
http://localhost:3000 &
CHROME_PID=$!
echo "OpenHamClock kiosk running (PID: $CHROME_PID)"
echo "Exit methods:"
echo " - Alt+F4 (close Chromium)"
echo " - Ctrl+Alt+T (open terminal, then: pkill -f kiosk)"
echo " - SSH in and run: pkill -f kiosk.sh"
wait $CHROME_PID
EOF
chmod +x "$INSTALL_DIR/kiosk.sh"
@ -339,7 +352,14 @@ print_summary() {
if [ "$KIOSK_MODE" = true ]; then
echo -e " ${GREEN}Kiosk Mode:${NC} Enabled"
echo " OpenHamClock will auto-start on boot in fullscreen"
echo " To disable: rm ~/.config/autostart/openhamclock-kiosk.desktop"
echo ""
echo -e " ${YELLOW}Exit kiosk:${NC}"
echo " Alt+F4 Close Chromium"
echo " Ctrl+Alt+T Open terminal (then: pkill -f kiosk)"
echo " SSH: pkill -f kiosk.sh"
echo ""
echo -e " ${YELLOW}Disable auto-start:${NC}"
echo " rm ~/.config/autostart/openhamclock-kiosk.desktop"
echo ""
fi

@ -148,7 +148,7 @@ const CONFIG = {
// DX Cluster settings
spotRetentionMinutes: parseInt(process.env.SPOT_RETENTION_MINUTES) || jsonConfig.dxCluster?.spotRetentionMinutes || 30,
dxClusterSource: jsonConfig.dxCluster?.source || 'auto',
dxClusterSource: process.env.DX_CLUSTER_SOURCE || jsonConfig.dxCluster?.source || 'auto',
// API keys (don't expose to frontend)
_openWeatherApiKey: process.env.OPENWEATHER_API_KEY || '',
@ -983,7 +983,7 @@ let dxSpiderCache = { spots: [], timestamp: 0 };
const DXSPIDER_CACHE_TTL = 90000; // 90 seconds cache - reduces reconnection frequency
app.get('/api/dxcluster/spots', async (req, res) => {
const source = (req.query.source || 'auto').toLowerCase();
const source = (req.query.source || CONFIG.dxClusterSource || 'auto').toLowerCase();
// Helper function for HamQTH (HTTP-based, works everywhere)
async function fetchHamQTH() {

@ -460,7 +460,7 @@ const App = () => {
</div>
<div style={{ marginBottom: '6px' }}>
<div style={{ color: '#888' }}>Kp</div>
<div style={{ color: '#00ff00', fontSize: '16px', fontWeight: '700' }}>{spaceWeather?.data?.kIndex ?? '--'}</div>
<div style={{ color: '#00ff00', fontSize: '16px', fontWeight: '700' }}>{solarIndices?.data?.kp?.current ?? spaceWeather?.data?.kIndex ?? '--'}</div>
</div>
<div style={{ marginBottom: '6px' }}>
<div style={{ color: '#888' }}>Bz</div>
@ -596,6 +596,7 @@ const App = () => {
localDate={localDate}
localWeather={localWeather}
spaceWeather={spaceWeather}
solarIndices={solarIndices}
use12Hour={use12Hour}
onTimeFormatToggle={handleTimeFormatToggle}
onSettingsClick={() => setShowSettings(true)}

@ -279,7 +279,7 @@ export const DXFilterManager = ({ filters, onFilterChange, isOpen, onClose }) =>
<div>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '13px', fontWeight: '600', color: 'var(--text-primary)', marginBottom: '8px' }}>
Exclude List - Hide these callsigns
Exclude List - Hide DX callsigns beginning with:
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<input

@ -12,6 +12,7 @@ export const Header = ({
localDate,
localWeather,
spaceWeather,
solarIndices,
use12Hour,
onTimeFormatToggle,
onSettingsClick,
@ -37,8 +38,8 @@ export const Header = ({
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexShrink: 0 }}>
<span
style={{
fontSize: config.callsignSize > 0.1 && config.callsignSize <= 2
? `${22 * config.callsignSize}px`
fontSize: config.headerSize > 0.1 && config.headerSize <= 2
? `${22 * config.headerSize}px`
: "22px", fontWeight: '900', color: 'var(--accent-amber)', cursor: 'pointer', fontFamily: 'Orbitron, monospace', whiteSpace: 'nowrap'
}}
onClick={onSettingsClick}
@ -53,7 +54,9 @@ export const Header = ({
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexShrink: 0 }}>
<span style={{ fontSize: '13px', color: 'var(--accent-cyan)', fontWeight: '600' }}>UTC</span>
<span style={{
fontSize: '24px',
fontSize: config.headerSize > 0.1 && config.headerSize <= 2
? `${24 * config.headerSize}px`
: "24px",
fontWeight: '700',
color: 'var(--accent-cyan)',
fontFamily: 'JetBrains Mono, Consolas, monospace',
@ -70,7 +73,9 @@ export const Header = ({
>
<span style={{ fontSize: '13px', color: 'var(--accent-amber)', fontWeight: '600' }}>LOCAL</span>
<span style={{
fontSize: '24px',
fontSize: config.headerSize > 0.1 && config.headerSize <= 2
? `${24 * config.headerSize}px`
: "24px",
fontWeight: '700',
color: 'var(--accent-amber)',
fontFamily: 'JetBrains Mono, Consolas, monospace',
@ -90,8 +95,18 @@ export const Header = ({
const windLabel = localWeather.data.windUnit || 'mph';
return (
<div title={`${localWeather.data.description} • Wind: ${localWeather.data.windSpeed} ${windLabel}`}>
<span style={{ marginRight: '3px' }}>{localWeather.data.icon}</span>
<span style={{ color: 'var(--accent-cyan)', fontWeight: '600' }}>
<span style={{ marginRight: '3px',
fontSize: config.headerSize > 0.1 && config.headerSize <= 2
? `${12 * config.headerSize}px`
: "12px",
}}>
{localWeather.data.icon}
</span>
<span style={{ color: 'var(--accent-cyan)', fontWeight: '600',
fontSize: config.headerSize > 0.1 && config.headerSize <= 2
? `${12 * config.headerSize}px`
: "12px",
}}>
{tempF}°F/{tempC}°C
</span>
</div>
@ -99,17 +114,17 @@ export const Header = ({
})()}
<div>
<span style={{ color: 'var(--text-muted)' }}>SFI </span>
<span style={{ color: 'var(--accent-amber)', fontWeight: '700' }}>{spaceWeather?.data?.solarFlux || '--'}</span>
<span style={{ color: 'var(--accent-amber)', fontWeight: '700' }}>{solarIndices?.data?.sfi?.current || spaceWeather?.data?.solarFlux || '--'}</span>
</div>
<div>
<span style={{ color: 'var(--text-muted)' }}>K </span>
<span style={{ color: parseInt(spaceWeather?.data?.kIndex) >= 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '700' }}>
{spaceWeather?.data?.kIndex ?? '--'}
<span style={{ color: parseInt(solarIndices?.data?.kp?.current ?? spaceWeather?.data?.kIndex) >= 4 ? 'var(--accent-red)' : 'var(--accent-green)', fontWeight: '700' }}>
{solarIndices?.data?.kp?.current ?? spaceWeather?.data?.kIndex ?? '--'}
</span>
</div>
<div>
<span style={{ color: 'var(--text-muted)' }}>SSN </span>
<span style={{ color: 'var(--accent-cyan)', fontWeight: '700' }}>{spaceWeather?.data?.sunspotNumber || '--'}</span>
<span style={{ color: 'var(--accent-cyan)', fontWeight: '700' }}>{solarIndices?.data?.ssn?.current || spaceWeather?.data?.sunspotNumber || '--'}</span>
</div>
</div>

@ -9,7 +9,7 @@ import { LANGUAGES } from '../lang/i18n.js';
export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
const [callsign, setCallsign] = useState(config?.callsign || '');
const [callsignSize, setCallsignSize] = useState(config?.callsignSize || 1.0);
const [headerSize, setheaderSize] = useState(config?.headerSize || 1.0);
const [gridSquare, setGridSquare] = useState('');
const [lat, setLat] = useState(config?.location?.lat || 0);
const [lon, setLon] = useState(config?.location?.lon || 0);
@ -26,7 +26,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
useEffect(() => {
if (config) {
setCallsign(config.callsign || '');
setCallsignSize(config.callsignSize || 1.0)
setheaderSize(config.headerSize || 1.0)
setLat(config.location?.lat || 0);
setLon(config.location?.lon || 0);
setTheme(config.theme || 'dark');
@ -149,7 +149,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
onSave({
...config,
callsign: callsign.toUpperCase(),
callsignSize: callsignSize,
headerSize: headerSize,
location: { lat: parseFloat(lat), lon: parseFloat(lon) },
theme,
layout,
@ -309,15 +309,15 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
<div style={{ marginBottom: '20px'}}>
<div>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase' }}>
{t('station.settings.callsignSize')}
{t('station.settings.headerSize')}
</label>
<input
type="number"
step="0.1"
value={isNaN(lat) ? '' : callsignSize}
value={isNaN(lat) ? '' : headerSize}
onChange={(e) => {
if (e.target.value >= 0.1 && e.target.value <= 2.0) {
setCallsignSize(e.target.value)
setheaderSize(e.target.value)
}}}
style={{
width: '100%',

@ -26,17 +26,15 @@ export const useDXCluster = (source = 'auto', filters = {}) => {
// Watchlist only mode - must match watchlist
if (filters.watchlistOnly && filters.watchlist?.length > 0) {
const matchesWatchlist = filters.watchlist.some(w =>
spot.call?.toUpperCase().includes(w.toUpperCase()) ||
spot.spotter?.toUpperCase().includes(w.toUpperCase())
spot.call?.toUpperCase().includes(w.toUpperCase())
);
if (!matchesWatchlist) return false;
}
// Exclude list - hide matching calls
// Exclude list - hide matching calls - match the call as a prefix
if (filters.excludeList?.length > 0) {
const isExcluded = filters.excludeList.some(exc =>
spot.call?.toUpperCase().includes(exc.toUpperCase()) ||
spot.spotter?.toUpperCase().includes(exc.toUpperCase())
spot.call?.toUpperCase().startsWith(exc.toUpperCase())
);
if (isExcluded) return false;
}
@ -89,7 +87,7 @@ export const useDXCluster = (source = 'auto', filters = {}) => {
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/dxcluster/spots');
const response = await fetch(`/api/dxcluster/spots?source=${encodeURIComponent(source)}`);
if (response.ok) {
const newSpots = await response.json();

@ -14,20 +14,20 @@ const API_URL = '/api/wsjtx';
const DECODES_URL = '/api/wsjtx/decodes';
// Generate or retrieve persistent session ID
// NOTE: Kept short (8 chars) intentionally — long UUIDs in query strings
// trigger false positives in Bitdefender and similar security software
function getSessionId() {
const KEY = 'ohc-wsjtx-session';
const generate = () => Math.random().toString(36).substring(2, 10);
try {
let id = localStorage.getItem(KEY);
if (id && id.length >= 16) return id;
// Generate a random ID
id = (typeof crypto !== 'undefined' && crypto.randomUUID)
? crypto.randomUUID()
: Math.random().toString(36).substring(2) + Date.now().toString(36) + Math.random().toString(36).substring(2);
if (id && id.length >= 8) return id;
id = generate();
localStorage.setItem(KEY, id);
return id;
} catch {
// Fallback for privacy browsers that block localStorage
return Math.random().toString(36).substring(2) + Date.now().toString(36) + Math.random().toString(36).substring(2);
return generate();
}
}

@ -9,6 +9,7 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "Höhe (m)",
"station.settings.antenna": "Antenne",
"station.settings.button.save": "Einstellungen Speichern",
@ -45,5 +46,12 @@
"station.settings.useLocation": "📍 Meinen Standort verwenden",
"station.settings.useLocation.error1": "Standort konnte nicht ermittelt werden. Bitte manuell eingeben.",
"station.settings.useLocation.error2": "Geolokalisierung wird von deinem Browser nicht unterstützt.",
"station.settings.welcome": "👋 Willkommen bei OpenHamClock!"
"station.settings.welcome": "👋 Willkommen bei OpenHamClock!",
"plugins.layers.aurora.name": "Aurora-Vorhersage",
"plugins.layers.aurora.description": "NOAA-OVATION-Aurora-Wahrscheinlichkeitsvorhersage (30 Min.)",
"plugins.layers.earthquakes.name": "Erdbeben",
"plugins.layers.earthquakes.description": "Live-USGS-Erdbebendaten (M2,5+ der letzten 24 Stunden)",
"plugins.layers.wxradar.name": "Wetterradar",
"plugins.layers.wxradar.description": "NEXRAD-Wetterradar-Überlagerung für Nordamerika",
"plugins.layers.wxradar.attribution": "Wetterdaten © Iowa State University Mesonet"
}

@ -9,12 +9,13 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "Altitude (m)",
"station.settings.antenna": "Antenna",
"station.settings.button.save": "Save Settings",
"station.settings.button.save.confirm": "Settings saved to your browser",
"station.settings.callsign": "Your Callsign",
"station.settings.callsignSize": "Your Callsign's size",
"station.settings.headerSize": "Your Callsign's size",
"station.settings.describe": "Enter your callsign and grid square to get started. Settings are saved in your browser.",
"station.settings.dx.describe": "→ Real-time DX Spider feed via our dedicated proxy service",
"station.settings.dx.option1": "⭐ DX Spider Proxy (Recommended)",
@ -46,5 +47,12 @@
"station.settings.useLocation": "📍 Use my current location",
"station.settings.useLocation.error1": "Could not get location. Please enter manually.",
"station.settings.useLocation.error2": "Geolocation is not supported by your browser.",
"station.settings.welcome": "👋 Welcome to OpenHamClock!"
"station.settings.welcome": "👋 Welcome to OpenHamClock!",
"plugins.layers.aurora.name": "Aurora Forecast",
"plugins.layers.aurora.description": "NOAA OVATION aurora probability forecast (30-min)",
"plugins.layers.earthquakes.name": "Earthquakes",
"plugins.layers.earthquakes.description": "Live USGS earthquake data (M2.5+ from last 24 hours)",
"plugins.layers.wxradar.name": "Weather Radar",
"plugins.layers.wxradar.description": "NEXRAD weather radar overlay for North America",
"plugins.layers.wxradar.attribution": "Weather data © Iowa State University Mesonet"
}

@ -9,12 +9,13 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "Altitud (m)",
"station.settings.antenna": "Antena",
"station.settings.button.save": "Guardar Configuración",
"station.settings.button.save.confirm": "La configuración se guarda en tu navegador",
"station.settings.callsign": "Tu Indicativo",
"station.settings.callsignSize": "El tamaño de tu Indicativo",
"station.settings.headerSize": "El tamaño de tu Indicativo",
"station.settings.describe": "Ingresa tu indicativo y cuadrícula para comenzar. Tu configuración se guardará en el navegador.",
"station.settings.dx.describe": "→ Feed en tiempo real de DX Spider a través de nuestro servicio proxy dedicado",
"station.settings.dx.option1": "⭐ Proxy DX Spider (Recomendado)",
@ -46,5 +47,12 @@
"station.settings.useLocation": "📍 Usar Mi Ubicación Actual",
"station.settings.useLocation.error1": "No se pudo obtener la ubicación. Por favor ingrésala manualmente.",
"station.settings.useLocation.error2": "La geolocalización no es compatible con tu navegador.",
"station.settings.welcome": "👋 ¡Bienvenido a OpenHamClock!"
"station.settings.welcome": "👋 ¡Bienvenido a OpenHamClock!",
"plugins.layers.aurora.name": "Pronóstico de auroras",
"plugins.layers.aurora.description": "Pronóstico de probabilidad de auroras NOAA OVATION (30 min)",
"plugins.layers.earthquakes.name": "Terremotos",
"plugins.layers.earthquakes.description": "Datos sísmicos en vivo del USGS (M2.5+ de las últimas 24 horas)",
"plugins.layers.wxradar.name": "Radar meteorológico",
"plugins.layers.wxradar.description": "Superposición del radar meteorológico NEXRAD para Norteamérica",
"plugins.layers.wxradar.attribution": "Datos meteorológicos © Iowa State University Mesonet"
}

@ -9,6 +9,7 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "Altitude (m)",
"station.settings.antenna": "Antenne",
"station.settings.button.save": "Enregistrer les paramètres",
@ -45,5 +46,12 @@
"station.settings.useLocation": "📍 Utiliser ma position actuelle",
"station.settings.useLocation.error1": "Impossible d'obtenir la position. Veuillez entrer manuellement.",
"station.settings.useLocation.error2": "La géolocalisation n'est pas prise en charge par votre navigateur.",
"station.settings.welcome": "👋 Bienvenue sur OpenHamClock !"
"station.settings.welcome": "👋 Bienvenue sur OpenHamClock !",
"plugins.layers.aurora.name": "Prévision daurores",
"plugins.layers.aurora.description": "Prévision de probabilité daurores NOAA OVATION (30 min)",
"plugins.layers.earthquakes.name": "Séismes",
"plugins.layers.earthquakes.description": "Données sismiques USGS en direct (M2,5+ sur les dernières 24 heures)",
"plugins.layers.wxradar.name": "Radar météo",
"plugins.layers.wxradar.description": "Surcouche du radar météo NEXRAD pour lAmérique du Nord",
"plugins.layers.wxradar.attribution": "Données météo © Iowa State University Mesonet"
}

@ -10,6 +10,7 @@ import translationPT from './pt.json';
import translationJA from './ja.json';
import translationIT from './it.json';
import translationNL from './nl.json';
import translationKO from './ko.json';
export const LANGUAGES = [
{ code: 'en', name: 'English', flag: '🇬🇧' },
@ -19,6 +20,7 @@ export const LANGUAGES = [
{ code: 'nl', name: 'Nederlands', flag: '🇳🇱' },
{ code: 'pt', name: 'Português', flag: '🇧🇷' },
{ code: 'ja', name: '日本語', flag: '🇯🇵' },
{ code: 'ko', name: '한국어', flag: '🇰🇷' },
{ code: 'it', name: 'Italiano', flag: '🇮🇹' }
];
@ -30,6 +32,7 @@ export const resources = {
nl: { translation: translationNL },
pt: { translation: translationPT },
ja: { translation: translationJA },
ko: { translation: translationKO },
it: { translation: translationIT }
};

@ -9,6 +9,7 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "Altitudine (m)",
"station.settings.antenna": "Antenna",
"station.settings.button.save": "Salva Impostazioni",
@ -45,5 +46,12 @@
"station.settings.useLocation": "📍 Usa la Mia Posizione Attuale",
"station.settings.useLocation.error1": "Impossibile ottenere la posizione. Inseriscila manualmente.",
"station.settings.useLocation.error2": "La geolocalizzazione non è supportata dal tuo browser.",
"station.settings.welcome": "👋 Benvenuto su OpenHamClock!"
"station.settings.welcome": "👋 Benvenuto su OpenHamClock!",
"plugins.layers.aurora.name": "Previsione dellaurora",
"plugins.layers.aurora.description": "Previsione di probabilità dellaurora NOAA OVATION (30 min)",
"plugins.layers.earthquakes.name": "Terremoti",
"plugins.layers.earthquakes.description": "Dati sismici USGS in tempo reale (M2,5+ delle ultime 24 ore)",
"plugins.layers.wxradar.name": "Radar meteorologico",
"plugins.layers.wxradar.description": "Sovrapposizione del radar meteorologico NEXRAD per il Nord America",
"plugins.layers.wxradar.attribution": "Dati meteo © Iowa State University Mesonet"
}

@ -9,6 +9,7 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "標高 (m)",
"station.settings.antenna": "アンテナ",
"station.settings.button.save": "設定を保存",
@ -45,5 +46,12 @@
"station.settings.useLocation": "📍 現在地を使用",
"station.settings.useLocation.error1": "位置情報を取得できません。手動で入力してください。",
"station.settings.useLocation.error2": "お使いのブラウザはジオロケーションに対応していません。",
"station.settings.welcome": "👋 OpenHamClockへようこそ"
"station.settings.welcome": "👋 OpenHamClockへようこそ",
"plugins.layers.aurora.name": "オーロラ予報",
"plugins.layers.aurora.description": "NOAA OVATION オーロラ出現確率予報30分",
"plugins.layers.earthquakes.name": "地震",
"plugins.layers.earthquakes.description": "USGSのリアルタイム地震データ過去24時間のM2.5以上)",
"plugins.layers.wxradar.name": "気象レーダー",
"plugins.layers.wxradar.description": "北米向けNEXRAD気象レーダーのオーバーレイ",
"plugins.layers.wxradar.attribution": "気象データ © Iowa State University Mesonet"
}

@ -0,0 +1,57 @@
{
"cancel": "취소",
"station.settings.language": "언어",
"station.settings.language.en": "English",
"station.settings.language.fr": "Français",
"station.settings.language.es": "Español",
"station.settings.language.de": "Deutsch",
"station.settings.language.pt": "Português",
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "고도 (m)",
"station.settings.antenna": "안테나",
"station.settings.button.save": "설정 저장하기",
"station.settings.button.save.confirm": "설정이 브라우저에 저장되었습니다.",
"station.settings.callsign": "콜사인",
"station.settings.describe": "콜사인과 그리드 스퀘어(예: PM37mp)를 입력하여 시작하세요. 설정은 브라우저에 저장됩니다.",
"station.settings.dx.describe": "→ 자체 프록시 서버를 이용한 실시간 DX Spider 연동",
"station.settings.dx.option1": "⭐ DX Spider 프록시 (권장)",
"station.settings.dx.option2": "HamQTH Cluster",
"station.settings.dx.option3": "DXWatch",
"station.settings.dx.option4": "자동 (전부 시도하기)",
"station.settings.dx.title": "DX Cluster 데이터 서버",
"station.settings.layout": "레이아웃",
"station.settings.layout.classic": "클래식",
"station.settings.layout.classic.describe": "→ HamClock과 유사한 레이아웃",
"station.settings.layout.modern": "모던",
"station.settings.layout.modern.describe": "→ 반응형 모던 그리드 레이아웃",
"station.settings.latitude": "위도",
"station.settings.locator": "그리드 스퀘어 (또는 아래에 직접 위도/경도 입력)",
"station.settings.longitude": "경도",
"station.settings.power": "출력 (W)",
"station.settings.theme": "테마",
"station.settings.theme.dark": "다크",
"station.settings.theme.dark.describe": "→ 현대적인 다크 테마 (기본)",
"station.settings.theme.legacy": "CRT",
"station.settings.theme.legacy.describe": "→ 초록 CRT 터미널 스타일",
"station.settings.theme.light": "라이트",
"station.settings.theme.light.describe": "→ 낮 시간 사용을 위한 밝은 테마",
"station.settings.theme.retro": "레트로",
"station.settings.theme.retro.describe": "→ 90년대 Windows 레트로 스타일",
"station.settings.timezone": "시간대",
"station.settings.title": "무선국 설정",
"station.settings.tip.env": "💡 팁: 영구적인 데이터 저장을 위해 <envExample>.env.example</envExample> 파일을 복사해 <env>.env</env> 파일을 덮어쓰고 CALLSIGN 과 LOCATOR 필드를 채우세요.",
"station.settings.useLocation": "📍 현재 내 위치 사용",
"station.settings.useLocation.error1": "위치 정보를 가져올 수 없습니다. 위도와 경도를 수동으로 입력해 주세요.",
"station.settings.useLocation.error2": "브라우저가 위치 정보 기능을 지원하지 않습니다.",
"station.settings.welcome": "👋 OpenHamClock에 오신 것을 환영합니다!",
"plugins.layers.aurora.name": "오로라 예보",
"plugins.layers.aurora.description": "NOAA OVATION 오로라 확률 예보 (30분)",
"plugins.layers.earthquakes.name": "실시간 지진 현황",
"plugins.layers.earthquakes.description": "USGS 실시간 지진 데이터 (지난 24시간 동안 일어난 규모 M2.5 이상의 지진)",
"plugins.layers.wxradar.name": "기상 레이더",
"plugins.layers.wxradar.description": "북아메리카 지역 NEXRAD 기상 레이더 오버레이",
"plugins.layers.wxradar.attribution": "기상 데이터 © Iowa State University Mesonet"
}

@ -9,6 +9,7 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiaans",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "Hoogte (m)",
"station.settings.antenna": "Antenne",
"station.settings.button.save": "Bewaar Settings",
@ -45,5 +46,12 @@
"station.settings.useLocation": "📍 Gebruik mijn huidige locatie",
"station.settings.useLocation.error1": "Kan niet de locatie vinden. Graag handmatig ingeven.",
"station.settings.useLocation.error2": "Geolocation is niet beschikbaar op je browser.",
"station.settings.welcome": "👋 Welkom bij OpenHamClock!"
"station.settings.welcome": "👋 Welkom bij OpenHamClock!",
"plugins.layers.aurora.name": "Auroravoorspelling",
"plugins.layers.aurora.description": "NOAA OVATION-voorspelling van aurorakans (30 min)",
"plugins.layers.earthquakes.name": "Aardbevingen",
"plugins.layers.earthquakes.description": "Live USGS-aardbevingsgegevens (M2,5+ van de afgelopen 24 uur)",
"plugins.layers.wxradar.name": "Weerradar",
"plugins.layers.wxradar.description": "NEXRAD-weerradaroverlay voor Noord-Amerika",
"plugins.layers.wxradar.attribution": "Weergegevens © Iowa State University Mesonet"
}

@ -9,6 +9,7 @@
"station.settings.language.ja": "日本語",
"station.settings.language.it": "Italiano",
"station.settings.language.nl": "Nederlands",
"station.settings.language.ko": "한국어",
"station.settings.altitude": "Altitude (m)",
"station.settings.antenna": "Antena",
"station.settings.button.save": "Salvar Configurações",
@ -45,5 +46,12 @@
"station.settings.useLocation": "📍 Usar Minha Localização Atual",
"station.settings.useLocation.error1": "Não foi possível obter a localização. Por favor, insira manualmente.",
"station.settings.useLocation.error2": "Geolocalização não é suportada pelo seu navegador.",
"station.settings.welcome": "👋 Bem-vindo ao OpenHamClock!"
"station.settings.welcome": "👋 Bem-vindo ao OpenHamClock!",
"plugins.layers.aurora.name": "Previsão de aurora",
"plugins.layers.aurora.description": "Previsão de probabilidade de aurora NOAA OVATION (30 min)",
"plugins.layers.earthquakes.name": "Terremotos",
"plugins.layers.earthquakes.description": "Dados sísmicos do USGS ao vivo (M2,5+ das últimas 24 horas)",
"plugins.layers.wxradar.name": "Radar meteorológico",
"plugins.layers.wxradar.description": "Sobreposição do radar meteorológico NEXRAD para a América do Norte",
"plugins.layers.wxradar.attribution": "Dados meteorológicos © Iowa State University Mesonet"
}

@ -1,3 +1,5 @@
import i18n from '../../lang/i18n';
import { useState, useEffect, useRef } from 'react';
// NOAA OVATION Aurora Forecast - JSON grid data
@ -7,8 +9,8 @@ import { useState, useEffect, useRef } from 'react';
export const metadata = {
id: 'aurora',
name: 'Aurora Forecast',
description: 'NOAA OVATION aurora probability forecast (30-min)',
name: i18n.t('plugins.layers.aurora.name'),
description: i18n.t('plugins.layers.aurora.description'),
icon: '🌌',
category: 'space-weather',
defaultEnabled: false,

@ -1,3 +1,5 @@
import i18n from '../../lang/i18n';
import { useState, useEffect, useRef } from 'react';
//Scaled markers - Bigger circles for stronger quakes
@ -11,8 +13,8 @@ import { useState, useEffect, useRef } from 'react';
export const metadata = {
id: 'earthquakes',
name: 'Earthquakes',
description: 'Live USGS earthquake data (all earthquakes from last hour) with animated detection',
name: i18n.t('plugins.layers.earthquakes.name'),
description: i18n.t('plugins.layers.earthquakes.description'),
icon: '🌋',
category: 'geology',
defaultEnabled: false,

@ -1,9 +1,11 @@
import i18n from '../../lang/i18n';
import { useState, useEffect } from 'react';
export const metadata = {
id: 'wxradar',
name: 'Weather Radar',
description: 'NEXRAD weather radar overlay for North America',
name: i18n.t('plugins.layers.wxradar.name'),
description: i18n.t('plugins.layers.wxradar.description'),
icon: '☁️',
category: 'weather',
defaultEnabled: false,
@ -21,7 +23,7 @@ export function useLayer({ enabled = false, opacity = 0.6, map = null }) {
layers: 'nexrad-n0r-900913',
format: 'image/png',
transparent: true,
attribution: 'Weather data © Iowa State University Mesonet',
attribution: i18n.t('plugins.layers.wxradar.attribution'),
opacity: opacity,
zIndex: 200
}

@ -242,8 +242,7 @@ export const filterDXPaths = (paths, filters) => {
// Exclude list - hide matching callsigns
if (filters.excludeList?.length > 0) {
const isExcluded = filters.excludeList.some(e =>
path.dxCall?.toUpperCase().includes(e.toUpperCase()) ||
path.spotter?.toUpperCase().includes(e.toUpperCase())
path.dxCall?.toUpperCase().startsWith(e.toUpperCase())
);
if (isExcluded) return false;
}

@ -10,7 +10,7 @@
export const DEFAULT_CONFIG = {
callsign: 'N0CALL',
callsignSize: 1.0, // Float multiplies base px size (0.1 to 2.0)
headerSize: 1.0, // Float multiplies base px size (0.1 to 2.0)
locator: '',
location: { lat: 40.0150, lon: -105.2705 }, // Boulder, CO (default)
defaultDX: { lat: 35.6762, lon: 139.6503 }, // Tokyo

@ -130,9 +130,9 @@ export const getMoonPosition = (date) => {
export const getMoonPhase = (date) => {
const JD = date.getTime() / 86400000 + 2440587.5;
const T = (JD - 2451545.0) / 36525;
const D = (297.850 + 445267.1115 * T) % 360; // Mean elongation
// Phase angle (simplified)
const phase = ((D + 180) % 360) / 360;
const D = (297.850 + 445267.1115 * T) % 360; // Mean elongation: 0=new, 180=full
// Normalize to 0-1 range (0=new, 0.5=full)
const phase = (((D % 360) + 360) % 360) / 360;
return phase;
};

Loading…
Cancel
Save

Powered by TurnKey Linux.