Compare commits

...

9 Commits

Author SHA1 Message Date
4f7acf9ffb Adjust policy and error message language to be more friendly. 2025-05-02 21:02:46 +01:00
1c308321de Friendly error messages for featureDetect 2025-05-02 20:47:26 +01:00
182136fc6b Replace lookup card on homepage with FindByHeadcodeCard. 2025-05-02 20:45:05 +01:00
46c15f9601 Add find by headcode Card to PIS page 2025-05-02 20:41:51 +01:00
ec413b6e5c Fix train page: don't show error if no services are found 2025-05-02 20:24:02 +01:00
0011bdb751 Privacy improvements:
- Add telemetry consent modal
 - Conditionally load telemetry script
 - Add telemetry consent to settings
 - Update and clarify privacy policy
 - Bump version number
2025-03-09 22:50:42 +00:00
059eae3784 - Remove Matomo analytics code
- Add noindex tags on some pages
 - Bump version
 - Move Liwan analytics code from app.html to +layout.svelte
2025-03-06 14:50:11 +00:00
ee6e81de62 Upgrade analytics script 2025-03-06 14:10:31 +00:00
f6223ee826 Add Matomo tag mgr 2025-03-06 13:53:03 +00:00
26 changed files with 329 additions and 92 deletions

View File

@ -2,23 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script type="module" data-entity="owlboard-frontend" src="https://liwan.fjla.uk/script.js"></script>
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//owa.fjla.uk/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<!-- End Matomo Code -->
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@ -0,0 +1,14 @@
<script lang="ts">
import LookupCard from "./LookupCard.svelte";
const LookupCardConfig = {
title: "Find By Headcode",
helpText: "",
formAction: "/train",
placeholder: "enter headcode",
maxLen: 4,
fieldName: "headcode",
}
</script>
<LookupCard config={LookupCardConfig} />

View File

@ -0,0 +1,113 @@
<script lang="ts">
import { setTelemetryFalse, setTelemetryTrue } from "$lib/stores/SetTelemetryConsent";
import { telemetry } from "$lib/stores/telemetryConsent";
import { onMount } from "svelte";
import { browser } from "$app/environment";
onMount(() => {
if (!localStorage.getItem("telemetryRequested")) {
document.querySelector<HTMLDialogElement>("#analytics-consent")?.showModal();
}
})
// Setting Function Calls
function setAcceptAnalytics() {
setTelemetryTrue();
localStorage.setItem("telemetryRequested", "yes");
document.querySelector<HTMLDialogElement>("#analytics-consent")?.close();
}
function setDenyAnalytics() {
setTelemetryFalse();
localStorage.setItem("telemetryRequested", "yes");
document.querySelector<HTMLDialogElement>("#analytics-consent")?.close();
}
// Reactively call telemetry script functions
$: {
if (browser) {
if ($telemetry) {
loadTelemetryScript();
} else {
removeTelemetryScript();
}
}
}
function loadTelemetryScript() {
console.log("Activating telemetry script")
if (browser) {
if (document.querySelector("script[data-entity='owlboard-frontend']")) return; // Prevent duplicate loading
const script = document.createElement("script");
script.type = "module";
script.dataset.entity = "owlboard-frontend";
script.src = "https://liwan.fjla.uk/script.js";
document.body.appendChild(script);
}
}
function removeTelemetryScript() {
console.log("Deactivating telemetry script")
if (browser) {
document.querySelector("script[data-entity='owlboard-frontend']")?.remove();
}
}
</script>
<dialog id="analytics-consent">
<h1>Telemetry</h1>
<p>
OwlBoard collects <strong>anonymous</strong> usage data, such as the most visited pages. Any personal data is anonymized to ensure it cannot be linked to individuals.
</p>
<p>
This data is used to focus efforts, improving the most used features.
</p>
<p>
By selecting Accept, you are helping to steer OwlBoard's future - if
you change your mind, head over to Settings.
</p>
<p>
Nobody can be identified using any data stored, all data is available for
all to see <a href="https://liwan.fjla.uk" target="_blank">here</a>.
</p>
<p>
Further information can be found in the <a href="/more/privacy">Privacy Policy</a>.
</p>
<button class="modal-button" type="button" on:click={setAcceptAnalytics}>Accept</button>
<button class="modal-button" id="deny-button" type="button" on:click={setDenyAnalytics}>Deny</button>
</dialog>
<style>
::backdrop {
background-color: var(--main-bg-color);
opacity: 75%;
}
#analytics-consent {
width: 75vw;
max-width: 700px;
border-radius: 25px;
background-color: var(--island-bg-color);
opacity: 100;
color: var(--island-text-color);
border: none;
}
.modal-button {
width: 25%;
min-width: 120px;
height: 40px;
font-size: larger;
margin: 5px;
border-radius: 30px;
background-color: var(--second-bg-color);
color: var(--island-text-color);
box-shadow: var(--box-shadow);
border: none;
font-family: urwgothic, sans-serif;
}
#deny-button {
background-color: var(--main-bg-color);
}
</style>

