Skip to content

Commit de02dfc

Browse files
committed
ots: aria-live status, calendar-health badge, SEO title/description
Three quick wins from the iStampit comparison: 1. aria-live on stamp + verify status alerts. Wraps the @switch in a role="status" aria-live="polite" container so screen readers announce state transitions ('Submitted!', 'Could not stamp file', etc.) without us having to manually trigger announcements. 2. Calendar-health badge in the diagram. Tints the middle 'Calendar' box and adds a small dot+label under the sublabel based on the freshest calendar's lastBlocktime: <= 6h ago -> green 'healthy' <= 24h ago -> yellow 'slow' > 24h ago -> red 'stuck' no data -> default neutral Tooltip on the box names the freshest calendar and the absolute age. Live signal for visitors that the system they're about to use is actually working right now. 3. Per-route SEO meta on /ots/calendars. Sets title 'OpenTimestamps' and a description that pitches the page's actual capabilities ('multi-calendar fan-out, auto-upgrade, no third party in the loop, free'). Twitter / OG cards inherit ordpool's site-level og:image. Skipped (per the user's call): paste-a-hash input, API docs page, sparkline (the latter goes to ordpool-stats as a follow-up).
1 parent 727712e commit de02dfc

6 files changed

Lines changed: 164 additions & 41 deletions

File tree

frontend/src/app/components/_ordpool/ots-calendars/ots-calendars.component.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnDestroy } from '@angular/core';
1+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnDestroy, OnInit } from '@angular/core';
22
import { catchError, of, Subject, takeUntil } from 'rxjs';
33

44
import {
55
OrdpoolApiService,
66
OrdpoolOtsCalendarStats,
77
OrdpoolOtsRow,
88
} from '../../../services/ordinals/ordpool-api.service';
9+
import { SeoService } from '../../../services/seo.service';
910

1011
/*
1112
Test cases:
@@ -29,10 +30,11 @@ Test cases:
2930
changeDetection: ChangeDetectionStrategy.OnPush,
3031
standalone: false,
3132
})
32-
export class OtsCalendarsComponent implements OnDestroy {
33+
export class OtsCalendarsComponent implements OnInit, OnDestroy {
3334

3435
private api = inject(OrdpoolApiService);
3536
private cdr = inject(ChangeDetectorRef);
37+
private seo = inject(SeoService);
3638
private destroy$ = new Subject<void>();
3739

3840
calendars: OrdpoolOtsCalendarStats[] = [];
@@ -58,8 +60,18 @@ export class OtsCalendarsComponent implements OnDestroy {
5860
});
5961
}
6062

63+
ngOnInit(): void {
64+
this.seo.setTitle('OpenTimestamps');
65+
this.seo.setDescription(
66+
'Anchor any file to Bitcoin and verify .ots receipts entirely in your browser. ' +
67+
'Multi-calendar fan-out, auto-upgrade, no third party in the loop. Free.',
68+
);
69+
}
70+
6171
ngOnDestroy(): void {
6272
this.destroy$.next();
6373
this.destroy$.complete();
74+
this.seo.resetTitle();
75+
this.seo.resetDescription();
6476
}
6577
}

frontend/src/app/components/_ordpool/ots-diagram/ots-diagram.component.html

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@
1414
</div>
1515

1616
<div class="ots-diagram-box ots-diagram-calendar"
17-
title="Public calendar servers (alice / bob / finney / catallaxy) accept your hash and batch it with thousands of others into a Merkle tree.">
17+
[class.health-fresh]="health === 'fresh'"
18+
[class.health-aging]="health === 'aging'"
19+
[class.health-stale]="health === 'stale'"
20+
[title]="'Public calendar servers accept your hash and batch it with thousands of others into a Merkle tree. ' + healthLabel()">
1821
<fa-icon class="ots-diagram-icon" [icon]="['fas', 'calendar-check']" [fixedWidth]="true"></fa-icon>
1922
<div class="ots-diagram-label">Calendar</div>
2023
<div class="ots-diagram-sublabel">batches your hash<br>with thousands of others</div>
24+
@if (health !== 'unknown') {
25+
<div class="ots-diagram-health" [attr.aria-label]="healthLabel()">
26+
<span class="ots-health-dot"></span>
27+
@switch (health) {
28+
@case ('fresh') { healthy }
29+
@case ('aging') { slow }
30+
@case ('stale') { stuck }
31+
}
32+
</div>
33+
}
2134
</div>
2235

2336
<div class="ots-diagram-arrow" aria-hidden="true">

frontend/src/app/components/_ordpool/ots-diagram/ots-diagram.component.scss

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,34 @@
4949
&.ots-diagram-you { border-color: #4a5085; }
5050
&.ots-diagram-calendar { border-color: #d6a85a; }
5151
&.ots-diagram-bitcoin { border-color: #f7931a; }
52+
53+
// Calendar liveness override: the health colour wins over the default
54+
// calendar tint. Reflects the freshest calendar's last on-chain publish.
55+
&.ots-diagram-calendar.health-fresh { border-color: #5cb874; }
56+
&.ots-diagram-calendar.health-aging { border-color: #d6a85a; } // same as default; "slow but not broken"
57+
&.ots-diagram-calendar.health-stale { border-color: #d77a7a; }
58+
}
59+
60+
.ots-diagram-health {
61+
margin-top: 8px;
62+
font-size: 0.75rem;
63+
color: #9aa0c0;
64+
display: flex;
65+
align-items: center;
66+
justify-content: center;
67+
gap: 4px;
68+
69+
.ots-health-dot {
70+
width: 8px;
71+
height: 8px;
72+
border-radius: 50%;
73+
background-color: #9aa0c0;
74+
display: inline-block;
75+
}
76+
77+
.ots-diagram-calendar.health-fresh & .ots-health-dot { background-color: #5cb874; }
78+
.ots-diagram-calendar.health-aging & .ots-health-dot { background-color: #d6a85a; }
79+
.ots-diagram-calendar.health-stale & .ots-health-dot { background-color: #d77a7a; }
5280
}
5381

5482
.ots-diagram-arrow {
Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,91 @@
1-
import { ChangeDetectionStrategy, Component } from '@angular/core';
1+
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnDestroy, OnInit } from '@angular/core';
2+
import { Subject, takeUntil } from 'rxjs';
3+
import { catchError, of } from 'rxjs';
4+
5+
import { OrdpoolApiService, OrdpoolOtsCalendarStats } from '../../../services/ordinals/ordpool-api.service';
26

37
/*
48
Test cases:
59
- Live page: https://ordpool.space/ots/calendars
610
- Renders three boxes (you / calendar / bitcoin) on desktop, stacks vertical on mobile.
711
- Tooltips on each box explain the role in one sentence.
12+
- The Calendar box is tinted by the freshest calendar's lastBlocktime:
13+
- <= 6h ago -> green (system healthy)
14+
- <= 24h ago -> yellow (slow but not broken)
15+
- > 24h or null -> red (calendars are stuck or unreachable)
816
*/
917

