From 2c8d1bd881d198fd4ecd5bf8ed81d11211b71f46 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Tue, 27 Oct 2026 09:48:59 +0000 Subject: [PATCH] radio: classic ham-site public view (Opa's Radiohoek) with link directory - PublicRadio.svelte: websdr.org-style grey page, steel-blue bars, dense tables, NL default / EN toggle, radio facts, frequency list per band, banks, ISS, nets - links.ts: all 34 showcase links, status (online/warn/offline) + archive links - owner keeps the dashboard; ?view=public lets the owner preview the public page --- src/lib/radio/PublicRadio.svelte | 1103 +++++++++++++++++++++ src/lib/radio/links.ts | 453 +++++++++ src/routes/trackers/radio/+page.server.ts | 5 +- src/routes/trackers/radio/+page.svelte | 230 +++-- 4 files changed, 1684 insertions(+), 107 deletions(-) create mode 100644 src/lib/radio/PublicRadio.svelte create mode 100644 src/lib/radio/links.ts diff --git a/src/lib/radio/PublicRadio.svelte b/src/lib/radio/PublicRadio.svelte new file mode 100644 index 0000000..c880d59 --- /dev/null +++ b/src/lib/radio/PublicRadio.svelte @@ -0,0 +1,1103 @@ + + +{#snippet note(text: string)} + {#each parts(text) as q, qi (qi)}{#if q.url}{q.x}{:else}{q.x}{/if}{/each} +{/snippet} + +{#snippet antenna()} + +{/snippet} + +
+
+
+ {c.welcome} + {clock('Europe/Amsterdam')} {c.local} · {clock('UTC')} UTC + {#each LANGS as l, i (l)}{#if i} | {/if}{#if l === lang}{l === 'nl' ? 'Nederlands' : 'English'}{:else}{l === 'nl' ? 'Nederlands' : 'English'}{/if}{/each} +
+ + {#if ownerPreview} +

{c.preview} {c.back}

+ {/if} + {#if soonList.length} +

+ {c.latest} + {#each soonList.slice(0, 3) as s, i (s.key)}{#if i} · {/if}{s.what} + {dayLabel(s.at)} {fmtTime(s.at)} + {c.on} {s.freq}{/each} +

+ {/if} + + + +
+
+ {#if nextNet} +
+

{isHague(nextNet.what) ? c.next : c.nextAny}

+
+

{nextNet.freq}

+

+ {nextNet.what} · {dayLabel(nextNet.at)} + {fmtTime(nextNet.at)} +

+

{nextNet.at <= now ? t.onNow : h.until(nextNet.at, now)}

+
+
+ {/if} +
+

{c.station}

+ + + {#each c.stationRows as [k, v] (k)} + + {/each} + +
{k}{v}
+
+
+
+

{c.key}

+
+ + + + {#each keyRows as f (f.id)} + + + + + {/each} + +
{c.mhz}{c.stationCol}
{fq(f.freqHz)} {f.mode}{f.name}{#if f.marineCh} ({t.channel} {f.marineCh}){/if}
+
+
+
+ +
+

{c.upNext}

+

{c.timesNl}

+ {#if soonList.length} +
+ + + + {#each soonList as s (s.key)} + + + + + {/each} + +
{c.when}{c.what}
{dayLabel(s.at)} {fmtTime(s.at)}
{s.at <= now ? t.onNow : h.until(s.at, now)}
{s.what}{#if s.freq}
{s.freq}{/if}
+
+ {:else} +

{t.nothingScheduled}

+ {/if} +
+ +
+

{c.nets}

+ {#if nets.length} +
+ + + + {#each nets as n (n.id)} + {@const f = n.frequencyId ? byId.get(n.frequencyId) : undefined} + + + + + + {/each} + +
{c.name}{c.freq}{c.when}
{n.name}{#if n.notes}
{@render note(n.notes)}{/if}
{#if f}{fq(f.freqHz)}
{f.mode}{#if f.marineCh} + · {t.channel} {f.marineCh}{/if}{/if}
{h.daysLabel(n.days)}
{n.times.length + ? n.times.map((x) => x.slice(0, 5)).join(', ') + : t.timeUnknown}
{t.kinds[n.kind] ?? n.kind}
+
+ {:else} +

{t.noNets}

+ {/if} +
+ +
+

{c.iss}

+

+ {c.issHelp} live.ariss.org +

+ {#if nextPasses.length} +
+ + + + {#each nextPasses as p (p.startAt)} + + + + + + + {/each} + +
{c.date}{c.start}{c.max}{c.end}
{dayLabel(p.startAt)}{fmtTime(p.startAt)}{fmtTime(p.maxAt)} {Math.round(p.maxEl)}°
{dirs(p)}{#if p.sunlit}
{c.eyeShort}{/if}
{fmtTime(p.endAt)}
+
+ {:else} +

{t.noPasses}

+ {/if} +
+ +
+

{c.freqs} ({c.count(frequencies.length)})

+

{c.freqHelp}

+ {#each bands as x (x.band)} +

+ {bandLabel(x.band)} ({x.rows.length}) + {c.top} +

+
+ + {#if x.band === 'marine'}{/if} + + {#each x.rows as f (f.id)} + + + {#if x.band === 'marine'}{/if} + + + + {/each} + +
{c.mhz}{c.ch}{c.stationCol}{c.mem}
{fq(f.freqHz)}
{f.mode}
{f.marineCh ?? ''}{f.name}{#if f.location} + · {f.location}{/if}{#if f.notes}
{@render note(f.notes)}{/if}
{f.channels.join(', ')}
+
+ {/each} +
+ +
+

{c.banks}

+

{t.scanHelp}

+
+ + + + {#each banks as b (b.b)} + + + + + + + {/each} + +
{c.bank}{c.range}{c.used}{c.content}
{b.b}{b.from}–{b.to}{b.n}/{PER_BANK}{b.what.join(', ') || '—'}
+
+

{c.csv}

+
+ +
+

{c.facts}

+
+ + + {#each c.facts1 as [k, v] (k)} + + {/each} + +
{k}{v}
+
+
+ + {#if logTotal} +
+

{c.log}

+
+ + + + {#each log as l (l.id)} + {@const f = l.frequencyId ? byId.get(l.frequencyId) : undefined} + {@const hz = l.freqHz ?? f?.freqHz} + + + + + + {/each} + +
{c.when}{c.freq}{c.what}
{h.fmtDateTime(l.heardAt)}{hz ? fq(hz) : ''} + {l.mode ?? f?.mode ?? ''}{l.what}{#if l.signal}
{t.signal(l.signal)}{/if}
+
+
+ {/if} + + + +

{c.footer} 73! · {c.top}

+
+
+ + diff --git a/src/lib/radio/links.ts b/src/lib/radio/links.ts new file mode 100644 index 0000000..dd8dc8c --- /dev/null +++ b/src/lib/radio/links.ts @@ -0,0 +1,453 @@ +// Link directory for the public radio page (/trackers/radio#links), from the links Opa sent by +// email and WhatsApp (checked 24 Oct 2026). Plain data: add or edit entries here. Every entry is +// shown; offline ones get a label and, when there is one, a web.archive.org copy. Public sites only: +// never add a phone number, an email address or a link into someone's own mailbox. +export type Text = { nl: string; en: string }; +export type LinkStatus = 'online' | 'warn' | 'offline'; +export type RadioLink = { + title: string; + /** An http(s) URL, or "#anchor" for a section on this page. */ + url: string; + about: Text; + /** How it reached us: Opa by email or WhatsApp, or a supporting link we added. */ + via: 'email' | 'whatsapp' | 'added'; + /** Date Opa sent it (YYYY-MM-DD), when known. */ + sent?: string; + status: LinkStatus; + archive?: string; + note?: Text; +}; +export type LinkCategory = { id: string; title: Text; links: RadioLink[] }; + +export const LINKS_CHECKED = '2026-10-24'; + +export const LINK_CATEGORIES: LinkCategory[] = [ + { + id: 'websdr', + title: { + nl: 'WebSDR — luisteren via de browser', + en: 'WebSDR — listen in the browser' + }, + links: [ + { + title: 'websdr.org', + url: 'http://websdr.org/', + about: { + nl: 'Overzicht en kaart van alle WebSDR-ontvangers ter wereld', + en: 'Directory and map of every WebSDR receiver worldwide' + }, + via: 'whatsapp', + status: 'online' + }, + { + title: 'WebSDR Universiteit Twente', + url: 'http://websdr.ewi.utwente.nl:8901/', + about: { + nl: '0–29 MHz, de allereerste WebSDR', + en: '0–29 MHz, the original WebSDR' + }, + via: 'whatsapp', + status: 'online' + }, + { + title: 'WebSDR Maasbree (PH4RTM)', + url: 'http://sdr.websdrmaasbree.nl:8901/', + about: { + nl: 'Kortegolf, o.a. de KLM-ronde op 3785 kHz LSB', + en: 'Shortwave, e.g. the KLM net on 3785 kHz LSB' + }, + via: 'whatsapp', + status: 'online', + sent: '2026-09-20' + }, + { + title: 'WebSDR PI1DFT (OpenWebRX+)', + url: 'https://websdr.pi1dft.ampr.org/#freq=145450000,mod=nfm,sql=-66', + about: { + nl: 'Delft/Den Haag, al afgestemd op 145.450 NFM: de Haagse ronde', + en: 'Delft/The Hague, pre-tuned to 145.450 NFM for the Hague net' + }, + via: 'email', + status: 'online', + sent: '2025-03-07' + }, + { + title: 'WebSDR Utrecht (PI4UTR)', + url: 'https://websdr.pi4utr.nl/', + about: { + nl: 'WebSDR Utrecht van PI4UTR', + en: 'WebSDR Utrecht run by PI4UTR' + }, + via: 'whatsapp', + status: 'offline', + note: { + nl: 'adres niet bereikbaar; zie de site van PI4UTR hieronder', + en: 'address unreachable; see the PI4UTR site below' + } + }, + { + title: 'PI4UTR', + url: 'https://www.pi4utr.nl/', + about: { + nl: 'VERON-afdeling Utrecht, beheert de Utrechtse WebSDR', + en: 'VERON Utrecht club, runs the Utrecht WebSDR' + }, + via: 'added', + status: 'online' + } + ] + }, + { + id: 'air', + title: { + nl: 'Luchtvaart en ADS-B (1090 MHz)', + en: 'Aviation and ADS-B (1090 MHz)' + }, + links: [ + { + title: 'RTL-SDR: ADS-B-radar', + url: 'https://www.rtl-sdr.com/adsb-aircraft-radar-with-rtl-sdr/', + about: { + nl: 'Eigen vliegtuigradar met een RTL-SDR-stickje', + en: 'Build your own aircraft radar with an RTL-SDR dongle' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'Flightradar24: ADS-B-antenne', + url: 'https://www.flightradar24.com/blog/ads-b/ads-b-antenna-and-location-guide/', + about: { + nl: 'Waar en hoe je een ADS-B-antenne plaatst', + en: 'Where and how to place an ADS-B antenna' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'SDRshop: ADS-B-antenne', + url: 'https://sdrshop.nl/ads-b-antenne/', + about: { + nl: 'ADS-B-antenne (1090 MHz)', + en: 'ADS-B antenna (1090 MHz)' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'Elektor: ADS-B buitenantenne', + url: 'https://www.elektor.nl/products/ads-b-outdoor-antenna-1090-mhz', + about: { + nl: 'ADS-B buitenantenne 1090 MHz', + en: 'Outdoor 1090 MHz ADS-B antenna' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'Van Dijken: De Flight-antenne', + url: 'https://www.vandijkenelektronica.nl/product/de-flight-antenna-voor-ontvangst-van-ads-b-signalen-op-1090-mhz/', + about: { + nl: '“De Flight”-antenne voor ADS-B op 1090 MHz', + en: '“De Flight” antenna for ADS-B on 1090 MHz' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'RTL-SDR: FlightAware-antenne', + url: 'https://www.rtl-sdr.com/review-flightaware-ads-b-antenna-and-filter/', + about: { + nl: 'Review van de FlightAware ADS-B-antenne en filter', + en: 'Review of the FlightAware ADS-B antenna and filter' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'Linux Magazine: Raspberry Pi', + url: 'https://linuxmag.nl/vliegtuigen-spotten-met-je-raspberry-pi/', + about: { + nl: 'Vliegtuigen spotten met een Raspberry Pi', + en: 'Plane spotting with a Raspberry Pi' + }, + via: 'email', + status: 'online', + sent: '2025-03-11' + }, + { + title: 'PD0G: ADS-B decoderen', + url: 'https://pd0g.wordpress.com/2018/04/02/ads-b-decoding-with-rtl-sdr-adsbsharp-and-virtual-radar-server/', + about: { + nl: 'ADS-B met RTL-SDR, ADSBSharp en Virtual Radar Server', + en: 'ADS-B with RTL-SDR, ADSBSharp and Virtual Radar Server' + }, + via: 'email', + status: 'online', + sent: '2025-03-11' + }, + { + title: 'PD2EW: ADS-B', + url: 'https://www.wijkamp.com/pd2ew/ads-b.html', + about: { + nl: 'ADS-B-pagina van zendamateur PD2EW', + en: "A ham's ADS-B page (PD2EW)" + }, + via: 'email', + status: 'online', + sent: '2025-03-11' + } + ] + }, + { + id: 'iss', + title: { + nl: 'ISS en satellieten', + en: 'ISS and satellites' + }, + links: [ + { + title: 'ARISS live', + url: 'https://live.ariss.org/', + about: { + nl: 'Radiocontacten tussen scholen en de ISS (145.800 MHz)', + en: 'Radio contacts between schools and the ISS (145.800 MHz)' + }, + via: 'email', + status: 'online', + sent: '2022-12-08' + }, + { + title: 'Weerstation Grootegast', + url: 'https://www.weerstation-grootegast.nl/pagina32.html', + about: { + nl: 'Ontvangst van NOAA-weersatellieten', + en: 'Receiving NOAA weather satellites' + }, + via: 'email', + status: 'warn', + sent: '2025-03-06', + archive: + 'https://web.archive.org/web/20250611043333/https://weerstation-grootegast.nl/pagina32.html', + note: { + nl: 'verouderde beveiliging, opent soms niet; gebruik dan het archief', + en: 'outdated security, may not open; use the archive copy' + } + } + ] + }, + { + id: 'marine', + title: { + nl: 'Marifoon', + en: 'Marine VHF' + }, + links: [ + { + title: "Opa's marifoonlijst (banken 7–10)", + url: '#band-marine', + about: { + nl: 'Kanaal 301–495, staat in de frequentielijst op deze pagina', + en: 'Channels 301–495, included in the frequency list on this page' + }, + via: 'email', + status: 'online', + sent: '2025-05-21' + } + ] + }, + { + id: 'sdr', + title: { + nl: 'Scanner en SDR: apparatuur en software', + en: 'Scanner and SDR: hardware and software' + }, + links: [ + { + title: 'SDRshop: SDRplay RSP', + url: 'https://sdrshop.nl/sdrplayrsp/', + about: { + nl: 'SDRplay RSP-ontvangers (Opa heeft de RSP1B)', + en: 'SDRplay RSP receivers (Opa has the RSP1B)' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'SDRshop: SDRplay-overzicht', + url: 'https://sdrshop.nl/sdrplay-overzicht/', + about: { + nl: 'Vergelijking van alle SDRplay-modellen', + en: 'Comparison of all SDRplay models' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'SDRshop: SDRuno-boek', + url: 'https://sdrshop.nl/sdrplay-en-sdruno-boek/', + about: { + nl: 'Nederlandstalig SDRplay/SDRuno-boek (ADS-B op blz. 140)', + en: 'Dutch SDRplay/SDRuno handbook (ADS-B on p. 140)' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'SDRuno-boek: inhoud (pdf)', + url: 'https://sdrshop.nl/wp-content/uploads/SDRuno-handleiding-inhoud-1.pdf', + about: { + nl: 'Inhoudsopgave van dat boek', + en: 'Table of contents of that book' + }, + via: 'added', + status: 'online' + }, + { + title: 'SDRplay RSP1B', + url: 'https://www.sdrplay.com/rsp1b/', + about: { + nl: 'Officiële pagina van de RSP1B', + en: 'Official RSP1B page' + }, + via: 'added', + status: 'online' + }, + { + title: 'RTL-SDR.com', + url: 'https://www.rtl-sdr.com/', + about: { + nl: "Nieuws en projecten met goedkope SDR's", + en: 'News and projects with cheap SDRs' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'RTL-SDR: wat is het?', + url: 'https://www.rtl-sdr.com/about-rtl-sdr/', + about: { + nl: 'Wat is een RTL-SDR?', + en: 'What is an RTL-SDR?' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'PC5E: frequentielijst Nederland (pdf)', + url: 'https://www.pc5e.nl/downloads/scanner/20240102%20Frequenties%20Nederland.pdf', + about: { + nl: 'Lijst van PC5E, 2 jan 2024, 364 blz., op pc5e.nl', + en: "PC5E's list, 2 Jan 2024, 364 pages, on pc5e.nl" + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'PC5E', + url: 'https://www.pc5e.nl/', + about: { + nl: 'Homepage van zendamateur PC5E (Heerenveen), met scannerfrequenties', + en: 'Homepage of ham PC5E (Heerenveen), with scanner frequencies' + }, + via: 'whatsapp', + status: 'online' + } + ] + }, + { + id: 'forums', + title: { + nl: 'Forums en clubs', + en: 'Forums and clubs' + }, + links: [ + { + title: 'Het Scannerforum', + url: 'https://www.scannerforum.nl/', + about: { + nl: 'Alles over scanners, frequenties en P2000', + en: 'Dutch scanner, frequency and P2000 forum' + }, + via: 'whatsapp', + status: 'online' + }, + { + title: 'Scannerforum: zoeken', + url: 'https://www.scannerforum.nl/index.php?action=search2', + about: { + nl: 'Zoekpagina (zoek op “ads-b”)', + en: 'Search page (search for “ads-b”)' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'Zendamateur.com', + url: 'https://www.zendamateur.com/', + about: { + nl: '“24 uur per dag QRV”, het NL/BE-radioamateurforum (inloggen aanbevolen)', + en: '“On the air 24/7”, the Dutch/Belgian ham forum (logging in recommended)' + }, + via: 'whatsapp', + status: 'online' + }, + { + title: 'Zendamateur.com: ADS-B-topic', + url: 'https://www.zendamateur.com/viewtopic.php?t=26620', + about: { + nl: 'Forumtopic over ADS-B', + en: 'Forum topic about ADS-B' + }, + via: 'email', + status: 'online', + sent: '2025-03-06' + }, + { + title: 'QRZ.com: PA4VB', + url: 'https://www.qrz.com/db/PA4VB', + about: { + nl: 'QRZ-pagina van roepnaam PA4VB (details na inloggen)', + en: 'QRZ page for callsign PA4VB (details need a login)' + }, + via: 'email', + status: 'online', + sent: '2026-10-01' + }, + { + title: 'VRZA Zuid-West (PI4ZWN)', + url: 'https://www.pi4zwn.nl/', + about: { + nl: 'Afdelingsnieuws en agenda', + en: 'Club news and calendar' + }, + via: 'added', + status: 'online' + }, + { + title: 'PI4ZWN: SDRplay populair', + url: 'https://www.pi4zwn.nl/sdrplay-blijkt-populaire-ontvanger/', + about: { + nl: 'Artikel: “SDRplay blijkt populaire ontvanger”', + en: 'Article: “SDRplay turns out to be a popular receiver”' + }, + via: 'email', + status: 'online', + sent: '2025-03-07' + } + ] + } +]; diff --git a/src/routes/trackers/radio/+page.server.ts b/src/routes/trackers/radio/+page.server.ts index 83c8983..eb1cbc2 100644 --- a/src/routes/trackers/radio/+page.server.ts +++ b/src/routes/trackers/radio/+page.server.ts @@ -20,7 +20,9 @@ import type { Actions, PageServerLoad } from './$types'; // below check the owner, and hooks.server.ts refuses non-GET requests here (ownerWritePrefixes). // Language: ?lang=nl|en (remembered in a cookie), else the cookie, else Accept-Language, else NL. export const load: PageServerLoad = async (event) => { - const owner = isOwner(event); + const isOwnerReq = isOwner(event); + // ?view=public lets the owner preview exactly what visitors get. + const owner = isOwnerReq && event.url.searchParams.get('view') !== 'public'; // Varies by login, cookie and Accept-Language, so never cache it in between. event.setHeaders({ 'cache-control': 'private, no-store', vary: 'cookie, accept-language' }); const asked = event.url.searchParams.get('lang'); @@ -41,6 +43,7 @@ export const load: PageServerLoad = async (event) => { return { ...(owner ? radio : publicRadio(radio)), owner, + ownerPreview: isOwnerReq && !owner, lang, now: Date.now(), seo: { diff --git a/src/routes/trackers/radio/+page.svelte b/src/routes/trackers/radio/+page.svelte index 5caf474..f32b4db 100644 --- a/src/routes/trackers/radio/+page.svelte +++ b/src/routes/trackers/radio/+page.svelte @@ -11,6 +11,7 @@ import Nets from '#lib/radio/Nets.svelte'; import IssPasses from '#lib/radio/IssPasses.svelte'; import LogList from '#lib/radio/LogList.svelte'; + import PublicRadio from '#lib/radio/PublicRadio.svelte'; import { BANDS } from '#lib/radio/index.js'; import { DICTS, LANGS, setRadio } from '#lib/radio/i18n.js'; import LayoutDashboard from 'lucide-svelte/icons/layout-dashboard'; @@ -91,125 +92,142 @@ resolve(`/trackers/radio?tab=frequencies&band=${band}` as '/'); -
-
- -
-

{t.title}

-

{data.owner ? t.intro : t.introPublic}

-
-
- - - {#if tab === 'overview'} -
-
-

- {t.upNext} -

- -
+ {#if tab === 'overview'} +
+
+

+ {t.upNext} +

+ +
+ {#if data.owner} +
+

+ {t.quickLog} +

+ +
+ +
+
+ {:else} +
+

+ {t.byBand} +

+ +
+ {/if} +
+

+ {t.nextIss} + {t.all} +

+ +
+
+

+ {t.scanner} +

+

{t.channelsProgrammed(data.channels.length, data.frequencies.length)}

+

{t.scanHelp}

+ {t.openBanks} +
+ {#if !data.owner && data.log.length} +
+

+ {t.recentLog} +

+ +
+ {/if} +
+ {:else if tab === 'frequencies'} + + {:else if tab === 'banks'} + + {:else if tab === 'nets'} + + {:else if tab === 'iss'} +
+ {:else} {#if data.owner}
-

- {t.quickLog} -

-
- -
-
- {:else} -
-

- {t.byBand} -

-
{/if}
-

- {t.nextIss} - {t.all} -

- -
-
-

- {t.scanner} -

-

{t.channelsProgrammed(data.channels.length, data.frequencies.length)}

-

{t.scanHelp}

- {t.openBanks} -
- {#if !data.owner && data.log.length} -
-

- {t.recentLog} -

- -
- {/if} -
- {:else if tab === 'frequencies'} - - {:else if tab === 'banks'} - - {:else if tab === 'nets'} - - {:else if tab === 'iss'} -
- {:else} - {#if data.owner} -
- + {#if logFreq}

+ {t.oneFreq} {t.showAllLog} +

{/if} +
{/if} -
- {#if logFreq}

- {t.oneFreq} {t.showAllLog} -

{/if} - -
- {/if} -
+ +{/if}