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
14 changes: 1 addition & 13 deletions apps/web/e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { Page } from '@playwright/test';

import { home } from './demo';
import { expect, test } from './fixtures';
import { inkMuted } from './ink';

test('the Home greets the user and lists what needs them', async ({ page }) => {
await page.route('/api/home', (route) => route.fulfill({ json: home }));
Expand All @@ -27,17 +26,6 @@ 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' } })
Expand Down
12 changes: 12 additions & 0 deletions apps/web/e2e/ink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Page } from '@playwright/test';

// The colour --ink-muted gives text in the page's theme.
export 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;
});
}
62 changes: 62 additions & 0 deletions apps/web/e2e/page-status.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Page } from '@playwright/test';

import { home, tickets } from './demo';
import { expect, test } from './fixtures';
import { inkMuted } from './ink';
import { answerDismissals, answerTickets } from './jira';
import { answerPulls, answerReviews } from './pulls';

// Each page's line in place of its sections, and the answers that make the page show it.
const lines: { name: string; path: string; says: string; answer: (page: Page) => Promise<void> }[] = [
{
name: 'PRs',
path: '/pulls',
says: "Bitbucket isn't set up.",
answer: async (page) => {
await answerPulls(page, 404);
await answerReviews(page, 404);
},
},
{
name: 'Time',
path: '/time',
says: "Jira isn't set up.",
answer: async (page) => {
await answerTickets(page, tickets);
await page.route('/api/time/report', (route) => route.fulfill({ status: 404 }));
},
},
{
name: 'Nudge',
path: '/nudge',
says: "Couldn't load the questions.",
answer: async (page) => {
await page.route('/api/nudge/state', (route) => route.fulfill({ status: 502 }));
await page.route('/api/nudge/events', (route) =>
route.fulfill({ contentType: 'text/event-stream', body: 'retry: 600000\n: open\n\n' })
);
},
},
{
name: 'Todos',
path: '/todos',
says: "Couldn't load the todos.",
answer: async (page) => {
await page.route('/api/todos', (route) => route.fulfill({ status: 500, body: 'Internal Server Error\n' }));
await answerDismissals(page);
await answerTickets(page, tickets);
},
},
];

for (const { name, path, says, answer } of lines) {
test(`the ${name} page's status line is muted, with a section's space below it`, async ({ page }, testInfo) => {
await page.route('/api/home', (route) => route.fulfill({ json: home }));
await answer(page);
await page.goto(path);

const line = page.getByText(says);
await expect(line).toHaveCSS('color', await inkMuted(page));
await expect(line).toHaveCSS('padding-bottom', testInfo.project.name === 'phone' ? '48px' : '64px');
});
}
1 change: 1 addition & 0 deletions apps/web/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import './styles/list.css';
import './styles/chip.css';
import './styles/icon-button.css';
import './styles/dialog.css';
import './styles/page.css';

