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
63 changes: 3 additions & 60 deletions apps/web/src/home/ThisWeek.svelte
Original file line number Diff line number Diff line change
@@ -1,69 +1,12 @@
<script lang="ts">
import { appHref } from '../shell/apps';
import Section from '../shell/Section.svelte';
import type { Week } from './api';
import { hoursLabel, weekPercent } from './week';
import WeekCard from '../time/WeekCard.svelte';
import type { Week } from '../time/week';

let { week }: { week: Week } = $props();
</script>

<Section title="This" accent="week" link={{ href: appHref('time'), label: 'Day report' }}>
<div class="card">
<p class="hours">
<span class="value">{hoursLabel(week)}</span>
<span class="unit">Logged on Jira</span>
</p>
<!-- The hours above say the same in words. The % sits inside the braces, so Svelte adds no `?? ''` fallback. -->
<div class="track" aria-hidden="true">
<div class="fill" style:width={`${weekPercent(week)}%`}></div>
</div>
</div>
<WeekCard {week} />
</Section>

<style>
.card {
padding: 20px;
border: 1px solid var(--rule);
background: var(--surface);
box-shadow: var(--offset) var(--offset) 0 var(--shadow);
}

.hours {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0 8px;
margin: 0;
}

.value {
font-size: 28px;
font-weight: 500;
letter-spacing: -0.03em;
line-height: 1.2;
}

.unit {
color: var(--ink-muted);
font: 400 11px / 1.6 var(--font-mono);
letter-spacing: 0.045em;
text-transform: uppercase;
}

.track {
height: 8px;
margin-top: 16px;
background: var(--fill);
}

.fill {
height: 100%;
background: var(--ink);
}

@media (max-width: 700px) {
.value {
font-size: 24px;
}
}
</style>
6 changes: 2 additions & 4 deletions apps/web/src/home/ThisWeek.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { expect, test } from 'vitest';

import ThisWeek from './ThisWeek.svelte';

