2026-08-19 17:11:58 +03:00
import { createEffect , createResource , createSignal , For , Show } from "solid-js" ;
2026-08-18 22:31:56 +03:00
import { MapView } from "../../components/map/MapView" ;
2026-08-19 17:11:58 +03:00
import { apiHost , ResultKeyVal } from "../../consts" ;
2026-08-18 22:31:56 +03:00
import { LoadingSpinner } from "../../components/spinner/LoadingSpinner" ;
2026-08-19 17:11:58 +03:00
import { faktiskaExport , faktiskaStations , FaktiskaStation } from "./faktiskaConfig" ;
2026-08-20 22:23:23 +03:00
import type { FaktiskaTemplate } from "./faktiskaTemplates" ;
2026-08-18 22:31:56 +03:00
import "./mapGraphics.css" ;
2026-08-19 17:11:58 +03:00
type LatestTemperature = { city : FaktiskaStation ; observedAt : string ; value : number | null };
const emptyValues = () : Record < FaktiskaStation , string > =>
Object . fromEntries ( faktiskaStations . map ( city => [ city , "" ])) as Record < FaktiskaStation , string >;
2026-08-18 22:45:14 +03:00
export function Faktiska() {
2026-08-19 17:11:58 +03:00
const [ values , setValues ] = createSignal ( emptyValues ());
const [ overrides , setOverrides ] = createSignal < Set < FaktiskaStation >>( new Set ());
2026-08-20 22:23:23 +03:00
const [ template , setTemplate ] = createSignal < FaktiskaTemplate >( faktiskaExport . defaultTemplate );
const productionSize = () => template () === "faktiska_1920x1080"
? { width : 1920 , height : 1080 }
: { width : 3840 , height : 1440 };
2026-08-19 17:11:58 +03:00
const fetchLatest = async () : Promise < LatestTemperature [] > => {
const response = await fetch ( ` ${ apiHost } /api/query/latest-temperatures/ ${ faktiskaStations . join ( "," ) } ` );
if ( ! response . ok ) throw new Error ( `Latest observations failed ( ${ response . status } )` );
2026-08-18 22:31:56 +03:00
return response . json ();
};
2026-08-19 17:11:58 +03:00
const [ latest , { refetch }] = createResource ( fetchLatest );
createEffect (() => {
const observations = latest ();
if ( ! observations ) return ;
const next = emptyValues ();
observations . forEach ( observation => {
next [ observation . city ] = observation . value == null ? "" : observation . value . toString ();
});
setValues ( next );
2026-08-21 10:18:52 +03:00
setOverrides ( new Set < FaktiskaStation >());
2026-08-19 17:11:58 +03:00
});
const observationsByCity = () => new Map (( latest () ?? []). map ( item => [ item . city , item ]));
const newestTimestamp = () => {
const timestamps = ( latest () ?? []). map ( item => Date . parse ( item . observedAt )). filter ( Number . isFinite );
return timestamps . length ? new Date ( Math . max (... timestamps )) : undefined ;
};
const isStale = ( city : FaktiskaStation ) => {
const newest = newestTimestamp ();
const observed = observationsByCity (). get ( city ) ? . observedAt ;
return Boolean ( newest && observed && newest . getTime () - Date . parse ( observed ) > 90 * 60 * 1000 );
};
const cityData = () : ResultKeyVal [] => faktiskaStations . flatMap ( city => {
const value = Number ( values ()[ city ]. replace ( "," , "." ));
return values ()[ city ]. trim () && Number . isFinite ( value ) ? [[ city , value ] as ResultKeyVal ] : [];
});
const updateValue = ( city : FaktiskaStation , value : string ) => {
setValues ( current => ({ ... current , [ city ] : value }));
setOverrides ( current => new Set ( current ). add ( city ));
};
const resetValue = ( city : FaktiskaStation ) => {
const observation = observationsByCity (). get ( city );
setValues ( current => ({ ... current , [ city ] : observation ? . value == null ? "" : observation . value . toString () }));
setOverrides ( current => {
const next = new Set ( current );
next . delete ( city );
return next ;
});
};
return < div class = "mapGraphicsPage pageWorkspace faktiskaWorkspace" >
< div class = "pageHeading" >
< div >< span class = "eyebrow" > Daily newsroom production </ span >< h1 > Faktiskā </ h1 >< p > Current temperatures with manually prepared weather symbols and wind .</ p ></ div >
2026-08-20 22:23:23 +03:00
< div class = "productionSize" >{ productionSize (). width } × { productionSize (). height } PNG </ div >
2026-08-18 22:31:56 +03:00
</ div >
2026-08-19 17:11:58 +03:00
< section class = "faktiskaDataBand" >
< div class = "faktiskaStatus" >
< div >< span class = "stepLabel" > 1 . Latest temperatures </ span >< strong >{ newestTimestamp () ? newestTimestamp () ! . toLocaleString ( "lv-LV" ) : "Waiting for observations" }</ strong >< small > Newest available reading in the configured station set </ small ></ div >
< button class = "secondary" disabled = { latest . loading } onClick = {() => refetch ()}> Refresh data </ button >
</ div >
< Show when = { latest . loading }>< LoadingSpinner text = "Loading latest temperatures" /></ Show >
< Show when = { latest . error }>< div class = "inlineNotice error" >< strong > Could not load observations .</ strong > Existing manual fields remain available .</ div ></ Show >
< div class = "temperatureEditor" >
< For each = { faktiskaStations }>{ city => {
const observation = () => observationsByCity (). get ( city );
return < label class = "temperatureField" classList = {{ manual : overrides (). has ( city ), stale : isStale ( city ), missing : observation () ? . value == null }}>
< span class = "temperatureFieldHeading" >< strong >{ city }</ strong >< small >{ overrides (). has ( city ) ? "Manual" : isStale ( city ) ? "Older reading" : observation () ? . observedAt ? new Date ( observation () ! . observedAt ). toLocaleTimeString ( "lv-LV" , { hour : "2-digit" , minute : "2-digit" }) : "No data" }</ small ></ span >
< span class = "temperatureInput" >< input type = "text" inputmode = "decimal" value = { values ()[ city ]} onInput = { event => updateValue ( city , event . currentTarget . value )} aria-label = { ` ${ city } temperature` }/>< span > ° C </ span ></ span >
< button type = "button" class = "resetTemperature" disabled = { ! overrides (). has ( city )} onClick = {() => resetValue ( city )}> Reset </ button >
</ label >;
}}</ For >
</ div >
</ section >
< section class = "mapProduction faktiskaProduction" >
< h2 > 2 . Add weather symbols and wind , then export </ h2 >
2026-08-20 22:23:23 +03:00
< div class = "resolutionChoices faktiskaResolutionChoices" aria-label = "Faktiskā export size" >
< button classList = {{ active : template () === "faktiska_1920x1080" }} onClick = {() => setTemplate ( "faktiska_1920x1080" )}> 1920 × 1080 </ button >
< button classList = {{ active : template () === "faktiska_3840x1440" }} onClick = {() => setTemplate ( "faktiska_3840x1440" )}> 3840 × 1440 </ button >
</ div >
< Show when = { template ()} keyed >{ currentTemplate =>
< MapView type = { currentTemplate } data = { cityData } mode = "faktiska" productionTemplate />
}</ Show >
2026-08-19 17:11:58 +03:00
</ section >
2026-08-18 22:31:56 +03:00
</ div >;
}