Compare commits

..

7 Commits

8 changed files with 322 additions and 317 deletions

View File

@@ -66,7 +66,7 @@
border-radius: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.5);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
}
.toggle-thumb {

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import type { ApiStationsBoard } from '@owlboard/owlboard-ts';
import { formatUkTime, estClass, delayClassFromTimePair } from '$lib/utils/time';
import { fade } from 'svelte/transition';
import { formatUkTime, estClass, delayClassFromTimePair, isRightTime } from '$lib/utils/time';
import { fade, slide, fly } from 'svelte/transition';
import { flip } from 'svelte/animate';
let { services }: { services: ApiStationsBoard.BoardService[] } = $props();
@@ -10,364 +11,311 @@
</script>
<section class="departure-board">
<div class="header">
<div class="header-row"></div>
<div class="header-row"></div>
<div class="header container">
<div class="header row">
<div class="upper-header-blank"></div>
<div class="upper-header-text">Arr</div>
<div class="upper-header-text">Dep</div>
</div>
<div class="header row">
<div class="cell">ID</div>
<div class="cell">Orig</div>
<div class="cell">Dest</div>
<div class="cell">Plt</div>
<div class="cell">Sch</div>
<div class="cell">Act</div>
<div class="cell">Sch</div>
<div class="cell">Act</div>
</div>
</div>
<div class="services">
<!-- Keyed EACH Here -->
{#each services as service (getRowKey(service))}
{@const realArrival = formatUkTime(service.ata || service.eta)}
{@const realDeparture = formatUkTime(service.atd || service.etd)}
{@const realPass = formatUkTime(service.atp || service.etp)}
<div
class="service-block"
animate:flip={{ duration: 400 }}
transition:slide={{ duration: 300 }}
>
<div
class="service row"
class:passing={service.wtp}
class:nonPax={!service.ip}
class:cancelled={service.c}
>
<div class="cell id">{service.h}</div>
<div class="cell orig">
{#key service.og.t}
<div transition:fade={{ duration: 300 }}>
{service.og.t}
</div>
{/key}
</div>
<div class="cell dest">
{#key service.dt.t}<div transition:fade={{ duration: 300 }}>{service.dt.t}</div>{/key}
</div>
<div class="cell plt" class:platsup={service.ps} class:platchange={service.pc}>
{#key service.p}<div transition:fade={{ duration: 300 }}>{service.p || '-'}</div>{/key}
</div>
{#if service.wtp}
<div class="cell pass">PASS</div>
<div class="cell sch">
{formatUkTime(service.wtp)}
</div>
<div
class="cell act {estClass(service.atp, service.etp)} {delayClassFromTimePair(service.wtp, service.atp || service.etp)}"
>
{#if isRightTime(service.wtp, service.atp || service.etp)}
RT
{:else if service.c}
CANC
{:else}
{realPass}
{/if}
</div>
{:else}
<div class="cell sch">{formatUkTime(service.sta)}</div>
<div
class="cell act {estClass(service.ata, service.eta)} {delayClassFromTimePair(service.sta, service.ata || service.eta)}"
>
{#if isRightTime(service.sta, service.ata || service.eta)}
RT
{:else if service.sta && service.c} CAN {:else}
{realArrival}
{/if}
</div>
<div class="cell sch">{formatUkTime(service.std)}</div>
<div class="cell act {estClass(service.atd, service.etd)} {delayClassFromTimePair(service.std, service.atd || service.etd)}">
{#if isRightTime(service.std, service.atd || service.etd)}
RT
{:else if service.std && service.c} CANC {:else}
{realDeparture}
{/if}
</div>
{/if}
</div>
<div class="operator row">{service.o}</div>
{#if service?.cr?.r}
{#if service.c}
<div class="cancel-reason row" transition:slide={{ duration: 300 }}>{service.cr.r}</div>
{:else}
<div class="cancel-reason row" transition:slide={{ duration: 300 }}>
Part of this service has been cancelled
</div>
{/if}
{:else if service?.dr?.r}
<div class="delay-reason row" transition:slide={{ duration: 300 }}>{service.dr.r}</div>
{/if}
</div>
{/each}
</div>
</section>
<table class="departure-board">
<colgroup>
<col style="width:10%;" />
<!-- ID (Headcode) -->
<col style="width:17%;" />
<!-- ORIG (Origin) -->
<col style="width:17%;" />
<!-- DEST (Destination) -->
<col style="width:9%;" />
<!-- PLT (Platform) -->
<col style="width: 12%;" />
<!-- STA -->
<col style="width: 12%;" />
<!-- ETA/ATA -->
<col style="width: 12%;" />
<!-- STD -->
<col style="width: 11%;" />
<!-- ETD/ATD -->
</colgroup>
<thead>
<tr>
<th colspan="4" aria-hidden="true"></th>
<th scope="colgroup" colspan="2" class="upper-head"><abbr title="Arrival">Arr</abbr></th>
<th scope="colgroup" colspan="2" class="upper-head"><abbr title="Departure">Dep</abbr></th>
</tr>
<tr>
<th scope="col"><abbr title="Headcode">ID</abbr></th>
<th scope="col"><abbr title="Origin">Orig</abbr></th>
<th scope="col"><abbr title="Destination">Dest</abbr></th>
<th scope="col"><abbr title="Platform">Plt</abbr></th>
<th scope="col"><abbr title="Scheduled">Sch</abbr></th>
<th scope="col"><abbr title="Actual/Expected">Act</abbr></th>
<th scope="col"><abbr title="Scheduled">Sch</abbr></th>
<th scope="col"><abbr title="Actual/Expected">Act</abbr></th>
</tr>
</thead>
{#each services as service (getRowKey(service))}
<tbody>
<tr
in:fade={{duration: 150, delay:150}}
out:fade={{duration:150}}
class="service-row"
class:serviceCancelled={service.c}
class:servicePass={service.wtp}
class:serviceNonPassenger={!service.ip}
>
<td class="id-cell">{service.h}</td>
<td class="orig-cell"
><abbr title={service.og.n.toLocaleUpperCase()}>{service.og.t}</abbr></td
>
<td class="dest-cell"
><abbr title={service.dt.n.toLocaleUpperCase()}>{service.dt.t}</abbr></td
>
<td class="plt-cell" class:platSup={service.ps} class:platChange={service.pc}
>{service.p || '-'}</td
>
<!-- Handle different display for a passing train -->
{#if service.wtp}
<td class="pass-cell" colspan="2">Pass</td>
<td class="time-cell">{formatUkTime(service.wtp)}</td>
<!-- If cancelled, show '-', otherwise check for RT or show time -->
<td
class="time-cell {estClass(service.atp, service.etp)} {delayClassFromTimePair(
service.wtp,
service.atp || service.etp
)}"
>
<span>
{service.c
? '-'
: delayClassFromTimePair(service.wtp, service.atp || service.etp) === 'delay-rt'
? 'RT'
: formatUkTime(service.atp || service.etp)}
</span>
</td>
{:else}
<td class="time-cell">{formatUkTime(service.sta)}</td>
<!-- Arrival Actual/Expected -->
<td
class="time-cell {estClass(service.ata, service.eta)} {delayClassFromTimePair(
service.sta,
service.ata || service.eta
)}"
>
<span>
{service.c
? '-'
: delayClassFromTimePair(service.sta, service.ata || service.eta) === 'delay-rt'
? 'RT'
: formatUkTime(service.ata || service.eta)}
</span>
</td>
<td class="time-cell">{formatUkTime(service.std)}</td>
<!-- Departure Actual/Expected -->
<td
class="time-cell {estClass(service.atd, service.etd)} {delayClassFromTimePair(
service.std,
service.atd || service.etd
)}"
>
<span>
{service.c
? '-'
: delayClassFromTimePair(service.std, service.atd || service.etd) === 'delay-rt'
? 'RT'
: formatUkTime(service.atd || service.etd)}
</span>
</td>
{/if}
</tr>
{#if service.o}
<tr class="toc-coach-row" in:fade={{ duration: 150, delay: 150 }} out:fade={{ duration: 150 }}>
<td colspan="8">
{service.o}
</td>
</tr>
{/if}
{#if service.c && service.cr?.r}
<tr class="cancel-row" in:fade={{ duration: 150, delay: 150 }} out:fade={{ duration: 150 }}>
<td colspan="8">
{service.cr.r}
{#if service.cr.l}
{service.cr.n ? 'near' : 'at'}
{service.cr.l}
{/if}
</td>
</tr>
{/if}
{#if service.dr?.r}
<tr class="delay-row" in:fade={{ duration: 150, delay: 150 }} out:fade={{ duration: 150 }}>
<td colspan="9">
{service.dr.r}
{#if service.dr.l}
{service.dr.n ? 'near' : 'at'}
{service.dr.l}
{/if}
</td>
</tr>
{/if}
</tbody>
{/each}
</table>
<style>
.departure-board {
width: 100%;
max-width: 100%;
margin: 5px auto;
border-collapse: collapse;
table-layout: fixed;
}
.departure-board td {
font-family: 'Inconsolata Variable', monospace;
font-size: clamp(1rem, 0.475rem + 2.8vw, 1.35rem);
font-variant-ligatures: additional-ligatures;
box-sizing: border-box;
overflow: hidden;
}
thead,
.cancel-row td,
.delay-row td {
letter-spacing: -0.15ch;
}
thead {
.header.container {
position: sticky;
top: 0;
z-index: 2;
background: var(--color-bg-dark);
}
abbr {
text-decoration: none;
border-bottom: none;
cursor: help;
.row.header {
font-weight: 875;
}
.row.header:last-child {
padding-bottom: 1rem;
}
tbody tr:first-child td {
border-top: 5px solid transparent;
.upper-header-blank {
grid-column: span 4;
}
.upper-header-text {
grid-column: span 2;
text-align: center;
}
/* Row Logic */
.serviceCancelled {
color: rgb(255, 131, 131);
.row {
display: grid;
grid-template-columns: 10% 17% 17% 9% 12% 12% 12% 11%;
align-items: center;
text-align: center;
gap: 0;
box-sizing: border-box;
font-family: 'Inconsolata Variable', monospace;
font-size: clamp(1rem, 0.475rem + 2.8vw, 1.35rem);
font-variant-ligatures: additional-ligatures;
}
.servicePass td {
opacity: 0.75;
.service.row.passing {
font-style: italic;
opacity: 0.25;
font-weight: 200;
}
.serviceNonPassenger td {
opacity: 0.5;
font-weight: 290;
font-style: italic;
.service.row.nonPax {
opacity: 0.25;
font-weight: 200;
}
.service-block {
width: 100%;
max-width: 100%;
padding-bottom: 7px;
}
.cell > div {
display: block;
width: 100%;
}
/* Row Logic */
.service.cancelled {
color: rgb(255, 131, 131);
}
/* Special Row Styles */
.cancel-row td,
.delay-row td {
font-size: 0.88rem;
.operator.row,
.delay-reason.row,
.cancel-reason.row {
grid-template-columns: 1fr;
text-align: left;
font-size: 0.88rem;
}
.cancel-row td[colspan],
.delay-row td[colspan] {
padding-left: 0ch;
}
.cancel-row td {
.cancel-reason.row {
color: rgb(255, 131, 131);
font-weight: 400;
letter-spacing: -0.1ch;
}
.delay-row td {
.delay-reason.row {
color: var(--delay-orange);
font-style: italic;
font-weight: 400;
letter-spacing: -0.1ch;
}
@media (min-width: 375px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.08rem;
}
}
@media (min-width: 420px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.12rem;
}
}
@media (min-width: 550px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.18rem;
}
}
@media (min-width: 620px) {
.cancel-row td,
.delay-row td {
.operator.row,
.delay-reason.row,
.cancel-reason.row {
font-size: 1.2rem;
}
}
.toc-coach-row td {
font-family: 'URW Gothic', sans-serif;
font-size: 0.7rem;
.operator.row {
/* font-family: 'URW Gothic', sans-serif; */
padding: 0;
color: rgb(187, 187, 255);
}
@media (min-width: 400px) {
.toc-coach-row td {
font-size: 0.8rem;
}
}
@media (min-width: 550px) {
.toc-coach-row td {
font-size: 0.85rem;
}
}
/* Column Specifics */
.id-cell {
.cell.id {
text-align: left;
font-weight: 400;
font-stretch: 85%;
filter: brightness(0.75);
}
@media (min-width: 400px) {
.id-cell {
.cell.id {
font-stretch: 90%;
}
}
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 80%;
font-weight: 400;
}
.orig-cell {
.cell.orig {
text-align: left;
color: var(--location-yellow);
}
.dest-cell {
.cell.dest {
text-align: right;
color: var(--location-yellow);
}
@media (min-width: 350px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 85%;
}
@media (min-width: 400px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 90%;
}
}
@media (min-width: 490px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 95%;
}
}
@media (min-width: 520px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
font-stretch: 100%;
}
.orig-cell {
.cell.orig {
text-align: right;
padding-right: 7px;
}
.dest-cell {
.cell.dest {
text-align: left;
padding-left: 7px;
}
}
@media (min-width: 600px) {
.orig-cell,
.dest-cell {
.cell.orig,
.cell.dest {
letter-spacing: 0.05ch;
}
}
}
.plt-cell {
.cell.plt {
text-align: center;
font-weight: 410;
font-stretch: 100%;
}
@media (min-width: 525px) {
.plt-cell {
.cell.plt {
font-stretch: 110%;
}
}
.plt-cell.platSup {
font-weight: 200;
opacity: 0.3;
.cell.plt.platsup {
font-weight: 150;
}
.plt-cell.platChange {
.cell.plt.platchange {
animation: fast-pulse 2s ease-out infinite;
}
.service-row.serviceCancelled .plt-cell {
text-decoration: line-through;
}
.pass-cell {
text-align: center;
font-stretch: 100%;
@@ -378,54 +326,58 @@
color: rgb(255, 131, 131);
}
.time-cell {
.cell.pass {
grid-column: span 2;
}
.cell.sch,
.cell.act {
text-align: center;
font-stretch: 72%;
}
.cell.act.delay-late {
color: var(--delay-orange);
}
.cell.act.delay-early {
color: var(--early-blue);
}
@media (min-width: 350px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 77%;
}
}
@media (min-width: 400px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 82%;
}
}
@media (min-width: 525px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 90%;
}
}
@media (min-width: 600px) {
.time-cell {
.cell.sch,
.cell.act {
font-stretch: 100%;
}
}
/* RT Logic */
.time-cell.delay-rt span {
display: none;
}
.time-cell.delay-rt::after {
content: 'RT';
}
.time-cell.delay-early {
color: var(--early-blue);
}
.time-cell.delay-late {
color: var(--delay-orange);
.cell.act.est {
font-style: italic;
font-weight: 300;
opacity: 0.5;
}
/* Time Types */
.est {
.cell.act.estimate {
font-style: italic;
opacity: 0.75;
font-weight: 350;
}
.act {
.cell.act.actual {
font-weight: 600;
}
</style>

View File

@@ -49,8 +49,8 @@
{#each services as service (getRowKey(service))}
<tbody>
<tr
in:fade={{duration: 150, delay:150}}
out:fade={{duration:150}}
in:fade={{ duration: 150, delay: 150 }}
out:fade={{ duration: 150 }}
class="service-row"
class:serviceCancelled={service.c}
class:servicePass={service.wtp}
@@ -126,7 +126,11 @@
</tr>
{#if service.o}
<tr class="toc-coach-row" in:fade={{ duration: 150, delay: 150 }} out:fade={{ duration: 150 }}>
<tr
class="toc-coach-row"
in:fade={{ duration: 150, delay: 150 }}
out:fade={{ duration: 150 }}
>
<td colspan="8">
{service.o}
</td>

View File

@@ -13,7 +13,7 @@ const DEFAULT_PREFS: Preferences = {
NearToMe: false,
ShowPassingTrains: true,
BoardWakeLock: true,
AutoRefresh: true,
AutoRefresh: true
};
function loadPrefs(): Preferences {

View File

@@ -1,6 +1,6 @@
import type { ApiTrainsTrainDetails } from '@owlboard/owlboard-ts';
export const estClass = (act: any, est: any) => (act ? 'act' : 'est');
export const estClass = (act: any, est: any) => (act ? 'actual' : 'estimate');
/**
* Converts ISO/JSON time to UK-formatted HH:MM string, with optional (default off) seconds
@@ -132,3 +132,23 @@ export function delayClassFromTimePair(sched: any, act: any): string {
return diff > 0 ? 'delay-late' : 'delay-early';
}
/**
* Accepts a pair of times (string or Date) and returns true if service considered 'on-time'
* @param sched Scheduled Time (string, Date)
* @param act Actual Time (string, Date)
*/
export function isRightTime(
sched: string | Date | undefined,
act: string | Date | undefined
): boolean {
console.log(`Checking [sched: ${sched}, act: ${act}]`);
if (!sched || !act) return false;
const s = new Date(sched).getTime();
const a = new Date(act).getTime();
if (isNaN(s) || isNaN(a)) return false;
return Math.abs(a - s) < 60000;
}

View File

@@ -21,7 +21,7 @@
IconSettings,
IconHelp,
IconDots,
IconSpy,
IconSpy
} from '@tabler/icons-svelte-runes';
onMount(async () => {
@@ -49,7 +49,7 @@
{ label: 'PIS', path: '/pis/', icon: IconDialpad },
{ label: 'Options', path: '/preferences/', icon: IconSettings },
{ label: 'About', path: '/about/', icon: IconHelp },
{ label: 'Privacy Policy', path: '/privacy/', icon: IconSpy },
{ label: 'Privacy Policy', path: '/privacy/', icon: IconSpy }
];
let navWidth = $state(0);

View File

@@ -24,10 +24,10 @@
if ('wakeLock' in navigator && !wakeLock) {
try {
wakeLock = await navigator.wakeLock.request('screen');
console.log("Wake lock obtained");
console.log('Wake lock obtained');
} catch (error) {
console.warn("Wake lock request rejected: ", error);
}
console.warn('Wake lock request rejected: ', error);
}
}
};
@@ -35,9 +35,9 @@
if (wakeLock) {
try {
await wakeLock.release();
console.log("Wake lock released");
console.log('Wake lock released');
} catch (error) {
console.error("Wake lock release failed: ", error);
console.error('Wake lock release failed: ', error);
}
wakeLock = null;
}
@@ -48,20 +48,20 @@
if (intervalId) return;
intervalId = window.setInterval(async () => {
try {
console.log("Invalidating data")
console.log('Invalidating data');
await invalidateAll();
} catch (error) {
console.error("Background data fetch failed: ", error)
console.error('Background data fetch failed: ', error);
}
}, 20000);
console.log("Polling started")
console.log('Polling started');
};
const stopPolling = async () => {
if (intervalId) {
window.clearInterval(intervalId);
intervalId = undefined;
console.log("Polling ended")
console.log('Polling ended');
}
await releaseWakeLock();
};
@@ -85,7 +85,7 @@
stopPolling();
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
})
});
onMount(() => {
const interval = setInterval(() => {
@@ -96,24 +96,25 @@
});
const currentBoardId = $derived(
data.BoardLocation ? (data.BoardLocation.c ?? data.BoardLocation.t) : null
);
data.BoardLocation ? (data.BoardLocation.c ?? data.BoardLocation.t) : null
);
// Update 'QuickLinks' when currentBoardId changes
$effect(() => {
if (currentBoardId) {
// Keep the execution and logs inside untrack to isolate dependencies completely
untrack(() => {
quickLinks.recordVisit(currentBoardId);
console.log(`QuickLink visit recorded: ${JSON.stringify(data.BoardLocation)}`);
});
}
});
$effect(() => {
if (currentBoardId) {
// Keep the execution and logs inside untrack to isolate dependencies completely
untrack(() => {
quickLinks.recordVisit(currentBoardId);
console.log(`QuickLink visit recorded: ${JSON.stringify(data.BoardLocation)}`);
});
}
});
// Wake Lock API Handling
// Load Data Invalidation Handling
// Refresh countdown logic
</script>
<section class="board-wrapper">
<div class="time-data">
<div class="loadedlive">
@@ -129,6 +130,10 @@ $effect(() => {
<div class="service-list-wrapper">
<StaffServicesGrid services={data.boardData.data.s} />
</div>
{:else}
<div class="no-service">
No services at this location in the next three hours
</div>
{/if}
</section>
@@ -144,6 +149,13 @@ $effect(() => {
gap: 0;
}
.no-service {
display: block;
margin: auto;
max-width: 75%;
font-family: 'URW Gothic', sans-serif;
}
.time-data {
display: flex;
justify-content: space-between;
@@ -177,17 +189,31 @@ $effect(() => {
div.isLive {
background-color: #10b981;
animation:
led-pulse 2.5s infinite ease-in-out;
animation: led-pulse 2.5s infinite ease-in-out;
}
@keyframes led-blink {
0%,100%{opacity:1;}
50%{opacity:0.2;filter:saturate(0.2)}
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.2;
filter: saturate(0.2);
}
}
@keyframes led-pulse{
0%,100%{opacity: 1;filter:saturate(1);box-shadow: 0 0 6px 1px rgba(16,185,129,0.4)}
50%{opacity: 0.2;filter:saturate(0.2);box-shadow:0 0 2px 0px rgba(16,185,129,0.1)}
@keyframes led-pulse {
0%,
100% {
opacity: 1;
filter: saturate(1);
box-shadow: 0 0 6px 1px rgba(16, 185, 129, 0.4);
}
50% {
opacity: 0.2;
filter: saturate(0.2);
box-shadow: 0 0 2px 0px rgba(16, 185, 129, 0.1);
}
}
.time-loaded,

View File

@@ -20,8 +20,8 @@
requested solely to send a verification code. This needs to happen, as some data is
restricted to rail staff only due to licensing restrictions.
<strong
>The moment the verification code is sent, the email address is permanently deleted
from the system.</strong
>The moment the verification code is sent, the email address is permanently deleted from
the system.</strong
> It is not stored, it is not logged, and it cannot be looked up later.
</li>
<li class="list-item">
@@ -42,16 +42,17 @@
</li>
<li class="list-item">
<strong class="highlight-text">Fuzzy Location Data:</strong> If the "Near to Me" toggle is
enabled, exact GPS coordinates are calculated, but never leave your device. Your device compresses the position
into a <strong class="highlight-text">"fuzzy location block"</strong> (a geohash) roughly 1.2km
by 0.6km in size before contacting the server to request nearby stations. OwlBoard only processes
that neighborhood block—never an exact location.
enabled, exact GPS coordinates are calculated, but never leave your device. Your device
compresses the position into a
<strong class="highlight-text">"fuzzy location block"</strong> (a geohash) roughly 1.2km by 0.6km
in size before contacting the server to request nearby stations. OwlBoard only processes that
neighborhood block—never an exact location.
</li>
<li class="list-item">
<strong class="highlight-text">Server Logs &amp; IP Addresses:</strong> Like any web application,
self-hosted security tools and the Web Application Firewall (WAF) temporarily process network
IP addresses. This is strictly for system health, infrastructure monitoring, and blocking malicious or
automated attacks.
IP addresses. This is strictly for system health, infrastructure monitoring, and blocking malicious
or automated attacks.
</li>
</ul>
</section>
@@ -89,19 +90,20 @@
<ul class="policy-list">
<li class="list-item">
<strong class="highlight-text">Instant SAR (Right of Access):</strong> The
<strong>Account</strong> dashboard contains a dedicated section displaying
every single line of data which can be identified with the account key block. It can be viewed or copied instantly.
<strong>Account</strong> dashboard contains a dedicated section displaying every single line of
data which can be identified with the account key block. It can be viewed or copied instantly.
</li>
<li class="list-item">
<strong class="highlight-text">The Right to be Forgotten (Instant Erasure):</strong> A
prominent <strong>"Delete Account"</strong> button is available on that same page. Clicking this
instantly wipes the account key. You will be able to re-register at any time, and all of your preferences remain only on your device.
instantly wipes the account key. You will be able to re-register at any time, and all of your
preferences remain only on your device.
</li>
</ul>
<p class="policy-text">
While the pseudonymized settings left behind are not technically personal data once
disconnected from an identity, the OwlBoard project operates on a simple philosophy.
When the delete button is pressed, the data is erased.
disconnected from an identity, the OwlBoard project operates on a simple philosophy. When the
delete button is pressed, the data is erased.
</p>
</section>
@@ -111,8 +113,9 @@
<h3 class="section-heading">4. No Third-Party Conglomerates</h3>
<p class="policy-text">
All data is hosted transparently on isolated, self-hosted server infrastructure in Somerset. OwlBoard does
not use third-party analytics trackers, tracking cookies, or advertising networks.
All data is hosted transparently on isolated, self-hosted server infrastructure in Somerset.
OwlBoard does not use third-party analytics trackers, tracking cookies, or advertising
networks.
</p>
</section>
</section>