test('shows the hours logged against the target, and links to the day report', () => {
const { container } = render(ThisWeek, { props: { week: { loggedHours: 30, targetHours: 40 } } });
test("shows the week's card under its head, which links to the day report", () => {
render(ThisWeek, { props: { week: { loggedHours: 30, targetHours: 40 } } });

expect(screen.getByRole('heading', { level: 2, name: 'This week' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Day report' })).toHaveAttribute('href', '/time');
expect(screen.getByText('30h / 40h')).toBeInTheDocument();
expect(screen.getByText('Logged on Jira')).toBeInTheDocument();
expect(container.querySelector('.fill')).toHaveStyle({ width: '75%' });
});
8 changes: 2 additions & 6 deletions apps/web/src/home/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Week } from '../time/week';

/** The apps that have items so far, as home names them. */
type App = 'github' | 'planner' | 'pulls' | 'time' | 'todos';

Expand All @@ -21,12 +23,6 @@ interface SourceStatus {
state: 'ok' | 'unavailable';
}

/** The hours logged this week, against the weekly target. */
export interface Week {
loggedHours: number;
targetHours: number;
}

/** The Home screen's data from GET /api/home, with only the fields the app reads so far. */
export interface Home {
name: string;
Expand Down
64 changes: 64 additions & 0 deletions apps/web/src/time/WeekCard.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import { hoursLabel, type Week, weekPercent } from './week';

let { week }: { week: Week } = $props();
</script>

<div class="card">
<p class="hours">
<span class="value">{hoursLabel(week)}</span>
<span class="unit">Logged on Jira</span>
</p>
<!-- The hours above say the same in words. The % sits inside the braces, so Svelte adds no `?? ''` fallback. -->
<div class="track" aria-hidden="true">
<div class="fill" style:width={`${weekPercent(week)}%`}></div>
</div>
</div>

<style>
.card {
padding: 20px;
border: 1px solid var(--rule);
background: var(--surface);
box-shadow: var(--offset) var(--offset) 0 var(--shadow);
}

.hours {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0 8px;
margin: 0;
}

.value {
font-size: 28px;
font-weight: 500;
letter-spacing: -0.03em;
line-height: 1.2;
}

.unit {
color: var(--ink-muted);
font: 400 11px / 1.6 var(--font-mono);
letter-spacing: 0.045em;
text-transform: uppercase;
}

.track {
height: 8px;
margin-top: 16px;
background: var(--fill);
}

.fill {
height: 100%;
background: var(--ink);
}

@media (max-width: 700px) {
.value {
font-size: 24px;
}
}
</style>
13 changes: 13 additions & 0 deletions apps/web/src/time/WeekCard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { render, screen } from '@testing-library/svelte';
import { expect, test } from 'vitest';

import WeekCard from './WeekCard.svelte';

test('shows the hours logged against the target, and fills the bar as far', () => {
const { container } = render(WeekCard, { props: { week: { loggedHours: 30, targetHours: 40 } } });

expect(screen.getByText('30h / 40h')).toBeInTheDocument();
expect(screen.getByText('Logged on Jira')).toBeInTheDocument();
expect(container.querySelector('.track')).toHaveAttribute('aria-hidden', 'true');
expect(container.querySelector('.fill')).toHaveStyle({ width: '75%' });
});
File renamed without changes.
6 changes: 5 additions & 1 deletion apps/web/src/home/week.ts → apps/web/src/time/week.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { Week } from './api';
/** The hours logged this week, against the weekly target. */
export interface Week {
loggedHours: number;
targetHours: number;
}

const minutesPerHour = 60;
const full = 100;
Expand Down
5 changes: 4 additions & 1 deletion docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ The v1.2 design in §4 to §6 stays as reference. A piece of it is built only wh
| `time-report` | A package of the new `time` service, `report`, works out the time report from my worklogs: the hours I logged on each of the last 7 days, the weekdays before today with nothing logged marked as missing, and this week's hours against its 40-hour target. The third of `time-tracking`'s twelve steps (decision 181) | Done (#149) |
| `time-service` | A new `time` service polls Jira for my worklogs of the last 7 days as it starts and every 5 minutes after, and answers the time report at `GET /time/report`, with when the latest poll to succeed began and why the latest poll failed, if it did. It reads todos' Jira settings and token: without them it answers 404, and with only some it doesn't start. `mise run up` runs it on port 8088 and the Gitea workflow builds its image; the gateway reaches it with `time-gateway`. The fourth of `time-tracking`'s twelve steps (decision 182) | Done (#150) |
| `time-gateway` | The gateway forwards `/api/time` to the `time` service, behind both of its locks, so the app reaches my time report at `GET /api/time/report`. `time` is one of the gateway's peers, each of which `BAKHYARD_PEERS` must name, and `mise run up` starts the gateway once time is ready. The sixth of `time-tracking`'s twelve steps (decision 181) | Done (#151) |
| `week-card` | The Home's "This week" card, the hours logged against the target over a bar, moves with its formatting and the week's type to the Time app's own folder, `src/time`, as a component of its own, `WeekCard`, ahead of `time-page`, which shows it too; nothing changes on screen or in behaviour. The seventh of `time-tracking`'s thirteen steps (decision 183) | Done (#152) |
| `nudge-merge` | Nudge, the user's phone-questions tool for agents (`codedogapp/nudge`), moves into Bakhyard: one app, not two (decision 157). The nudge session does the work, one small PR per step, from its plan, [`bakhyard-merge.md`](https://github.com/codedogapp/nudge/blob/main/docs/bakhyard-merge.md); this session doesn't stop its own steps for it | In review |

After these: the product phases (§8.2), each split into steps.
Expand Down Expand Up @@ -1195,13 +1196,14 @@ Every item is pending unless it says done. The service an item lands in is in br
- `pulls-reminders`: at 10:00 and 15:00 on weekdays, pulls pushes one reminder that lists my PRs under their required approvals (decision 177). Done (#144).
- `homelab-pulls-notify`: done (codedogapp/homelab#40). In the homelab repo, pulls' `BAKHYARD_PEERS` names notify, `TZ` gives it the user's zone, and a NetworkPolicy lets pulls reach notify.
- Later, with a database for pulls: snooze (2 h / tomorrow / until updated) and mark as nudged, from the PRs page.
- `time-tracking` [time]. Needs `todos-jira`. Reads and writes standard Jira worklogs (§2, decision 46), with no timers: Clockwork already has them inside Jira. Twelve steps (decisions 180 and 181):
- `time-tracking` [time]. Needs `todos-jira`. Reads and writes standard Jira worklogs (§2, decision 46), with no timers: Clockwork already has them inside Jira. Thirteen steps (decisions 180, 181 and 183):
- `shared-jira`: todos' Jira client moves to the root `internal/jira`, with the code that reads its settings and token, for the `time` service; todos' poller of my tickets moves to a package of its own, `tickets`. Nothing changes in behaviour. Done (#147).
- `jira-worklogs`: the client reads my worklogs in a span of days: my account from `/myself`, the tickets I logged on from a `worklogAuthor` search, and each ticket's worklogs. Done (#148).
- `time-report`: a package of the `time` service, `report`, works out the time report from my worklogs: the hours I logged on each of the last 7 days, with every weekday before today that has none marked as missing, plus this week's total against the target. Done (#149).
- `time-service`: a new `time` service polls the last 7 days' worklogs every 5 minutes and answers the time report at `GET /time/report` (decision 182). Done (#150).
- `homelab-time`: done (codedogapp/homelab#42). In the homelab repo, `time` runs on the laptop with todos' Jira settings and token, and the gateway's and home's peers name it.
- `time-gateway`: the gateway forwards `/api/time` to `time`. Done (#151).
- `week-card`: the Home's week card, its formatting and the week's type move to `src/time`, for the Time page to share; nothing changes on screen (decision 183). Done (#152).
- `time-page`: the Time page shows the time report.
- `time-week`: `time` answers `TimeService/GetWeek` (§4.10).
- `home-week`: home asks `time` for the week, and the Home's "This week" shows it, or "Hours unavailable" when `time` doesn't answer (§7).
Expand Down Expand Up @@ -1554,6 +1556,7 @@ Made on 2026-10-23:
- **The zone**: days follow `time.Local`, the Mac's zone or the container's `TZ` (decision 180). The distroless static image holds tzdata (its README lists it), so `TZ` works without importing `time/tzdata`, which would add about 450 KB to the binary. Without `TZ` a container keeps UTC, so `homelab-time` sets it, as `homelab-pulls-notify` did for pulls.
- **The image**: `services/time/Dockerfile` builds as pulls' does, and the Gitea workflow builds and pushes `time` with the other services.
- **Rejected**: the poller inside `report`, which would bring the clock and Jira into it; a Refresh that polls on demand; embedding `time/tzdata`; `time.Now()` read twice, once for the span and once for the report, which could disagree across midnight.
183. **The Time page in two steps**: the lead's choices (2026-10-29), for the user to review. The Time page shows this week's hours against the target on the card that the Home's "This week" shows, so the two share it. The card sat inside the Home's `ThisWeek`, under the section head whose "Day report" link leads to the Time page, with its formatting and the `Week` type in the Home's folder. So `time-page` splits in two and `time-tracking` has thirteen steps (§8.2): `week-card`, a pure refactor, moves the card into a component of its own, `WeekCard`, and moves it, `week.ts` and the `Week` type to the Time app's folder, `src/time`, as `shell-section` (#85) moved the section head ahead of `todos-groups`; `time-page` then builds the page. The Home takes the week's card from the Time app, as home will take the week from the time service (`home-week`). Rejected: the Time page importing the Home's files, which would make an app depend on the Home, as `MorePage` does on its app cards; a copy of the card.

## 10. Risks

Expand Down
Loading