View File

@ -0,0 +1,14 @@
import { get } from "svelte/store";
import { telemetry } from "./telemetryConsent";
export function setTelemetryTrue() {
telemetry.set(true);
}
export function setTelemetryFalse() {
telemetry.set(false);
}
export function getTelemetry(): boolean {
return get(telemetry);
}

View File

@ -0,0 +1,25 @@
// src/stores.js
import { writable, type Writable } from "svelte/store";
import { browser } from "$app/environment";
// Initialize the store with a boolean value from local storage
export const telemetry: Writable<boolean> = writable(fromLocalStorage("telemetry", false));
toLocalStorage(telemetry, "telemetry");
function fromLocalStorage(storageKey: string, fallback: boolean): boolean {
if (browser) {
const storedValue = localStorage.getItem(storageKey);
if (storedValue !== null && storedValue !== "undefined") {
return storedValue === "true";
}
}
return fallback;
}
function toLocalStorage(store: Writable<boolean>, storageKey: string) {
if (browser) {
store.subscribe((value: boolean) => {
localStorage.setItem(storageKey, String(value));
});
}
}

View File

@ -1,2 +1,2 @@
export const version: string = "2025.03.2"; export const version: string = "2025.05.1";
export const versionTag: string = ""; export const versionTag: string = "";

View File

@ -5,6 +5,10 @@
const title = "OwlBoard - Error"; const title = "OwlBoard - Error";
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<h1>{$page.status}: {$page?.error?.message}</h1> <h1>{$page.status}: {$page?.error?.message}</h1>

View File

@ -5,6 +5,7 @@
import DevBanner from "$lib/DevBanner.svelte"; import DevBanner from "$lib/DevBanner.svelte";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { Toaster } from "svelte-french-toast"; import { Toaster } from "svelte-french-toast";
import AnalyticsConsent from "$lib/popover/analytics-consent.svelte";
</script> </script>
<svelte:head> <svelte:head>
@ -33,6 +34,7 @@
</svelte:head> </svelte:head>
<Toaster /> <Toaster />
<AnalyticsConsent />
{#if dev} {#if dev}
<DevBanner /> <DevBanner />

View File

@ -8,6 +8,7 @@
import LookupCard from "$lib/cards/LookupCard.svelte"; import LookupCard from "$lib/cards/LookupCard.svelte";
import NearToMeCard from "$lib/cards/NearToMeCard.svelte"; import NearToMeCard from "$lib/cards/NearToMeCard.svelte";
import QuickLinkCard from "$lib/cards/QuickLinkCard.svelte"; import QuickLinkCard from "$lib/cards/QuickLinkCard.svelte";
import FindByHeadcodeCard from "$lib/cards/FindByHeadcodeCard.svelte";
const title = "OwlBoard"; const title = "OwlBoard";
const lookupCards: LookupCardConfig[] = [ const lookupCards: LookupCardConfig[] = [
{ {
@ -17,23 +18,15 @@
placeholder: "enter crs/tiploc", placeholder: "enter crs/tiploc",
maxLen: 7, maxLen: 7,
fieldName: "station", fieldName: "station",
}, }
{
title: "Timetable & PIS",
helpText: "",
formAction: "/train",
placeholder: "enter headcode",
maxLen: 4,
fieldName: "headcode",
},
]; ];
onMount(async () => { onMount(async () => {
const featureSupport = featureDetect(); const featureSupport = featureDetect();
if (!featureSupport.critical) { if (!featureSupport.critical) {
toast.error("Your browser is missing critical features, OwlBoard might not work properly. See `Menu > Statistics` for more information."); toast.error("Use a newer browser or OwlBoard might not work properly. See `Menu > Statistics` for more information.");
} else if (!featureSupport.nice) { } else if (!featureSupport.nice) {
toast.error("Your browser is missing some features, see `Menu > Statistics` for more information."); toast.error("Use a newer browser for the best experience, see `Menu > Statistics` for more information.");
} }
}); });
</script> </script>
@ -42,6 +35,7 @@
{#each lookupCards as config} {#each lookupCards as config}
<LookupCard {config} /> <LookupCard {config} />
{/each} {/each}
<FindByHeadcodeCard />
<NearToMeCard /> <NearToMeCard />
<QuickLinkCard /> <QuickLinkCard />

View File

@ -5,6 +5,10 @@
const title = "404 - Not Found"; const title = "404 - Not Found";
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<h1 class="heading">There's no light at the end of this tunnel</h1> <h1 class="heading">There's no light at the end of this tunnel</h1>
<p>The page you were looking for wasn't found</p> <p>The page you were looking for wasn't found</p>

View File

@ -5,6 +5,10 @@
const title = "50x - Server Error"; const title = "50x - Server Error";
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<h1 class="heading">This page has been delayed by more servers than usual needing repairs at the same time</h1> <h1 class="heading">This page has been delayed by more servers than usual needing repairs at the same time</h1>
<p>There was an error with the server, please try again later</p> <p>There was an error with the server, please try again later</p>

View File

@ -32,6 +32,10 @@
}); });
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
{#if !blockLoading} {#if !blockLoading}

View File

@ -2,6 +2,9 @@
import LargeLogo from "$lib/images/large-logo.svelte"; import LargeLogo from "$lib/images/large-logo.svelte";
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<LargeLogo /> <LargeLogo />
<h1> <h1>
OwlBoard is down for maintenance OwlBoard is down for maintenance

View File

@ -29,6 +29,10 @@
]; ];
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
{#each links as item} {#each links as item}

View File

@ -8,7 +8,7 @@
<Header {title} /> <Header {title} />
<LargeLogo /> <LargeLogo />
<p class="neg">&copy; 2022-2023 Frederick Boniface</p> <p class="neg">&copy; 2022-2025 Frederick Boniface</p>
<p>OwlBoard was created by train crew for train crew.</p> <p>OwlBoard was created by train crew for train crew.</p>
<p>I developed OwlBoard in 2022 with the aim of providing fast and easy access to the information we need on a daily basis.</p> <p>I developed OwlBoard in 2022 with the aim of providing fast and easy access to the information we need on a daily basis.</p>
<p>Data is sourced from National Rail Enquiries, the OwlBoard Project, and Network Rail.</p> <p>Data is sourced from National Rail Enquiries, the OwlBoard Project, and Network Rail.</p>

View File

@ -35,6 +35,10 @@
}); });
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<p>OwlBoard stores as little data about you as possible to offer the service.</p> <p>OwlBoard stores as little data about you as possible to offer the service.</p>