10-
/**
11-
* Educational diagram for /ots/calendars: shows the three-stage pipeline
12-
* (your file -> public calendar -> Bitcoin) so the average visitor understands
13-
* what the drop-zone below is actually doing before they touch it.
14-
*
15-
* Pure presentation; no inputs, no state, no signals. The heavy lifting
16-
* happens in app-ots-stamp-verify.
17-
*/
18+
type CalendarHealth = 'fresh' | 'aging' | 'stale' | 'unknown';
19+
1820
@Component({
1921
selector: 'app-ots-diagram',
2022
templateUrl: './ots-diagram.component.html',
2123
styleUrls: ['./ots-diagram.component.scss'],
2224
changeDetection: ChangeDetectionStrategy.OnPush,
2325
standalone: false,
2426
})
25-
export class OtsDiagramComponent { }
27+
export class OtsDiagramComponent implements OnInit, OnDestroy {
28+
29+
private api = inject(OrdpoolApiService);
30+
private cdr = inject(ChangeDetectorRef);
31+
private destroy$ = new Subject<void>();
32+
33+
health: CalendarHealth = 'unknown';
34+
freshestNickname = '';
35+
freshestMinutesAgo: number | null = null;
36+
37+
ngOnInit(): void {
38+
this.api.getOtsCalendars$()
39+
.pipe(catchError(() => of([] as OrdpoolOtsCalendarStats[])), takeUntil(this.destroy$))
40+
.subscribe(rows => {
41+
this.computeHealth(rows);
42+
this.cdr.markForCheck();
43+
});
44+
}
45+
46+
ngOnDestroy(): void {
47+
this.destroy$.next();
48+
this.destroy$.complete();
49+
}
50+
51+
private computeHealth(rows: OrdpoolOtsCalendarStats[]): void {
52+
if (!rows || rows.length === 0) {
53+
this.health = 'unknown';
54+
return;
55+
}
56+
let bestSecs = 0;
57+
let bestRow: OrdpoolOtsCalendarStats | null = null;
58+
for (const r of rows) {
59+
if (typeof r.lastBlocktime === 'number' && r.lastBlocktime > bestSecs) {
60+
bestSecs = r.lastBlocktime;
61+
bestRow = r;
62+
}
63+
}
64+
if (!bestRow || !bestSecs) {
65+
this.health = 'unknown';
66+
return;
67+
}
68+
const ageMs = Date.now() - bestSecs * 1000;
69+
const ageMin = Math.max(0, Math.round(ageMs / 60000));
70+
this.freshestMinutesAgo = ageMin;
71+
this.freshestNickname = bestRow.calendar;
72+
if (ageMs <= 6 * 60 * 60 * 1000) this.health = 'fresh';
73+
else if (ageMs <= 24 * 60 * 60 * 1000) this.health = 'aging';
74+
else this.health = 'stale';
75+
}
76+
77+
/** Human-readable health string for the tooltip. */
78+
healthLabel(): string {
79+
if (this.health === 'unknown') return 'Calendar status: unknown (no data yet).';
80+
const ago = this.freshestMinutesAgo ?? 0;
81+
const human =
82+
ago < 60 ? `${ago} min ago` :
83+
ago < 24 * 60 ? `${Math.round(ago / 60)} h ago` :
84+
`${Math.round(ago / (24 * 60))} d ago`;
85+
const flavour =
86+
this.health === 'fresh' ? 'system healthy' :
87+
this.health === 'aging' ? 'slow but not broken' :
88+
'calendars look stuck';
89+
return `Most recent calendar publish: ${this.freshestNickname} ${human} (${flavour}).`;
90+
}
91+
}