import App from './App.svelte';
import { applyChoice, loadChoice } from './shell/theme';
Expand Down
10 changes: 1 addition & 9 deletions apps/web/src/nudge/NudgePage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,5 @@ onMount(() => {
<Questions questions={loaded.state.questions} answered={refresh} />
<Sessions sessions={loaded.state.sessions} />
{:else if loaded.kind === 'failed'}
<p class="failed">Couldn't load the questions.</p>
<p class="page-status">Couldn't load the questions.</p>
{/if}

<style>
.failed {
margin: 0;
padding: 0 var(--page-padding) 64px;
color: var(--ink-muted);
}
</style>
16 changes: 1 addition & 15 deletions apps/web/src/pulls/PullsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ onMount(() => {
<PageHead>Pull requests</PageHead>
<!-- Without Bitbucket, the pulls service answers 404 for both lists; one older than the reviews, for those alone. -->
{#if mine.kind === 'off' && reviews.kind === 'off'}
<p class="off">Bitbucket isn't set up.</p>
<p class="page-status">Bitbucket isn't set up.</p>
{:else}
<PullsSection
pulls={mine}
Expand All @@ -40,17 +40,3 @@ onMount(() => {
failed="Couldn't load the review requests."
/>
{/if}

<style>
.off {
margin: 0;
padding: 0 var(--page-padding) 64px;
color: var(--ink-muted);
}

@media (max-width: 700px) {
.off {
padding-bottom: 48px;
}
}
</style>
14 changes: 14 additions & 0 deletions apps/web/src/styles/page.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* A page's status line in place of its sections, such as "Bitbucket isn't set up." (decision 206): muted, with the
space that a section leaves under it. */
.page-status {
margin: 0;
padding: 0 var(--page-padding) 64px;
color: var(--ink-muted);
overflow-wrap: anywhere;
}

@media (max-width: 700px) {
.page-status {
padding-bottom: 48px;
}
}
18 changes: 3 additions & 15 deletions apps/web/src/time/TimePage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ onMount(() => {

<PageHead>Time</PageHead>
{#if report.kind === 'off'}
<p class="off">Jira isn't set up.</p>
<p class="page-status">Jira isn't set up.</p>
{:else if report.kind === 'failed'}
<p class="off">Couldn't load the time report.</p>
<p class="page-status">Couldn't load the time report.</p>
{:else if report.kind === 'loaded'}
{@const { days, week, searched, failure } = report.snapshot}
{#if failure}
<p class={days ? 'failure' : 'off'}>
<p class={['page-status', { failure: days }]}>
The last search failed: {failure}.
{#if days && searched}
These hours are from <time datetime={searched}>{searchedLabel(searched)}</time>.
Expand All @@ -59,24 +59,12 @@ onMount(() => {
<FillDialog {fill} />

<style>
.off,
.failure {
margin: 0;
padding: 0 var(--page-padding) 64px;
color: var(--ink-muted);
overflow-wrap: anywhere;
}

/* Over the sections, the line leaves the space that the Todos page's form leaves over its first section. */
.failure {
padding-bottom: 30px;
}

@media (max-width: 700px) {
.off {
padding-bottom: 48px;
}

.failure {
padding-bottom: 24px;
}
Expand Down
9 changes: 6 additions & 3 deletions apps/web/src/time/TimePage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ test.each([
answer(async () => new Response('No\n', { status }));
render(TimePage);

expect(await screen.findByText(says)).toHaveClass('off');
const line = await screen.findByText(says);
expect(line).toHaveClass('page-status');
expect(line).not.toHaveClass('failure');
expect(heads()).toEqual([]);
});

Expand All @@ -90,7 +92,7 @@ test('says why the last search failed, and when the hours it shows are from', as
expect(line).toHaveTextContent(
'The last search failed: search worklogs: 502 Bad Gateway. These hours are from 21 October at 09:38.'
);
expect(line).toHaveClass('failure');
expect(line).toHaveClass('page-status', 'failure');
expect(within(line).getByText('21 October at 09:38')).toHaveAttribute('datetime', searched);
expect(heads()).toEqual(['This week', 'Last 7 days']);
});
Expand All @@ -101,6 +103,7 @@ test('says why the last search failed, with no section, before any search has su

const line = await screen.findByText(/^The last search failed/);
expect(line).toHaveTextContent(/^The last search failed: find me: 401 Unauthorized\.$/);
expect(line).toHaveClass('off');
expect(line).toHaveClass('page-status');
expect(line).not.toHaveClass('failure');
expect(heads()).toEqual([]);
});
11 changes: 2 additions & 9 deletions apps/web/src/todos/TodosPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ onMount(() => {
</Section>
{/each}
{:else if list.failed}
<p class="failed">Couldn't load the todos.</p>
<p class="page-status">Couldn't load the todos.</p>
{/if}
<!-- The tickets wait for the todos to show or fail, so the todos never push them down. -->
{#if todos || list.failed}
Expand All @@ -81,15 +81,8 @@ onMount(() => {
list-style: none;
}

.failed {
margin: 0;
padding: 0 var(--page-padding) 64px;
color: var(--ink-muted);
}

@media (max-width: 700px) {
.last,
.failed {
.last {
padding-bottom: 48px;
}
}
Expand Down
16 changes: 13 additions & 3 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh
| `homelab-github` | The homelab repo runs the `github` service on the laptop, with the token in a Secret, and names it among the gateway's and home's peers, before `github-gateway` makes the gateway require it. The fourth of `github-widgets`' seven steps (decision 200) | Pending |
| `github-gateway` | The gateway forwards `/api/github` to the `github` service, behind both of its locks. The fifth of `github-widgets`' seven steps (decision 200) | Pending |
| `shared-polled-section` | The PRs page's lists and the Todos page's Jira tickets show a poller's list through one component in the shell, `PolledSection`, for the GitHub page's lists, its third user; nothing changes on screen. The sixth of `github-widgets`' eight steps (decision 205) | Done (#184) |
| `github-page` | The app shows the three lists, on a page of their own at `/github` or beside Bitbucket's on the PRs page, as the user chooses. The seventh of `github-widgets`' eight steps (decisions 200 and 205) | Pending |
| `github-attention` | The `github` service answers the attention contract, so the Home's "Needs you" lists what the user chooses of the review requests, the issues assigned to me and my PRs. The last of `github-widgets`' eight steps (decisions 200 and 205) | Pending |
| `shared-page-status` | The PRs, Time, Nudge and Todos pages draw the line that takes their sections' place, such as "Bitbucket isn't set up.", from one global class, for the GitHub page's, its fifth user; on phones, the Nudge page's line now leaves a section's 48 px below it. The seventh of `github-widgets`' nine steps (decision 206) | Done (#187) |
| `github-page` | The app shows the three lists, on a page of their own at `/github` or beside Bitbucket's on the PRs page, as the user chooses. The eighth of `github-widgets`' nine steps (decisions 200, 205 and 206) | Pending |
| `github-attention` | The `github` service answers the attention contract, so the Home's "Needs you" lists what the user chooses of the review requests, the issues assigned to me and my PRs. The last of `github-widgets`' nine steps (decisions 200, 205 and 206) | Pending |
| `games-exophase` | The games widgets from Exophase (§8.2 Phase 5), by a session of their own in `../bakhyard-games/`, which adds its steps' rows under this one (decision 195) | In progress |
| `games-plan` | `games-exophase` splits in eleven steps: the Exophase client, the cache, the service, the Twitch token, the IGDB search, the covers, the laptop, the gateway, the page and its covers; the games list, the covers' matches and the Twitch token stay in memory. The first of `games-exophase`'s eleven steps (decision 300) | Done (#168) |
| `exophase-client` | The `games` service's Exophase client: the games I last played since a given time, the last played first, each with its platforms, playtime and trophies, from the games list's pages until one ends before that time, four at most; a 403 from Cloudflare's challenge says so. The second of `games-exophase`'s eleven steps (decision 301) | Done (#172) |
Expand Down Expand Up @@ -1011,6 +1012,8 @@ Cards are solid `--surface`, so they sit on the grain like paper on a desk. Thes

**Section head**: a 1 px `--ink` rule on top with 13 px of space under it (ref), then the title on the left and a mono note or a text link on the right, aligned to the bottom, with 30 px below (24 on phones) (ref). A note that doesn't fit beside the title wraps under it. Each section has 64 px of space below it (48 on phones).

**Status line**: a line that takes a page's sections' place, such as "Bitbucket isn't set up." or "Couldn't load the todos.", in body text and `--ink-muted` under the page head, with a section's space below it, and a long word breaks rather than widen the page (decision 206).

- One word of the title is the serif accent: "Needs you *today*", "Your *apps*", "This *week*".
- **Text link**: 14 px with a line height of 1.5, kept on one line (ref). A 1 px `--ink-muted` underline sits 5 px below the text (ref) and turns `--ink` on hover, with no transition, like the nav. A → arrow, the same as the app cards', follows 22 px after the text (ref), and the underline runs under it too.

Expand Down Expand Up @@ -1263,13 +1266,14 @@ Every item is pending unless it says done. The service an item lands in is in br

**Phase 4: GitHub**

- `github-widgets` [github]. Needs `frontend-shell`. Eight steps (decisions 200 to 205):
- `github-widgets` [github]. Needs `frontend-shell`. Nine steps (decisions 200 to 206):
- `shared-web-link`: the Bitbucket client's check that a PR's page is an https URL moves to the root `internal/weblink`, for the GitHub client; nothing changes in behaviour (decision 201). Done (#171).
- `github-client`: a client of GitHub's search finds my open PRs, the PRs that wait for my review or my teams', and the open issues assigned to me, across every owner, through a classic PAT with `repo` and `read:org` (decisions 202 and 203). Done (#173).
- `github-service`: the `github` service polls the three searches every 5 minutes, within search's 30 requests a minute, and answers them at `GET /github/mine`, `/github/reviews` and `/github/issues`, or 404 without its setting (decision 204). Done (#176).
- `homelab-github`: the laptop runs the service, with its token in a Secret, before the gateway requires it.
- `github-gateway`: the gateway forwards `/api/github` to the service.
- `shared-polled-section`: the PRs page's lists and the Todos page's Jira tickets show a poller's list through one component in the shell, for the GitHub page's lists; nothing changes on screen (decision 205). Done (#184).
- `shared-page-status`: the PRs, Time, Nudge and Todos pages draw the line that takes their sections' place from one global class, for the GitHub page's (decision 206). Done (#187).
- `github-page`: the lists in the app, where the user chooses: a page of their own at `/github`, or beside Bitbucket's on the PRs page.
- `github-attention`: what the user chooses of the lists reaches the Home's "Needs you", over the attention contract.

Expand Down Expand Up @@ -1765,6 +1769,12 @@ Decisions 200 to 299 are the `github-widgets` session's.
- **Biome** doesn't read the `generics` attribute (biomejs/biome#11215, §10), so the attribute names `Polls` with `import()` rather than through an import, which Biome would take for unused and which would need a suppression.
- **`github-widgets` has eight steps** (decision 200): this one comes sixth, before `github-page`. It serves both of the user's answers on where the lists live.
- **Rejected**: a third copy for the GitHub page; global classes for the card, as for the list rows (decision 123), which would leave the markup and the texts copied; one key for every snapshot's list, which would change three services' answers; the Time page's failure line, which sits over the page's sections rather than in one.
206. **A page's status line, shared** (`shared-page-status`): the lead's choices (2026-10-30), for the user to review. The PRs page's "Bitbucket isn't set up.", the Time page's "Jira isn't set up." and "Couldn't load the time report.", the Nudge page's "Couldn't load the questions." and the Todos page's "Couldn't load the todos." each drew the line that takes the page's sections' place from a copy of the same rules, and the GitHub page's "GitHub isn't set up." would have made a fifth, so `shared-page-status` moves them to one global class a step ahead of `github-page`, as decisions 112 and 205 did.
- **The class**: `.page-status`, in a stylesheet of its own, `src/styles/page.css`, which `main.ts` loads after the others: no margin, the page's padding at the sides, `--ink-muted`, a section's 64 px below it (48 on phones), and long words that break rather than widen the page (§7's status line). A class rather than a component, as the list rows' and the form's (decisions 112 and 123), since the line is a paragraph with nothing in it but each page's own text.
- **What changes**: on phones, the Nudge page's line now leaves 48 px below it, as a section does, rather than 64 px, since its copy lacked the phone rule. Only the Time page's copy broke long words; the others' texts are short, so nothing else changes on screen.
- **The Time page's failure line**, when the line sits over the page's sections, keeps its own smaller space below it, 30 px (24 on phones), as the Todos page's form leaves over its first section.
- **`github-widgets` has nine steps** (decision 200): this one comes seventh, before `github-page`.
- **Rejected**: a fifth copy for the GitHub page; a component, which would wrap a paragraph and its text for no gain; the polled sections' muted lines (decision 205) and the Notes pages' failure lines, which sit in a block that gives them their space: a section, under its head, or a Notes page's content, under its tools.

Decisions 300 to 399 are the `games-exophase` session's.

Expand Down
Loading