View File

@ -3,6 +3,10 @@
import Nav from "$lib/navigation/nav.svelte"; import Nav from "$lib/navigation/nav.svelte";
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header title={"Help"} /> <Header title={"Help"} />
<Nav /> <Nav />
<br /><br /> <br /><br />

View File

@ -4,41 +4,72 @@
const title = "Privacy Policy"; const title = "Privacy Policy";
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<div> <div>
<p> <h2>Your Data</h2>
OwlBoard stores the minimum amount of data necessary to provide its functions for your use. No personal data is stored unless you report an issue. To review the specific <p>
data that we store, please visit <a href="/more/data">My Data</a>. OwlBoard logs the IP addresses of its users, this is done on the basis of legitimate
</p> interest to ensure the security of the platform and to protect all users from
<p>OwlBoard does not utilize any cookies. IP addresses and browser fingerprints are not logged.</p> malicious activity. See <a href="#datasharing">Data Sharing</a> for details on how
<h2>If You Do Not Sign Up</h2> we may share this data.
<p> </p>
If you choose not to sign up, no personal data will be processed or stored unless you report an issue. Any personal settings are stored locally in your browser and do not <p>
leave your device. With the exception of sending emails, all data is held - and always remains within -
</p> the United Kingdom.
<h2>If You Sign Up</h2> </p>
<p> <h3>Telemetry</h3>
If you sign up for the rail staff version of OwlBoard, we do require the storage of some data. However, none of this data can be used to personally identify you. Any <p>
personal settings are stored locally in your browser and do not leave your device. If you opt-in to Telemetry, you will share your IP address and information about the
</p> type of device and software you're using to access the service. This data is
<p> anonymised and cannot be traced back to any individual. You can opt-in or opt-out in
During the sign-up process, you will be asked to provide a work email address, which will be checked to confirm its origin from a railway company. Once confirmed, an email your <a href="/more/settings">Settings</a>. All of the anonymised data can be viewed
containing a registration link will be sent to you. At this point, the username portion of your email address is discarded. For example, if your email address is at: <a target="_blank" href="https://liwan.fjla.uk">liwan.fjla.uk</a> at any time.
'a-user@owlboard.info', only 'owlboard.info' will be stored. This host portion of your email address is stored to filter and display relevant results prominently. </p>
</p> <p>
<p>The email server may store the address and message content as part of its regular operation, and your consent to this is implied when you sign up.</p> All of the data that is stored is held within the United Kingdom.
<p>In addition to the host portion of your email address, a randomly generated UUID is stored for the purpose of authorizing access to the rail staff data.</p> </p>
<p> <p>
If you enable location data, your location will be sent to the server when you navigate to the homepage to determine your closest stations. This data is never stored on the Telemetry data is used to identify which areas of the webapp are used most frequently
server after the nearest stations have been send to your device. and where improvements need to be made.
</p> </p>
<h2>Reporting an Issue</h2> <h3>Registering</h3>
<p>When you report an issue, certain data is collected, including your browser's User Agent string and the size of the window in which you are viewing the website.</p> <p>
<p> If you register, your email address will be used to verify that you work for an
Any data submitted when reporting an issue will be publicly viewable alongside the <a href="https://git.fjla.uk/owlboard/backend/issues" target="_blank" organisation that is permitted to access staff data on the service. An activation
>OwlBoard/backend git repository</a email will be sent before your email address is anonymised. For example, if your
>. email address is hello@owlboard.info, it will be anonymized to @owlboard.info.
</p> </p>
<p>
OwlBoard emails are sent using Proton Mail, a privacy-first email service based in
Switzerland. To facilitate this, your email address will be securely sent to
Proton Mail securely.
</p>
<p>
You will be assigned a unique identifier which will be stored alongside your
anonymised email address as well as in your browser. This is how the service
verifies that you are authorised to access staff data.
</p>
<h2 id="datasharing">Data Sharing</h2>
<p>
OwlBoard utilises CrowdSec, a security service that helps to protect the platform from
malicious activity. As part of its operation, CrowdSec analyzes IP addresses and may
share certain information with its community of users to identify and mitigate
security threats. If your IP address is identified as part of a security incident or
threat, it may be shared with CrowdSec's network for further analysis and to prevent
malicious activity. This sharing of IP addresses is done under the legitimate
interest basis for ensuring the security of the platform and protecting all users
from malicious activity.
</p>
<p>
CrowdSec anonymizes and processes data in accordance with its own privacy policy, which
is available for review here. We recommend reviewing their policy to understand how
they handle any data collected.
</p>
</div> </div>
<Nav /> <Nav />
@ -50,12 +81,23 @@
h2 { h2 {
color: var(--second-text-color); color: var(--second-text-color);
margin: 10px; margin: auto;
width: 90vw;
padding-top: 20px; padding-top: 20px;
} }
h3 {
color: var(--second-text-color);
margin: auto;
width: 90vw;
padding-top: 10px;
}
p { p {
color: white; color: white;
margin: 10px; margin: auto;
padding-top: 5px;
padding-bottom: 8px;
width: 90vw;
max-width: 550px;
} }
</style> </style>

