Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 2 additions & 23 deletions showcase/angular/src/app/pages/stats/stats-page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,8 @@
<h1><span i18n="@@stats.header.title">Stats</span> <span class="muted" i18n="@@stats.header.muted">for nerds</span></h1>
<p class="stats-sub" i18n="@@stats.header.subtitle">Live signals from <code [attr.translate]="'no'">fullselfbrowsing/FSB</code>.</p>
</div>
<span class="stats-live-badge" [class.is-live]="viewState.kind === 'ready'"
[class.is-partial]="viewState.kind === 'partial'"
[attr.title]="stateBadgeTitle" [attr.aria-label]="stateBadgeAriaLabel" role="status">
@switch (viewState.kind) {
@case ('ready') { <span i18n="@@stats.live.label">Live</span> }
@case ('partial') { <span i18n="@@stats.status.partial">Partial</span> }
@case ('loading') { <span i18n="@@stats.status.loadingLabel">Loading</span> }
@case ('error') { <span i18n="@@stats.status.unavailableLabel">Unavailable</span> }
}
<span class="stats-live-badge is-live" role="status">
<span i18n="@@stats.live.label">Live</span>
</span>
</header>

Expand Down Expand Up @@ -70,20 +63,6 @@ <h1><span i18n="@@stats.header.title">Stats</span> <span class="muted" i18n="@@s
</div>
}
@if (isViewRenderable) {
@if (viewState.kind === 'partial') {
<div class="stats-notice freshness-notice" role="status" aria-live="polite">
<i class="ph ph-clock" aria-hidden="true"></i>
<span>{{ partialFreshnessMessage }}</span>
</div>
}
@for (notice of qualityNotices; track notice.id) {
<div class="stats-notice quality-notice" [class.retry-notice]="notice.kind === 'retry'"
role="status" aria-live="polite">
<i class="ph" [class.ph-info]="notice.kind === 'quality'"
[class.ph-arrows-clockwise]="notice.kind === 'retry'" aria-hidden="true"></i>
<span>{{ notice.message }}</span>
</div>
}
<div class="chart-mount" [class.chart-mount-hidden]="selectedView === 'fsb-active-now'">
<canvas #chartCanvas role="img" [attr.aria-label]="chartAriaLabel"
[attr.aria-describedby]="accessibleChartData.length ? 'stats-chart-data' : null">
Expand Down
47 changes: 0 additions & 47 deletions showcase/angular/src/app/pages/stats/stats-page.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,6 @@
animation: stats-live-pulse 2.2s ease-in-out infinite;
}