frontend/src/app/components/_ordpool/ots-stamp-verify/ots-stamp.component.html

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,36 @@ <h2 class="card-title mb-2">Stamp a file</h2>
1919
</span>
2020
</label>
2121

22-
@switch (status.kind) {
23-
@case ('busy') {
24-
<div class="alert alert-info mt-3 mb-0">
25-
<span class="spinner-border spinner-border-sm me-2"></span>
26-
{{ status.message }}
27-
</div>
22+
<div role="status" aria-live="polite" aria-atomic="true">
23+
@switch (status.kind) {
24+
@case ('busy') {
25+
<div class="alert alert-info mt-3 mb-0">
26+
<span class="spinner-border spinner-border-sm me-2"></span>
27+
{{ status.message }}
28+
</div>
29+
}
30+
@case ('error') {
31+
<div class="alert alert-danger mt-3 mb-0">
32+
<strong>Could not stamp file:</strong> {{ status.message }}
33+
<button type="button" class="btn btn-sm btn-outline-light ms-2" (click)="reset()">Try again</button>
34+
</div>
35+
}
36+
@case ('wrong-zone') {
37+
<div class="alert alert-warning mt-3 mb-0">
38+
That looks like a <code>.ots</code> receipt, not a file to stamp. Drop
39+
it in the <strong>Verify a receipt</strong> zone instead.
40+
<button type="button" class="btn btn-sm btn-outline-light ms-2" (click)="reset()">OK</button>
41+
</div>
42+
}
43+
@case ('queued') {
44+
<div class="alert alert-success mt-3 mb-0">
45+
<strong>Submitted!</strong> <code>{{ status.filename }}</code> went out to
46+
{{ status.calendars.length }} calendar{{ status.calendars.length === 1 ? '' : 's' }}
47+
(<strong>{{ status.calendars.join(', ') }}</strong>) and is now in the queue
48+
below. You can drop another file in the meantime.
49+
</div>
50+
}
2851
}
29-
@case ('error') {
30-
<div class="alert alert-danger mt-3 mb-0">
31-
<strong>Could not stamp file:</strong> {{ status.message }}
32-
<button type="button" class="btn btn-sm btn-outline-light ms-2" (click)="reset()">Try again</button>
33-
</div>
34-
}
35-
@case ('wrong-zone') {
36-
<div class="alert alert-warning mt-3 mb-0">
37-
That looks like a <code>.ots</code> receipt, not a file to stamp. Drop
38-
it in the <strong>Verify a receipt</strong> zone instead.
39-
<button type="button" class="btn btn-sm btn-outline-light ms-2" (click)="reset()">OK</button>
40-
</div>
41-
}
42-
@case ('queued') {
43-
<div class="alert alert-success mt-3 mb-0">
44-
<strong>Submitted!</strong> <code>{{ status.filename }}</code> went out to
45-
{{ status.calendars.length }} calendar{{ status.calendars.length === 1 ? '' : 's' }}
46-
(<strong>{{ status.calendars.join(', ') }}</strong>) and is now in the queue
47-
below. You can drop another file in the meantime.
48-
</div>
49-
}
50-
}
52+
</div>
5153
</div>
5254
</div>

frontend/src/app/components/_ordpool/ots-stamp-verify/ots-verify.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ <h2 class="card-title mb-2">Verify a receipt</h2>
2020
</span>
2121
</label>
2222

23+
<div role="status" aria-live="polite" aria-atomic="true">
2324
@switch (status.kind) {
2425
@case ('busy') {
2526
<div class="alert alert-info mt-3 mb-0">
@@ -152,5 +153,6 @@ <h3 class="h5 mb-2">Receipt details</h3>
152153
</div>
153154
}
154155
}
156+
</div>
155157
</div>
156158
</div>

0 commit comments

Comments
 (0)