View File

@ -43,9 +43,9 @@
function send() { function send() {
toast.promise(request(), { toast.promise(request(), {
loading: "Contacting Server...", loading: "Sending email...",
success: "Request Answered.", success: "Sent, check your inbox",
error: "Unable to contact server.", error: "Error sending email",
}); });
} }

View File

@ -45,6 +45,10 @@
} }
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
{#if state} {#if state}

View File

@ -76,6 +76,10 @@
} }
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
{#if isLoading} {#if isLoading}

View File

@ -4,6 +4,7 @@
import QlSet from "$lib/islands/quick-link-set-island.svelte"; import QlSet from "$lib/islands/quick-link-set-island.svelte";
import Island from "$lib/islands/island.svelte"; import Island from "$lib/islands/island.svelte";
import { location } from "$lib/stores/location"; import { location } from "$lib/stores/location";
import { telemetry } from "$lib/stores/telemetryConsent";
import { getCurrentLocation } from "$lib/scripts/getLocation"; import { getCurrentLocation } from "$lib/scripts/getLocation";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
const title = "Settings"; const title = "Settings";
@ -12,11 +13,15 @@
getCurrentLocation(); getCurrentLocation();
} }
function locationToast() { function confirmationToast() {
toast("Settings updated"); toast.success("Settings updated");
} }
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<QlSet /> <QlSet />
@ -25,10 +30,19 @@
<p>Use your location to quickly check departure boards near you</p> <p>Use your location to quickly check departure boards near you</p>
<div class="checkbox-container"> <div class="checkbox-container">
<label for="location_enable">Enabled</label> <label for="location_enable">Enabled</label>
<input id="location_enable" type="checkbox" bind:checked={$location} on:click={locationToast} /> <input id="location_enable" type="checkbox" bind:checked={$location} on:click={confirmationToast} />
</div> </div>
</Island> </Island>
<Island variables={{title:"Telemetry"}}>
<p>Telemetry helps shape the future of OwlBoard - all data is anonymised. To find out more, see the
<a href="/more/privacy">privacy policy</a>.
</p>
<div class="checkbox-container">
<label for="telemetry_enable">Enabled</label>
<input id="telemetry_enable" type="checkbox" bind:checked={$telemetry} on:click={confirmationToast} />
</Island>
<Nav /> <Nav />
<style> <style>
@ -47,4 +61,5 @@
margin-right: 25px; margin-right: 25px;
font-weight: 800; font-weight: 800;
} }
</style> </style>

