diff --git a/apps/web/e2e/home.spec.ts b/apps/web/e2e/home.spec.ts
index 0dd7970..793224e 100644
--- a/apps/web/e2e/home.spec.ts
+++ b/apps/web/e2e/home.spec.ts
@@ -1,3 +1,5 @@
+import type { Page } from '@playwright/test';
+
import { home } from './demo';
import { expect, test } from './fixtures';
@@ -25,6 +27,35 @@ test('the Home leaves out This week when its answer has no week', async ({ page
await expect(page.locator('main section').last()).toContainText('Your apps');
});
+// The colour --ink-muted gives text in the page's theme.
+function inkMuted(page: Page) {
+ return page.evaluate(() => {
+ const probe = document.body.appendChild(document.createElement('span'));
+ probe.style.color = 'var(--ink-muted)';
+ const { color } = getComputedStyle(probe);
+ probe.remove();
+ return color;
+ });
+}
+
+test("the Home says the hours are unavailable when home couldn't get the week", async ({ page }, testInfo) => {
+ await page.route('/api/home', (route) =>
+ route.fulfill({ json: { ...home, week: undefined, weekState: 'unavailable' } })
+ );
+ await page.goto('/');
+
+ const week = page.locator('main section').last();
+ await expect(week.getByRole('heading', { level: 2 })).toHaveText('This week');
+ await expect(week.getByRole('link', { name: 'Day report' })).toHaveAttribute('href', '/time');
+ // In place of the hours, in their type and muted, without the label or the bar (decision 187).
+ const hours = week.getByText('Hours unavailable');
+ await expect(hours).toHaveCSS('font-size', testInfo.project.name === 'phone' ? '24px' : '28px');
+ await expect(hours).toHaveCSS('font-weight', '500');
+ await expect(hours).toHaveCSS('color', await inkMuted(page));
+ await expect(week.getByText('Logged on Jira')).toHaveCount(0);
+ await expect(week.locator('.track')).toHaveCount(0);
+});
+
test('without a session, every page shows the Signed out screen', async ({ page }) => {
await page.route('/api/home', (route) => route.fulfill({ status: 401 }));
await page.goto('/');
diff --git a/apps/web/src/home/HomePage.svelte b/apps/web/src/home/HomePage.svelte
index 70c1885..c09020b 100644
--- a/apps/web/src/home/HomePage.svelte
+++ b/apps/web/src/home/HomePage.svelte
@@ -5,7 +5,8 @@ import NeedsYou from './NeedsYou.svelte';
import ThisWeek from './ThisWeek.svelte';
import YourApps from './YourApps.svelte';
-// Until the Home first loads, only the greeting and the apps show; the week, only when the Home has one.
+// Until the Home first loads, only the greeting and the apps show; the week, when the Home has one, and its
+// head with "Hours unavailable" when home couldn't get it from the time service (decision 187).
let { now, home }: { now: Date; home?: Home } = $props();
@@ -16,4 +17,6 @@ let { now, home }: { now: Date; home?: Home } = $props();
{#if home?.week}
+{:else if home?.weekState === 'unavailable'}
+
{/if}
diff --git a/apps/web/src/home/HomePage.test.ts b/apps/web/src/home/HomePage.test.ts
index 7031b10..26a7a2c 100644
--- a/apps/web/src/home/HomePage.test.ts
+++ b/apps/web/src/home/HomePage.test.ts
@@ -14,6 +14,9 @@ const withoutWeek: Home = {
const withWeek: Home = { ...withoutWeek, week: { loggedHours: 31.5, targetHours: 40 } };
+// home asked the time service for the week, and couldn't get it.
+const unavailable: Home = { ...withoutWeek, weekState: 'unavailable' };
+
// The week's head, its link and its hours; null for each that doesn't show.
function weekParts(): (HTMLElement | null)[] {
return [
@@ -74,3 +77,52 @@ test('shows no week before the Home first loads', () => {
expect(weekParts()).toEqual([null, null, null]);
expect(screen.getByRole('heading', { level: 2, name: 'Your apps' })).toBeInTheDocument();
});
+
+test("says the hours are unavailable when home couldn't get the week, under its head and link, after the apps", () => {
+ render(HomePage, { props: { now, home: unavailable } });
+
+ const [head, link, hours] = weekParts();
+ expect(head).toBeInTheDocument();
+ expect(link).toHaveAttribute('href', '/time');
+ expect(hours).toBeNull();
+ expect(screen.getByText('Hours unavailable')).toBeInTheDocument();
+ const heads = screen.getAllByRole('heading', { level: 2 }).map((heading) => heading.textContent);
+ expect(heads).toEqual(['Needs you today', 'Your apps', 'This week']);
+});
+
+test('shows the week when the Home has one, whatever its state says', () => {
+ render(HomePage, { props: { now, home: { ...withWeek, weekState: 'unavailable' } } });
+
+ expect(weekParts()).not.toContain(null);
+ expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument();
+});
+
+test('leaves out the week when the time service had none to give, as without Jira', () => {
+ render(HomePage, { props: { now, home: { ...withoutWeek, weekState: 'ok' } } });
+
+ expect(weekParts()).toEqual([null, null, null]);
+ expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument();
+});
+
+test('follows each later Home: from the hours to unavailable, to no week, and back', async () => {
+ const { rerender } = render(HomePage, { props: { now, home: withWeek } });
+
+ await rerender({ home: unavailable });
+
+ expect(screen.getByText('Hours unavailable')).toBeInTheDocument();
+ expect(screen.queryByText('31h 30m / 40h')).not.toBeInTheDocument();
+
+ await rerender({ home: withoutWeek });
+
+ expect(weekParts()).toEqual([null, null, null]);
+ expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument();
+
+ await rerender({ home: unavailable });
+
+ expect(screen.getByText('Hours unavailable')).toBeInTheDocument();
+
+ await rerender({ home: withWeek });
+
+ expect(weekParts()).not.toContain(null);
+ expect(screen.queryByText('Hours unavailable')).not.toBeInTheDocument();
+});
diff --git a/apps/web/src/home/ThisWeek.svelte b/apps/web/src/home/ThisWeek.svelte
index 2013b1b..ab919c4 100644
--- a/apps/web/src/home/ThisWeek.svelte
+++ b/apps/web/src/home/ThisWeek.svelte
@@ -4,7 +4,7 @@ import Section from '../shell/Section.svelte';
import WeekCard from '../time/WeekCard.svelte';
import type { Week } from '../time/week';
-let { week }: { week: Week } = $props();
+let { week }: { week?: Week } = $props();
diff --git a/apps/web/src/home/ThisWeek.test.ts b/apps/web/src/home/ThisWeek.test.ts
index ba3ffd2..541dce7 100644
--- a/apps/web/src/home/ThisWeek.test.ts
+++ b/apps/web/src/home/ThisWeek.test.ts
@@ -10,3 +10,11 @@ test("shows the week's card under its head, which links to the day report", () =
expect(screen.getByRole('link', { name: 'Day report' })).toHaveAttribute('href', '/time');
expect(screen.getByText('30h / 40h')).toBeInTheDocument();
});
+
+test('without a week, keeps its head and link, and its card says the hours are unavailable', () => {
+ render(ThisWeek);
+
+ expect(screen.getByRole('heading', { level: 2, name: 'This week' })).toBeInTheDocument();
+ expect(screen.getByRole('link', { name: 'Day report' })).toHaveAttribute('href', '/time');
+ expect(screen.getByText('Hours unavailable')).toBeInTheDocument();
+});
diff --git a/apps/web/src/home/api.ts b/apps/web/src/home/api.ts
index 8364c55..ff442eb 100644
--- a/apps/web/src/home/api.ts
+++ b/apps/web/src/home/api.ts
@@ -18,9 +18,12 @@ export interface Item {
late: boolean;
}
+/** How home's asking went: the peer answered, or it failed or didn't answer in time. */
+type State = 'ok' | 'unavailable';
+
/** How asking one of home's sources went, with only the field the app reads so far. */
interface SourceStatus {
- state: 'ok' | 'unavailable';
+ state: State;
}
/** The Home screen's data from GET /api/home, with only the fields the app reads so far. */
@@ -29,8 +32,10 @@ export interface Home {
items: Item[];
/** One per source home asks; absent from the answer of a home older than the field. */
sources?: SourceStatus[];
- /** Absent when home has no week, such as before the time service logs hours. */
+ /** Absent when home has no week: the time service has none, as without Jira, couldn't give one, or isn't there. */
week?: Week;
+ /** How asking the time service for the week went; absent without one to ask, or from a home older than the field. */
+ weekState?: State;
}
/** The error fetchHome fails with when the gateway answers 401: the user has no session. */
diff --git a/apps/web/src/time/WeekCard.svelte b/apps/web/src/time/WeekCard.svelte
index 6f0e122..f6f64dc 100644
--- a/apps/web/src/time/WeekCard.svelte
+++ b/apps/web/src/time/WeekCard.svelte
@@ -1,18 +1,24 @@