Compare commits

...

35 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
58832ea2a8 Add Matomo analytics 2025-03-06 13:14:25 +00:00
479cc3051f Add anonymous analytics 2025-03-05 21:57:22 +00:00
de2258e309 Fix manifest image width 2025-02-13 01:55:03 +00:00
c254588a55 Bump version 2025-02-13 01:47:57 +00:00
4f84653c79 Fix small screenshots in manifest 2025-02-13 01:46:55 +00:00
7cfcdc7205 Bump version number 2025-01-22 00:10:55 +00:00
6d2ddb9966 Remove superfluous warning toasts.
Change registration code input to single input field
2025-01-22 00:10:30 +00:00
58ef9c153e Add maintenance mode 2024-11-22 18:04:10 +00:00
e9028153cb Fix translation of date selector elements 2024-11-15 11:35:48 +00:00
fb540c7a46 Fix date handling on train page 2024-11-15 11:34:02 +00:00
c672495a5f Adjust menu order 2024-11-14 01:52:25 +00:00
8b361bb7de Trial date selection in timetable search 2024-11-13 14:35:19 +00:00
29b2054b4c Update import to import type on more/reasons 2024-11-13 12:42:14 +00:00
c9262d64c8 Update PIS Finder to use Cards rather than Islands 2024-11-13 12:40:25 +00:00
2bc6efc677 Bump version to 2024.11.1 2024-11-11 10:56:02 +00:00
a7d4158fb5 Update src/routes/pis/+page.svelte 2024-11-10 19:17:38 +00:00
1d1e9416ab Re-enable 'Find by Code'
Backend fixes now mean this feature works again.
2024-11-10 19:13:32 +00:00
8a030964a4 Fix nginx-cnf 2024-10-12 20:22:57 +01:00
fe3e6be4ad Update nginx config 2024-10-12 19:57:36 +01:00
be850f5bd1 Stop caching shortcut icons - that is down to the OS. 2024-07-12 20:50:05 +01:00
0c635d99dd Change dash type used 2024-07-12 20:49:53 +01:00
70bba6635f Remove unused image sizes 2024-07-12 20:45:10 +01:00
53c5309485 Remove PNG logo images 2024-07-12 20:43:09 +01:00
7763f567f6 Move @tabler icons from dependencies to dev-dependencies 2024-07-12 20:17:29 +01:00
d48d4ffe4a Remove unneeded XCF version of file 2024-07-12 20:03:51 +01:00
10d749a5a7 Remove unused CSS Selector 2024-07-12 19:58:21 +01:00
61 changed files with 540 additions and 295 deletions

View File