View File

@ -43,6 +43,10 @@
} }
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
{#await getData()} {#await getData()}

View File

@ -17,6 +17,10 @@
} }
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<LargeLogo /> <LargeLogo />

View File

@ -9,6 +9,7 @@
import type { OB_Pis_FullObject } from "@owlboard/ts-types"; import type { OB_Pis_FullObject } from "@owlboard/ts-types";
import Card from "$lib/cards/Card.svelte"; import Card from "$lib/cards/Card.svelte";
import type { CardConfig } from "$lib/cards/Card.types"; import type { CardConfig } from "$lib/cards/Card.types";
import FindByHeadcodeCard from "$lib/cards/FindByHeadcodeCard.svelte";
const title = "PIS Finder"; const title = "PIS Finder";
let entryPIS = ""; let entryPIS = "";
@ -74,7 +75,7 @@
onMount(() => { onMount(() => {
if ($uuid == null || $uuid == "") { if ($uuid == null || $uuid == "") {
toast("This feature will soon require registration. Register in the menu.", { toast("You must register to see results", {
duration: 3000, duration: 3000,
}); });
} }
@ -96,14 +97,6 @@
onRefresh: () => {}, onRefresh: () => {},
refreshing: false, refreshing: false,
} }
const findByHeadcodeCard: CardConfig = {
title: "Find by Headcode",
showHelp: true,
helpText: "Find by Headcode can be found on the homepage",
showRefresh: false,
onRefresh: () => {},
refreshing: false,
}
const findByStartEndCard: CardConfig = { const findByStartEndCard: CardConfig = {
title: "Find by Start/End CRS", title: "Find by Start/End CRS",
showHelp: true, showHelp: true,
@ -147,12 +140,7 @@
</Card> </Card>
<button id="reset" type="reset" on:click={reset}>Reset</button> <button id="reset" type="reset" on:click={reset}>Reset</button>
{:else} {:else}
<Card config={findByHeadcodeCard}> <FindByHeadcodeCard />
<span class="important">Find by Headcode from the homepage</span>
<br>
The tools below are more helpful if you've been diverted or are not starting your journey at your scheduled origin.
<br><br>
</Card>
<Card config={findByStartEndCard}> <Card config={findByStartEndCard}>
<form on:submit={findByStartEnd}> <form on:submit={findByStartEnd}>
<input type="text" maxlength="3" pattern="^[A-Za-z]+$" autocomplete="off" placeholder="Start" required bind:value={entryStartCRS} /> <input type="text" maxlength="3" pattern="^[A-Za-z]+$" autocomplete="off" placeholder="Start" required bind:value={entryStartCRS} />
@ -174,10 +162,6 @@
<Nav /> <Nav />
<style> <style>
.important {
font-weight: 900;
color: whitesmoke;
}
p { p {
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;

View File

@ -49,7 +49,7 @@
load(); load();
if ($uuid == null || $uuid == "") { if ($uuid == null || $uuid == "") {
toast("PIS Codes will soon be hidden for unregistered users. Register in the menu.", { toast("Register to see PIS codes", {
duration: 3000, duration: 3000,
}); });
} }
@ -88,7 +88,7 @@
const url = `${getApiUrl()}/api/v2/timetable/train/${formattedDate}/${searchType}/${id}`; const url = `${getApiUrl()}/api/v2/timetable/train/${formattedDate}/${searchType}/${id}`;
try { try {
const res = await fetch(url, options); const res = await fetch(url, options);
if (res.status == 200) { if (res.status < 300) {
let services = await res.json(); let services = await res.json();
if (!services.length) { if (!services.length) {
error = true; error = true;
@ -113,6 +113,10 @@
} }
</script> </script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} /> <Header {title} />
<TimeBar updatedTime={undefined} /> <TimeBar updatedTime={undefined} />
<div id="dateSelector"> <div id="dateSelector">