.stats-live-badge.is-partial {
color: #d97706;
border-color: color-mix(in srgb, #d97706 45%, var(--border-color));
}

.stats-live-badge.is-partial::before {
background: #d97706;
}

@keyframes stats-live-pulse {
0%,
100% {
Expand Down Expand Up @@ -282,44 +273,6 @@
text-align: center;
}

.stats-notice {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin: 0 0 1rem;
padding: 0.65rem 0.8rem;
border: 1px solid color-mix(in srgb, #d97706 38%, var(--border-color));
border-radius: var(--radius-sm);
background: color-mix(in srgb, #d97706 9%, transparent);
color: var(--text-secondary);
font-size: 0.85rem;
line-height: 1.45;
}

.stats-notice i {
flex: 0 0 auto;
color: #d97706;
margin-top: 0.1rem;
}

.quality-notice {
border-color: color-mix(in srgb, #2563eb 38%, var(--border-color));
background: color-mix(in srgb, #2563eb 9%, transparent);
}

.quality-notice i {
color: #2563eb;
}

.retry-notice {
border-color: var(--border-color);
background: color-mix(in srgb, var(--bg-elevated) 72%, transparent);
}

.retry-notice i {
color: var(--text-muted);
}

.chart-mount canvas {
width: 100% !important;
height: 100% !important;
Expand Down
80 changes: 35 additions & 45 deletions showcase/angular/src/app/pages/stats/stats-page.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ describe('StatsPageComponent visualization lifecycle', () => {
});
});

describe('StatsPageComponent freshness and quality', () => {
describe('StatsPageComponent public status presentation', () => {
let github: FakeGitHubStatsService;
let fsb: FakeFSBTelemetryService;

Expand Down Expand Up @@ -352,61 +352,58 @@ describe('StatsPageComponent freshness and quality', () => {
.trim();
}

it('shows Live with a separate notice for incomplete star dates', async () => {
it('shows Live without exposing incomplete star history', async () => {
const fixture = await createFixture();

github.stars$.next(starState(Date.now(), '200', false));
await render(fixture);

expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(
fixture.nativeElement.querySelector('.stats-live-badge').getAttribute('title')
).toMatch(/Latest snapshot from .+; last checked .+/);
expect(
fixture.nativeElement.querySelector('.stats-live-badge').getAttribute('aria-label')
).toMatch(/^Live\. Latest snapshot from .+; last checked .+/);
expect(text(fixture, '.quality-notice')).toBe(
'The latest star total is available. Exact dates are unavailable for some star changes.'
);
expect(fixture.nativeElement.querySelector('.stats-live-badge').getAttribute('title'))
.toBeNull();
expect(fixture.nativeElement.querySelector('.stats-live-badge').getAttribute('aria-label'))
.toBeNull();
expect(fixture.nativeElement.querySelector('.stats-notice')).toBeNull();
});

it('shows Live with a neutral retry notice after a failed fresh GitHub check', async () => {
it('shows Live without exposing a failed fresh GitHub check', async () => {
const fixture = await createFixture();

github.stars$.next(starState(Date.now() - 5 * 60 * 1000, '403'));
await render(fixture);

expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(fixture.nativeElement.querySelector('.retry-notice')).not.toBeNull();
expect(text(fixture, '.retry-notice')).toContain('This snapshot is still current');
expect(fixture.nativeElement.querySelector('.stats-notice')).toBeNull();
});

it('does not call a future-skewed failed snapshot current', async () => {
it('keeps the public badge Live for a future-skewed usable snapshot', async () => {
const fixture = await createFixture();

github.stars$.next(starState(Date.now() + 6 * 60 * 1000, '403'));
await render(fixture);

expect(text(fixture, '.stats-live-badge')).toBe('Partial');
expect(fixture.nativeElement.querySelector('.retry-notice')).toBeNull();
expect(fixture.componentInstance.viewState.kind).toBe('partial');
expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(fixture.nativeElement.querySelector('.stats-notice')).toBeNull();
});

it('maps an old usable snapshot to Partial and a snapshot over 24 hours to Unavailable', async () => {
it('keeps the badge Live for partial and error states while preserving the hard-error card', async () => {
const fixture = await createFixture();

github.stars$.next(starState(Date.now() - 16 * 60 * 1000));
await render(fixture);
expect(text(fixture, '.stats-live-badge')).toBe('Partial');
expect(text(fixture, '.freshness-notice')).toContain(
'a fresh update is temporarily unavailable'
);
expect(fixture.componentInstance.viewState.kind).toBe('partial');
expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(fixture.nativeElement.querySelector('.stats-notice')).toBeNull();

github.stars$.next(starState(Date.now() - 24 * 60 * 60 * 1000 - 1));
await render(fixture);
expect(text(fixture, '.stats-live-badge')).toBe('Unavailable');
expect(fixture.componentInstance.viewState.kind).toBe('error');
expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(fixture.nativeElement.querySelector('.error-card')).not.toBeNull();
});

it('shows Live and coverage quality for Active now with 0 of 32 reporters', async () => {
it('does not expose Active now reporting coverage or missing regions', async () => {
const fixture = await createFixture('fsb-active-now');
fsb.headline$.next(readyState(fsbHeadline({
active_users_now: 32,
Expand All @@ -420,12 +417,7 @@ describe('StatsPageComponent freshness and quality', () => {
await render(fixture);

expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(text(fixture, '.quality-notice')).toContain(
'Agent totals include 0 of 32 active users in this snapshot that reported an agent count.'
);
expect(text(fixture, '.quality-notice:nth-of-type(2)')).toContain(
'regional aggregate is not available'
);
expect(fixture.nativeElement.querySelector('.stats-notice')).toBeNull();
const metricValues = Array.from(
fixture.nativeElement.querySelectorAll('.stats-tab-metrics strong')
).map((node) => (node as HTMLElement).textContent?.trim());
Expand All @@ -447,53 +439,51 @@ describe('StatsPageComponent freshness and quality', () => {
expect(text(fixture, '.stats-headline')).not.toContain('tokens (24h)');
});

it('does not let a delayed regional aggregate downgrade Active now', async () => {
it('does not expose a delayed regional aggregate', async () => {
const fixture = await createFixture('fsb-active-now');
fsb.headline$.next(readyState(fsbHeadline({
aggregate_updated_at: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(),
})));
await render(fixture);

expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(text(fixture, '.quality-notice')).toContain(
'The regional breakdown is older than two hours.'
);
expect(fixture.nativeElement.querySelector('.stats-notice')).toBeNull();
});

it('shows Active now as Partial when the request timestamp is missing', async () => {
it('keeps Active now publicly Live when its internal state is partial', async () => {
const fixture = await createFixture('fsb-active-now');
fsb.headline$.next(readyState(
fsbHeadline({ generated_at: 'not-a-date' }),
availability(null)
));
await render(fixture);

expect(text(fixture, '.stats-live-badge')).toBe('Partial');
expect(
fixture.nativeElement.querySelector('.stats-live-badge').getAttribute('aria-label')
).toContain('Partial. Latest snapshot update time unavailable');
expect(fixture.componentInstance.viewState.kind).toBe('partial');
expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(fixture.nativeElement.querySelector('.stats-live-badge').getAttribute('aria-label'))
.toBeNull();
});

it('keeps Tokens loading until independently ordered required responses arrive', async () => {
it('keeps the badge Live while Tokens waits for independently ordered responses', async () => {
const fixture = await createFixture('fsb-tokens');

fsb.series$.next(readyState(fsbSeries()));
await render(fixture);
expect(text(fixture, '.stats-live-badge')).toBe('Loading');
expect(fixture.componentInstance.viewState.kind).toBe('loading');
expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(fixture.nativeElement.querySelector('.skeleton')).not.toBeNull();

fsb.headline$.next(readyState(fsbHeadline()));
await render(fixture);
expect(text(fixture, '.stats-live-badge')).toBe('Live');
});

it('shows commit history quality without downgrading a fresh snapshot', async () => {
it('does not expose incomplete commit history', async () => {
const fixture = await createFixture('commits-cumulative');
github.commits$.next(readyState(commits(false)));
await render(fixture);

expect(text(fixture, '.stats-live-badge')).toBe('Live');
expect(text(fixture, '.quality-notice')).toBe(
'Commit history is incomplete, so totals and the chart may omit older commits.'
);
expect(fixture.nativeElement.querySelector('.stats-notice')).toBeNull();
});
});
Loading
Loading