@ -36,10 +36,10 @@ http {
#server owlboard-backend:8460;
# External to Kubernetes:
server 172.30.129.19:8460;
#server 172.30.129.19:8460;
# Within Docker:
#server owlboard-backend:8460
server backend:8460;
}
server {

18
package-lock.json generated
View File

@ -7,14 +7,12 @@
"": {
"name": "owlboard-svelte",
"version": "2024.03.2",
"dependencies": {
"@tabler/icons-svelte": "^3.2.0"
},
"devDependencies": {
"@owlboard/ts-types": "^1.2.0",
"@owlboard/ts-types": "^1.2.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.5.0",
"@tabler/icons-svelte": "^3.2.0",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.26.0",
@ -589,10 +587,11 @@
}
},
"node_modules/@owlboard/ts-types": {
"version": "1.2.0",
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fts-types/-/1.2.0/ts-types-1.2.0.tgz",
"integrity": "sha512-9xu7WJ+6eIiz6frd1O3/LGLrD4wAr16tI1Xd2WTkke8VONEm28f8T5M5J68pXPddHXOygXVkHUUFjAbTYrS+7Q==",
"dev": true
"version": "1.2.1",
"resolved": "https://git.fjla.uk/api/packages/OwlBoard/npm/%40owlboard%2Fts-types/-/1.2.1/ts-types-1.2.1.tgz",
"integrity": "sha512-3iLFBPmLblQiksvGciPxmnZ+1kvywYDH0Qb8BIY33tZqmkY+/IccqoaxLICRrVPzDo87YkiMwsjorHloxlXJog==",
"dev": true,
"license": "GPL-3.0-or-later"
},
"node_modules/@polka/url": {
"version": "1.0.0-next.25",
@ -696,6 +695,7 @@
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.7.0.tgz",
"integrity": "sha512-lJGIZLSWrPO6VygRUbaVvQjWgL2EaiBMD8e6leCYUQ8ZPO4LIzKMq358C8KlhXJcyNiRz1Io3YWoc/DNTcWqSg==",
"dev": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/codecalm"
@ -705,6 +705,7 @@
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@tabler/icons-svelte/-/icons-svelte-3.7.0.tgz",
"integrity": "sha512-G8/SINJ4sRxICHJMbQaLH2FWJZPFns4N383wvw2LQ7lQUT8NhhsKjK/i6LxyLZtyEjmVyGaEKpBLdz3SWldgBA==",
"dev": true,
"dependencies": {
"@tabler/icons": "3.7.0"
},
@ -2435,6 +2436,7 @@
"version": "3.59.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.59.2.tgz",
"integrity": "sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==",
"dev": true,
"engines": {
"node": ">= 8"
}

View File

@ -1,6 +1,6 @@
{
"name": "owlboard-svelte",
"version": "2024.03.2",
"version": "2024.11.4",
"private": true,
"scripts": {
"dev": "vite dev",
@ -13,7 +13,7 @@
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@owlboard/ts-types": "^1.2.0",
"@owlboard/ts-types": "^1.2.1",
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.5.0",
@ -27,10 +27,8 @@
"svelte-french-toast": "^1.2.0",
"svelte-sitemap": "^2.6.0",
"typescript": "^5.0.0",
"vite": "^4.3.0"
},
"type": "module",
"dependencies": {
"vite": "^4.3.0",
"@tabler/icons-svelte": "^3.2.0"
}
},
"type": "module"
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { onDestroy } from "svelte";
export let text: string;
let isVisible: boolean = false;
@ -44,15 +44,16 @@
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
top: 50%;
right: 125%;
transform: translateY(-50%);
margin-left: -60px;
opacity: 0;
transition: opacity 0.3s;
transition: opacity 0.5s;
}
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
opacity: 0.8;
}
</style>

View File

@ -1,4 +1,4 @@
export interface CardConfig {
interface CardConfig {
title: string;
showHelp: boolean;
showRefresh: boolean;
@ -7,7 +7,7 @@ export interface CardConfig {
refreshing: boolean;
}
export interface LookupCardConfig {
interface LookupCardConfig {
title: string;
formAction: string;
maxLen: number;
@ -15,3 +15,5 @@ export interface LookupCardConfig {
helpText: string;
fieldName: string;
}
export type {CardConfig, LookupCardConfig}

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

@ -96,10 +96,6 @@
{/await}
<style>
.transport-mode {
padding-top: 20px;
height: 17px;
}
.table-head-text {
color: white;
}

View File

@ -4,10 +4,7 @@
<div class="headerBar">
<a href="/">
<picture>
<source srcset="/images/logo/wide_logo.svg" type="image/svg+xml" />
<img src="/images/logo/wide_logo_200.png" alt="OwlBoard Logo" />
</picture>
<img src="/images/logo/wide_logo.svg" alt="OwlBoard Logo" />
</a>
<header>{title}</header>
</div>

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 = "2024.07.4";
export const version: string = "2025.05.1";
export const versionTag: string = "";

View File

@ -10,11 +10,12 @@
import TrainIcons from "./trainIcons.svelte";
export let service: OB_TrainTT_service;
export let date: Date;
let isExpanded = false;
async function getTrainByUID(tuid = "") {
const url = `${getApiUrl()}/api/v2/timetable/train/now/byTrainUid/${tuid}`;
const url = `${getApiUrl()}/api/v2/timetable/train/${date.toISOString().split('T')[0]}/byTrainUid/${tuid}`;
const options = {
method: "GET",
headers: {

View File

@ -5,12 +5,16 @@
const title = "OwlBoard - Error";
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<h1>{$page.status}: {$page?.error?.message}</h1>
{#if $page.status === 404}
<p>This is not the page you're looking for.</p>
<p>There's no light at the end of this tunnel</p>
<p>The page you are looking for doesn't exist, use the tabs below to find another page.</p>
{:else if $page.status === 500}
<p>

View File

@ -5,6 +5,7 @@
import DevBanner from "$lib/DevBanner.svelte";
import { page } from "$app/stores";
import { Toaster } from "svelte-french-toast";
import AnalyticsConsent from "$lib/popover/analytics-consent.svelte";
</script>
<svelte:head>
@ -21,7 +22,7 @@
<meta name="author" content="Frederick Boniface" />
<meta
name="description"
content="Get instant access to live train data, PIS codes, and location reference codes. Built by railway staff, for railway staff your fastest route to accurate information."
content="Get instant access to live train data, PIS codes, and location reference codes. Built by railway staff, for railway staff - your fastest route to accurate information."
/>
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content="#00b7b7" />
@ -33,6 +34,7 @@
</svelte:head>
<Toaster />
<AnalyticsConsent />
{#if dev}
<DevBanner />

View File

@ -8,6 +8,7 @@
import LookupCard from "$lib/cards/LookupCard.svelte";
import NearToMeCard from "$lib/cards/NearToMeCard.svelte";
import QuickLinkCard from "$lib/cards/QuickLinkCard.svelte";
import FindByHeadcodeCard from "$lib/cards/FindByHeadcodeCard.svelte";
const title = "OwlBoard";
const lookupCards: LookupCardConfig[] = [
{
@ -17,23 +18,15 @@
placeholder: "enter crs/tiploc",
maxLen: 7,
fieldName: "station",
},
{
title: "Timetable & PIS",
helpText: "",
formAction: "/train",
placeholder: "enter headcode",
maxLen: 4,
fieldName: "headcode",
},
}
];
onMount(async () => {
const featureSupport = featureDetect();
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) {
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>
@ -42,6 +35,7 @@
{#each lookupCards as config}
<LookupCard {config} />
{/each}
<FindByHeadcodeCard />
<NearToMeCard />
<QuickLinkCard />

View File

@ -5,6 +5,10 @@
const title = "404 - Not Found";
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<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>

View File

@ -5,6 +5,10 @@
const title = "50x - Server Error";
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<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>

View File

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

View File

@ -0,0 +1,14 @@
<script>
import LargeLogo from "$lib/images/large-logo.svelte";
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<LargeLogo />
<h1>
OwlBoard is down for maintenance
</h1>
<p>
Maintenance is expected to be complete by 23:59 on 22/11/2024
</p>

View File

@ -2,7 +2,6 @@
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import {
IconCode,
IconHelp,
IconInfoCircle,
IconLocation,
@ -17,11 +16,11 @@
const title = "More";
const links = [
{ title: "Your Data", path: "/more/data", icon: IconUser },
{ title: "Registration", path: "/more/reg", icon: IconUserPlus },
{ title: "Help", path: "https://docs.owlboard.info", icon: IconHelp },
{ title: "Settings", path: "/more/settings", icon: IconSettings },
{ title: "Help", path: "/more/help", icon: IconHelp },
{ title: "Your Data", path: "/more/data", icon: IconUser },
{ title: "About", path: "/more/about", icon: IconInfoCircle },
{ title: "Registration", path: "/more/reg", icon: IconUserPlus },
{ title: "Location Reference Code Lookup", path: "/more/corpus", icon: IconLocation },
{ title: "Reason Code Lookup", path: "/more/reasons", icon: IconMessageCode },
{ title: "Privacy Policy", path: "/more/privacy", icon: IconSpy },
@ -30,6 +29,10 @@
];
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
{#each links as item}

View File

@ -8,7 +8,7 @@
<Header {title} />
<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>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>

View File

@ -35,6 +35,10 @@
});
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<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";
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header title={"Help"} />
<Nav />
<br /><br />

View File

@ -4,41 +4,72 @@
const title = "Privacy Policy";
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<div>
<p>
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
data that we store, please visit <a href="/more/data">My Data</a>.
</p>
<p>OwlBoard does not utilize any cookies. IP addresses and browser fingerprints are not logged.</p>
<h2>If You Do Not Sign Up</h2>
<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
leave your device.
</p>
<h2>If You Sign Up</h2>
<p>
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
personal settings are stored locally in your browser and do not leave your device.
</p>
<p>
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
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
'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>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>
<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>
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
server after the nearest stations have been send to your device.
</p>
<h2>Reporting an Issue</h2>
<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>
Any data submitted when reporting an issue will be publicly viewable alongside the <a href="https://git.fjla.uk/owlboard/backend/issues" target="_blank"
>OwlBoard/backend git repository</a
>.
</p>
<h2>Your Data</h2>
<p>
OwlBoard logs the IP addresses of its users, this is done on the basis of legitimate
interest to ensure the security of the platform and to protect all users from
malicious activity. See <a href="#datasharing">Data Sharing</a> for details on how
we may share this data.
</p>
<p>
With the exception of sending emails, all data is held - and always remains within -
the United Kingdom.
</p>
<h3>Telemetry</h3>
<p>
If you opt-in to Telemetry, you will share your IP address and information about the
type of device and software you're using to access the service. This data is
anonymised and cannot be traced back to any individual. You can opt-in or opt-out in
your <a href="/more/settings">Settings</a>. All of the anonymised data can be viewed
at: <a target="_blank" href="https://liwan.fjla.uk">liwan.fjla.uk</a> at any time.
</p>
<p>
All of the data that is stored is held within the United Kingdom.
</p>
<p>
Telemetry data is used to identify which areas of the webapp are used most frequently
and where improvements need to be made.
</p>
<h3>Registering</h3>
<p>
If you register, your email address will be used to verify that you work for an
organisation that is permitted to access staff data on the service. An activation
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>
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>
<Nav />
@ -50,12 +81,23 @@
h2 {
color: var(--second-text-color);
margin: 10px;
margin: auto;
width: 90vw;
padding-top: 20px;
}
h3 {
color: var(--second-text-color);
margin: auto;
width: 90vw;
padding-top: 10px;
}
p {
color: white;
margin: 10px;
margin: auto;
padding-top: 5px;
padding-bottom: 8px;
width: 90vw;
max-width: 550px;
}
</style>

View File

@ -4,7 +4,7 @@
import Nav from "$lib/navigation/nav.svelte";
import Loading from "$lib/navigation/loading.svelte";
import Card from "$lib/cards/Card.svelte";
import { CardConfig } from "$lib/cards/Card.types";
import type { CardConfig } from "$lib/cards/Card.types";
import { apiGet } from "$lib/scripts/apiFetch";
interface ApiResponse {

View File

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

View File

@ -7,37 +7,11 @@
const title = "Submit Registration";
let state = false;
let status: string;
let inputs: { id: string; value: string }[] = [
{ id: "1", value: "" },
{ id: "2", value: "" },
{ id: "3", value: "" },
{ id: "4", value: "" },
{ id: "5", value: "" },
{ id: "6", value: "" },
];
function handleInput(index: number, event: KeyboardEvent): void {
if (event.key === "Backspace" && index > 0 && inputs[index].value === "") {
const prevInput = document.getElementById(`input-${index}`);
if (prevInput) {
prevInput.focus();
}
} else if (inputs[index].value.length === 1) {
const nextInput = document.getElementById(`input-${index + 2}`);
if (nextInput) {
nextInput.focus();
}
}
}
let inputString: string;
async function handleSubmit() {
let submitString: string = "";
for (const input of inputs) {
submitString += input.value.toUpperCase();
}
console.log(`Code: ${submitString}`);
const res = await submit(submitString);
console.log(`Code: ${inputString}`);
const res = await submit(inputString);
console.log(`Registration Status: ${res}`);
if (res == 201) {
status = "okay";
@ -71,6 +45,10 @@
}
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
{#if state}
@ -85,9 +63,7 @@
{:else}
<p class="title-ish">Enter your registration code below</p>
<form on:submit={handleSubmit} id="codeInputForm">
{#each inputs as input, index}
<input class="code-in" bind:value={input.value} id={`input-${input.id}`} maxlength="1" autocomplete="off" on:keydown={(event) => handleInput(index, event)} />
{/each}
<input class="code-in" bind:value={inputString} id={'input'} maxlength="6" autocomplete="off">
<br />
<button type="submit">Submit</button>
</form>
@ -102,7 +78,7 @@
.code-in {
margin: 3px;
margin-bottom: 20px;
width: 29px;
width: calc(29px * 6);
height: 39px;
font-size: 30px;
text-align: center;

View File

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

View File

@ -4,6 +4,7 @@
import QlSet from "$lib/islands/quick-link-set-island.svelte";
import Island from "$lib/islands/island.svelte";
import { location } from "$lib/stores/location";
import { telemetry } from "$lib/stores/telemetryConsent";
import { getCurrentLocation } from "$lib/scripts/getLocation";
import toast from "svelte-french-toast";
const title = "Settings";
@ -12,11 +13,15 @@
getCurrentLocation();
}
function locationToast() {
toast("Settings updated");
function confirmationToast() {
toast.success("Settings updated");
}
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<QlSet />
@ -25,10 +30,19 @@
<p>Use your location to quickly check departure boards near you</p>
<div class="checkbox-container">
<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>
</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 />
<style>
@ -47,4 +61,5 @@
margin-right: 25px;
font-weight: 800;
}
</style>

View File

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

View File

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

View File

@ -1,39 +1,34 @@
<script lang="ts">
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import Island from "$lib/islands/island.svelte";
import Loading from "$lib/navigation/loading.svelte";
import { uuid } from "$lib/stores/uuid";
import StylesToc from "$lib/train/styles-toc.svelte";
import { getApiUrl } from "$lib/scripts/upstream";
import toast from "svelte-french-toast";
import { onMount } from "svelte";
import type { OB_Pis_FullObject } from "@owlboard/ts-types";
import Card from "$lib/cards/Card.svelte";
import type { CardConfig } from "$lib/cards/Card.types";
import FindByHeadcodeCard from "$lib/cards/FindByHeadcodeCard.svelte";
const title = "PIS Finder";
const variables = { title: "Results" };
let entryPIS = "";
let entryStartCRS = "";
let entryEndCRS = "";
let data = [];
let data: OB_Pis_FullObject[] = [];
let error = false;
let errMsg = "Unknown Error";
let isLoading = false;
async function findByStartEnd() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/pis/byStartEnd/${entryStartCRS}/${entryEndCRS}`;
await fetchData(url);
isLoading = false;
}
/* Temporarily Disabled
async function findByPis() {
isLoading = true;
const url = `${getApiUrl()}/api/v2/pis/byCode/${entryPIS}`;
await fetchData(url);
isLoading = false;
}
*/
async function fetchData(url: string) {
const options = {
@ -76,29 +71,58 @@
entryPIS = "";
entryStartCRS = "";
entryEndCRS = "";
toast.success("Form reset");
isLoading = false;
}
onMount(() => {
toast("Registration soon required for PIS features.\n\nClick 'Register' in the menu.", {
if ($uuid == null || $uuid == "") {
toast("You must register to see results", {
duration: 3000,
});
}
});
const resultsCard: CardConfig = {
title: "Results",
showHelp: false,
helpText: "",
showRefresh: false,
onRefresh: ()=>{},
refreshing: false
}
const errorCard: CardConfig = {
title: "Error",
showHelp: true,
helpText: "There was an error searching for PIS Codes",
showRefresh: false,
onRefresh: () => {},
refreshing: false,
}
const findByStartEndCard: CardConfig = {
title: "Find by Start/End CRS",
showHelp: true,
helpText: "Enter a start and end CRS Code",
showRefresh: false,
onRefresh: () => {},
refreshing: false,
}
const findByPisCodeCard: CardConfig = {
title: "Find by PIS Code",
showHelp: true,
helpText: "Enter a PIS Code to see its details. (Four digits)",
showRefresh: false,
onRefresh: () => {},
refreshing: false,
}
</script>
<Header {title} />
{#if isLoading}
<Loading />
{/if}
{#if error}
<Island {variables}>
<Card config={errorCard}>
<p class="error">{errMsg}</p>
</Island>
</Card>
{:else if data.length}
<Island {variables}>
<Card config={resultsCard}>
<table>
<tr>
<th class="toc">TOC</th>
@ -113,27 +137,28 @@
</tr>
{/each}
</table>
</Island>
</Card>
<button id="reset" type="reset" on:click={reset}>Reset</button>
{:else}
<p>To search by headcode use the Train Finder on the homepage</p>
<p>This feature now supports all GWR Services</p>
<p class="label">Find By Start/End CRS:</p>
<FindByHeadcodeCard />
<Card config={findByStartEndCard}>
<form on:submit={findByStartEnd}>
<input type="text" maxlength="3" autocomplete="off" placeholder="Start" bind:value={entryStartCRS} />
<input type="text" maxlength="3" autocomplete="off" placeholder="End" bind:value={entryEndCRS} />
<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="End" required bind:value={entryEndCRS} />
<br />
<button type="submit">Search</button>
<button type="reset">Clear</button>
</form>
<!-- FIND BY PIS CODE NOT WORKING AT PRESENT
<p class="label">Find By PIS Code:</p>
<form on:submit={findByPis}>
<input type="number" max="9999" autocomplete="off" placeholder="PIS" bind:value={entryPIS} />
<br />
<button type="submit">Search</button>
</form>
-->
</Card>
<Card config={findByPisCodeCard}>
<form on:submit={findByPis}>
<input type="text" maxlength="4" pattern="^\d+$" autocomplete="off" placeholder="PIS" required bind:value={entryPIS} />
<br />
<button type="submit">Search</button>
<button type="reset" >Clear</button>
</form>
</Card>
{/if}
<button id="reset" type="reset" on:click={reset}>Reset</button>
<Nav />
<style>
@ -141,21 +166,20 @@
margin-left: 10px;
margin-right: 10px;
}
.label {
font-weight: 600;
color: var(--main-text-color);
}
input {
border: none;
border-radius: 50px;
font-family: urwgothic, sans-serif;
text-align: center;
text-transform: uppercase;
width: 30%;
width: 25%;
max-width: 250px;
height: 30px;
font-size: 16px;
margin-top: 10px;
margin-bottom: 15px;
margin-left: 10px;
margin-right: 10px;
box-shadow: var(--box-shadow);
}
button {
@ -167,8 +191,9 @@
margin: 0px;
margin-left: 10px;
margin-right: 10px;
margin-bottom: 15px;
height: 30px;
background-color: var(--island-bg-color);
background-color: var(--island-button-color);
color: white;
font-size: 16px;
box-shadow: var(--box-shadow);

View File

@ -1,39 +0,0 @@
<script lang="ts">
import type { LookupCardConfig } from "$lib/cards/Card.types";
import LookupCard from "$lib/cards/LookupCard.svelte";
import NearToMeCard from "$lib/cards/NearToMeCard.svelte";
import QuickLinkCard from "$lib/cards/QuickLinkCard.svelte";
import Header from "$lib/navigation/header.svelte";
import Nav from "$lib/navigation/nav.svelte";
import TimeBar from "$lib/navigation/TimeBar.svelte";
let LookupConfig: LookupCardConfig = {
title: "Live Arr/Dep Boards",
helpText: "Enter CRS, TIPLOC or STANOX code to see live departures",
placeholder: "Enter CRS/TIPLOC",
maxLen: 7,
formAction: "/ldb/",
fieldName: "station",
};
let TimetableConfig: LookupCardConfig = {
title: "Timetable & PIS",
helpText: "Enter a headcode to search the timetable and check PIS Codes",
placeholder: "Enter headcode",
maxLen: 4,
formAction: "/train/",
fieldName: "headcode",
};
function onRefresh() {
console.log("Refresh");
}
</script>
<Header title={"Test"} />
<TimeBar updatedTime={new Date()} />
<LookupCard config={LookupConfig} />
<LookupCard config={TimetableConfig} />
<NearToMeCard />
<QuickLinkCard />
<Nav />

View File

@ -1,6 +1,5 @@
<script lang="ts">
import Header from "$lib/navigation/header.svelte";
import Loading from "$lib/navigation/loading.svelte";
import Island from "$lib/islands/island.svelte";
import Nav from "$lib/navigation/nav.svelte";
import { uuid } from "$lib/stores/uuid";
@ -10,14 +9,17 @@
import { getApiUrl } from "$lib/scripts/upstream";
import toast from "svelte-french-toast";
import TimeBar from "$lib/navigation/TimeBar.svelte";
import { IconArrowLeft, IconArrowRight, IconCheck } from "@tabler/icons-svelte";
import Error from "../+error.svelte";
let title = "Timetable Results";
let id = "";
let data = [];
let isLoading = true;
let error = false;
let errMsg = "";
let formattedDate = new Date().toISOString().split('T')[0];
$: {
if (id) {
title = id.toUpperCase();
@ -30,26 +32,52 @@
return new URLSearchParams(window.location.search).get("headcode");
}
onMount(async () => {
isLoading = true;
id = (await getHeadcode()) || "";
const res = await fetchData(id);
if (res) {
data = res;
if (!data.length) {
error = true;
errMsg = "No services found";
}
}
isLoading = false;
function incrementDate() {
let dateInput = new Date(formattedDate)
dateInput.setDate(dateInput.getDate() + 1);
formattedDate = dateInput.toISOString().split('T')[0];
}
toast("Registration soon required for timetable features.\n\nClick 'Register' in the menu.", {
function decrementDate() {
let dateInput = new Date(formattedDate)
dateInput.setDate(dateInput.getDate() - 1);
formattedDate = dateInput.toISOString().split('T')[0];
}
onMount(async () => {
id = (await getHeadcode()) || "";
load();
if ($uuid == null || $uuid == "") {
toast("Register to see PIS codes", {
duration: 3000,
});
}
});
function load() {
error = false;
const selectedDate = new Date(formattedDate);
const currentDate = new Date();
const difference: number = currentDate.getTime() - selectedDate.getTime();
const differenceDays: number = difference / (1000 * 60 * 60 * 24)
if (differenceDays > 7) {
toast.error("Timetable data is not available for dates older than a week")
return
}
toast.promise(
fetchData(id),
{
loading: 'Searching Timetable',
success: 'Done',
error: 'No Services Found'
}
)
}
async function fetchData(id = "") {
const date = "now";
data = [];
const searchType = "headcode";
const options = {
method: "GET",
@ -57,44 +85,59 @@
uuid: $uuid,
},
};
const url = `${getApiUrl()}/api/v2/timetable/train/${date}/${searchType}/${id}`;
const url = `${getApiUrl()}/api/v2/timetable/train/${formattedDate}/${searchType}/${id}`;
try {
const res = await fetch(url, options);
if (res.status == 200) {
return await res.json();
if (res.status < 300) {
let services = await res.json();
if (!services.length) {
error = true;
errMsg = "No services found";
return Promise.reject(new Error(errMsg));
}
data = services
} else if (res.status === 401) {
error = true;
errMsg = "You must be logged into the staff version for this feature";
return false;
return Promise.reject(new Error(errMsg));
} else {
error = true;
errMsg = "Unable to connect, check your connection and try again";
return false;
return Promise.reject(new Error(errMsg));
}
} catch (err) {
error = true;
errMsg = "Connection error, try again later";
return Promise.reject(new Error(errMsg));
}
isLoading = false;
}
</script>
<svelte:head>
<meta name="robots" content="noindex">
</svelte:head>
<Header {title} />
<TimeBar updatedTime={undefined} />
<div id="dateSelector">
<button on:click={decrementDate}><IconArrowLeft /></button>
<input
type="date"
id="dateInput"
bind:value={formattedDate}
/>
<button on:click={incrementDate}><IconArrowRight /></button>
<button on:click={load}><IconCheck /></button>
</div>
{#if error}
<Island>
<p style="font-weight:600">{errMsg}</p>
</Island>
{/if}
{#if isLoading}
<Loading />
{/if}
{#each data as service}
{#if service}
<TrainDetail {service} />
<TrainDetail {service} date={new Date(formattedDate)} />
{/if}
{/each}
@ -106,4 +149,15 @@
font-size: 18px;
font-weight: 600;
}
#dateInput {
height: 25px;
transform: translateY(-8px) translateX(20px);
}
button {
background: none;
border: none;
color: white;
transform: translateX(20px);
}
</style>

View File

@ -7,8 +7,10 @@ const cacheName = `ob-${version}`;
const assets = [...build, ...files, "/service-worker.js"];
const excludePatterns = [
"/static/images/screnshots/",
"/static/images/screnshots",
"/images/screenshots",
"/static/images/shortcuts",
"/images/shortcuts",
];
self.addEventListener("install", (event) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@ -32,130 +32,88 @@
],
"screenshots": [
{
"src": "/images/screenshots/mobile/home.png",
"src": "/images/screenshots/new_mobile/home.png",
"type": "image/png",
"sizes": "270x600",
"sizes": "400x760",
"form_factor": "narrow",
"label": "Home"
},
{
"src": "/images/screenshots/mobile/cis.png",
"src": "/images/screenshots/new_mobile/cis.png",
"type": "image/png",
"sizes": "270x800",
"sizes": "400x760",
"form_factor": "narrow",
"label": "Live departure boards"
},
{
"src": "/images/screenshots/mobile/timetable-detail.png",
"src": "/images/screenshots/new_mobile/timetable-detail.png",
"type": "image/png",
"sizes": "270x800",
"sizes": "400x760",
"form_factor": "narrow",
"label": "Train details and PIS Codes"
},
{
"src": "/images/screenshots/mobile/pis-search.png",
"src": "/images/screenshots/new_mobile/pis-search.png",
"type": "image/png",
"sizes": "270x800",
"sizes": "400x760",
"form_factor": "narrow",
"label": "PIS Codes"
},
{
"src": "/images/screenshots/mobile/cis-train-detail.png",
"type": "image/png",
"sizes": "270x800",
"form_factor": "narrow",
"label": "Departure board detail"
},
{
"src": "/images/screenshots/mobile/cis-alert.png",
"type": "image/png",
"sizes": "270x800",
"form_factor": "narrow",
"label": "Service disruption messages"
},
{
"src": "/images/screenshots/mobile/settings.png",
"type": "image/png",
"sizes": "270x800",
"form_factor": "narrow",
"label": "Customise your settings"
},
{
"src": "/images/screenshots/mobile/location-code.png",
"type": "image/png",
"sizes": "270x800",
"form_factor": "narrow",
"label": "CRS, TIPLOC, STANOX & NLC Lookup"
},
{
"src": "/images/screenshots/mobile/reason-code.png",
"type": "image/png",
"sizes": "270x800",
"form_factor": "narrow",
"label": "Delay/Cancellation reason code lookup"
},
{
"src": "/images/screenshots/wide/home.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "Home"
},
{
"src": "/images/screenshots/wide/cis.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "Live departure boards"
},
{
"src": "/images/screenshots/wide/timetable-detail.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "Timetable & PIS"
},
{
"src": "/images/screenshots/wide/pis.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "PIS Lookup"
},
{
"src": "/images/screenshots/wide/cis-train-detail.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "Live train details"
},
{
"src": "/images/screenshots/wide/cis-alerts.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "Live service updates"
},
{
"src": "/images/screenshots/wide/settings.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "Customise your settings"
},
{
"src": "/images/screenshots/wide/location-code.png",
"type": "image/png",
"sizes": "1080x1369",
"sizes": "1639x1080",
"form_factor": "wide",
"label": "CRS, TIPLOC, STANOX & NLC Lookup"
},
{
"src": "/images/screenshots/wide/reason-code.png",
"type": "image/png",
"sizes": "1080x1369",
"form_factor": "wide",
"label": "Delay/Cancellation reason code lookup"
}
],
"shortcuts": [