fix: update local greetings and stabilize account navigation

This commit is contained in:
2026-10-08 01:10:49 +09:00
parent 67afc7bf15
commit c66b427a23
15 changed files with 164 additions and 254 deletions
+21 -91
View File
@@ -19,9 +19,7 @@
"react-perf", "react-perf",
"jsx-a11y" "jsx-a11y"
], ],
"jsPlugins": [ "jsPlugins": ["@shadcn/lint"],
"@shadcn/lint"
],
"rules": { "rules": {
"no-console": "error", "no-console": "error",
"no-unused-vars": [ "no-unused-vars": [
@@ -50,9 +48,7 @@
"shadcn/no-arbitrary-values": [ "shadcn/no-arbitrary-values": [
"error", "error",
{ {
"allow": [ "allow": ["layout"]
"layout"
]
} }
], ],
"shadcn/no-inline-styles": "error", "shadcn/no-inline-styles": "error",
@@ -60,114 +56,59 @@
"shadcn/no-restyle": [ "shadcn/no-restyle": [
"error", "error",
{ {
"allow": [ "allow": ["layout"],
"layout"
],
"contracts": [ "contracts": [
{ {
"pattern": "^Sidebar$", "pattern": "^Sidebar$",
"allow": [ "allow": ["layout", "border-r", "border-r-0", "border-border", "transition-none"]
"layout",
"border-r",
"border-r-0",
"border-border",
"transition-none"
]
}, },
{ {
"pattern": "^SidebarHeader$", "pattern": "^SidebarHeader$",
"allow": [ "allow": ["layout", "py-4", "gap-4", "border-b", "p-5"]
"layout",
"py-4",
"gap-4",
"border-b",
"p-5"
]
}, },
{ {
"pattern": "^SidebarGroup$", "pattern": "^SidebarGroup$",
"allow": [ "allow": ["layout", "px-3"]
"layout",
"px-3"
]
}, },
{ {
"pattern": "^SidebarFooter$", "pattern": "^SidebarFooter$",
"allow": [ "allow": ["layout", "px-3", "pb-4"]
"layout",
"px-3",
"pb-4"
]
}, },
{ {
"pattern": "^SidebarMenuButton$", "pattern": "^SidebarMenuButton$",
"allow": [ "allow": ["layout", "p-0"]
"layout",
"p-0"
]
}, },
{ {
"pattern": "^Label$", "pattern": "^Label$",
"allow": [ "allow": ["layout", "gap-2", "text-sm", "text-xs", "text-muted-foreground"]
"layout",
"gap-2",
"text-sm",
"text-xs",
"text-muted-foreground"
]
}, },
{ {
"pattern": "^MessageScrollerContent$", "pattern": "^MessageScrollerContent$",
"allow": [ "allow": ["layout", "gap-5", "p-4"]
"layout",
"gap-5",
"p-4"
]
}, },
{ {
"pattern": "^(Marker|Collapsible)$", "pattern": "^(Marker|Collapsible)$",
"allow": [ "allow": ["layout", "text-research-label", "text-muted-foreground"]
"layout",
"text-research-label",
"text-muted-foreground"
]
}, },
{ {
"pattern": "^SidebarProvider$", "pattern": "^SidebarProvider$",
"allow": [ "allow": ["layout", "bg-vital-bg", "text-vital-text", "font-arial"]
"layout",
"bg-vital-bg",
"text-vital-text",
"font-arial"
]
}, },
{ {
"pattern": "^Button$", "pattern": "^Button$",
"allow": [ "allow": ["layout", "px-2"]
"layout",
"px-2"
]
}, },
{ {
"pattern": "^Input$", "pattern": "^Input$",
"allow": [ "allow": ["layout", "pl-9"]
"layout",
"pl-9"
]
}, },
{ {
"pattern": "^SidebarContent$", "pattern": "^SidebarContent$",
"allow": [ "allow": ["layout", "gap-0"]
"layout",
"gap-0"
]
}, },
{ {
"pattern": "^Badge$", "pattern": "^Badge$",
"allow": [ "allow": ["layout", "gap-1"]
"layout",
"gap-1"
]
} }
] ]
} }
@@ -177,17 +118,13 @@
}, },
"overrides": [ "overrides": [
{ {
"files": [ "files": ["scripts/**"],
"scripts/**"
],
"rules": { "rules": {
"no-console": "off" "no-console": "off"
} }
}, },
{ {
"files": [ "files": ["src/components/ui/**"],
"src/components/ui/**"
],
"rules": { "rules": {
"shadcn/no-arbitrary-values": "off", "shadcn/no-arbitrary-values": "off",
"shadcn/no-restyle": "off", "shadcn/no-restyle": "off",
@@ -195,13 +132,8 @@
} }
}, },
{ {
"files": [ "files": ["src/**/*.{test,spec}.{ts,tsx}", "tests/**/*.{ts,tsx}"],
"src/**/*.{test,spec}.{ts,tsx}", "plugins": ["vitest"],
"tests/**/*.{ts,tsx}"
],
"plugins": [
"vitest"
],
"rules": { "rules": {
"typescript/unbound-method": "error", "typescript/unbound-method": "error",
"vitest/expect-expect": "error", "vitest/expect-expect": "error",
@@ -211,9 +143,7 @@
} }
}, },
{ {
"files": [ "files": ["e2e/**/*.{ts,tsx}"],
"e2e/**/*.{ts,tsx}"
],
"rules": { "rules": {
"react/rules-of-hooks": "off" "react/rules-of-hooks": "off"
} }
+4
View File
@@ -16,6 +16,10 @@ SQLite records across Home, Messages, and MCP. User edits and decisions survive
agent updates and reloads. Routines and Journal remain session-only prototypes; agent updates and reloads. Routines and Journal remain session-only prototypes;
Beeper history synchronization and actual message sending remain unconnected. Beeper history synchronization and actual message sending remain unconnected.
Home's greeting follows the device's local time: morning from 05:00, afternoon
from 12:00, and evening from 18:00 until 05:00. It refreshes while the page is
open and when returning to the app.
## Workspaces and current status ## Workspaces and current status
| Workspace | Route | Current implementation | | Workspace | Route | Current implementation |
+19 -64
View File
@@ -151,12 +151,8 @@
"name": "auth_sessions_owner_id_workspace_owner_id_fk", "name": "auth_sessions_owner_id_workspace_owner_id_fk",
"tableFrom": "auth_sessions", "tableFrom": "auth_sessions",
"tableTo": "workspace_owner", "tableTo": "workspace_owner",
"columnsFrom": [ "columnsFrom": ["owner_id"],
"owner_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -239,12 +235,8 @@
"name": "connection_credentials_connection_id_connections_id_fk", "name": "connection_credentials_connection_id_connections_id_fk",
"tableFrom": "connection_credentials", "tableFrom": "connection_credentials",
"tableTo": "connections", "tableTo": "connections",
"columnsFrom": [ "columnsFrom": ["connection_id"],
"connection_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@@ -252,12 +244,8 @@
"name": "connection_credentials_app_id_oauth_apps_id_fk", "name": "connection_credentials_app_id_oauth_apps_id_fk",
"tableFrom": "connection_credentials", "tableFrom": "connection_credentials",
"tableTo": "oauth_apps", "tableTo": "oauth_apps",
"columnsFrom": [ "columnsFrom": ["app_id"],
"app_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "restrict", "onDelete": "restrict",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -336,19 +324,12 @@
"indexes": { "indexes": {
"connections_account": { "connections_account": {
"name": "connections_account", "name": "connections_account",
"columns": [ "columns": ["platform", "origin", "account_id"],
"platform",
"origin",
"account_id"
],
"isUnique": true "isUnique": true
}, },
"connections_relay_profile": { "connections_relay_profile": {
"name": "connections_relay_profile", "name": "connections_relay_profile",
"columns": [ "columns": ["origin", "relay_profile"],
"origin",
"relay_profile"
],
"isUnique": true "isUnique": true
} }
}, },
@@ -406,10 +387,7 @@
"indexes": { "indexes": {
"deck_columns_position": { "deck_columns_position": {
"name": "deck_columns_position", "name": "deck_columns_position",
"columns": [ "columns": ["deck_id", "position"],
"deck_id",
"position"
],
"isUnique": true "isUnique": true
} }
}, },
@@ -418,12 +396,8 @@
"name": "deck_columns_deck_id_decks_id_fk", "name": "deck_columns_deck_id_decks_id_fk",
"tableFrom": "deck_columns", "tableFrom": "deck_columns",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@@ -431,22 +405,15 @@
"name": "deck_columns_connection_id_connections_id_fk", "name": "deck_columns_connection_id_connections_id_fk",
"tableFrom": "deck_columns", "tableFrom": "deck_columns",
"tableTo": "connections", "tableTo": "connections",
"columnsFrom": [ "columnsFrom": ["connection_id"],
"connection_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "restrict", "onDelete": "restrict",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_columns_deck_id_id_pk": { "deck_columns_deck_id_id_pk": {
"columns": [ "columns": ["deck_id", "id"],
"deck_id",
"id"
],
"name": "deck_columns_deck_id_id_pk" "name": "deck_columns_deck_id_id_pk"
} }
}, },
@@ -603,11 +570,7 @@
"indexes": { "indexes": {
"oauth_apps_configuration": { "oauth_apps_configuration": {
"name": "oauth_apps_configuration", "name": "oauth_apps_configuration",
"columns": [ "columns": ["origin", "redirect_uri", "scopes"],
"origin",
"redirect_uri",
"scopes"
],
"isUnique": true "isUnique": true
} }
}, },
@@ -675,12 +638,8 @@
"name": "oauth_attempts_app_id_oauth_apps_id_fk", "name": "oauth_attempts_app_id_oauth_apps_id_fk",
"tableFrom": "oauth_attempts", "tableFrom": "oauth_attempts",
"tableTo": "oauth_apps", "tableTo": "oauth_apps",
"columnsFrom": [ "columnsFrom": ["app_id"],
"app_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@@ -688,12 +647,8 @@
"name": "oauth_attempts_connection_id_connections_id_fk", "name": "oauth_attempts_connection_id_connections_id_fk",
"tableFrom": "oauth_attempts", "tableFrom": "oauth_attempts",
"tableTo": "connections", "tableTo": "connections",
"columnsFrom": [ "columnsFrom": ["connection_id"],
"connection_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
+25 -86
View File
@@ -151,12 +151,8 @@
"name": "auth_sessions_owner_id_workspace_owner_id_fk", "name": "auth_sessions_owner_id_workspace_owner_id_fk",
"tableFrom": "auth_sessions", "tableFrom": "auth_sessions",
"tableTo": "workspace_owner", "tableTo": "workspace_owner",
"columnsFrom": [ "columnsFrom": ["owner_id"],
"owner_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -251,10 +247,7 @@
"foreignKeys": {}, "foreignKeys": {},
"compositePrimaryKeys": { "compositePrimaryKeys": {
"beeper_chat_checkpoints_target_id_chat_id_pk": { "beeper_chat_checkpoints_target_id_chat_id_pk": {
"columns": [ "columns": ["target_id", "chat_id"],
"target_id",
"chat_id"
],
"name": "beeper_chat_checkpoints_target_id_chat_id_pk" "name": "beeper_chat_checkpoints_target_id_chat_id_pk"
} }
}, },
@@ -297,11 +290,7 @@
"foreignKeys": {}, "foreignKeys": {},
"compositePrimaryKeys": { "compositePrimaryKeys": {
"beeper_seen_messages_target_id_chat_id_message_id_pk": { "beeper_seen_messages_target_id_chat_id_message_id_pk": {
"columns": [ "columns": ["target_id", "chat_id", "message_id"],
"target_id",
"chat_id",
"message_id"
],
"name": "beeper_seen_messages_target_id_chat_id_message_id_pk" "name": "beeper_seen_messages_target_id_chat_id_message_id_pk"
} }
}, },
@@ -346,12 +335,8 @@
"name": "connection_credentials_connection_id_connections_id_fk", "name": "connection_credentials_connection_id_connections_id_fk",
"tableFrom": "connection_credentials", "tableFrom": "connection_credentials",
"tableTo": "connections", "tableTo": "connections",
"columnsFrom": [ "columnsFrom": ["connection_id"],
"connection_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@@ -359,12 +344,8 @@
"name": "connection_credentials_app_id_oauth_apps_id_fk", "name": "connection_credentials_app_id_oauth_apps_id_fk",
"tableFrom": "connection_credentials", "tableFrom": "connection_credentials",
"tableTo": "oauth_apps", "tableTo": "oauth_apps",
"columnsFrom": [ "columnsFrom": ["app_id"],
"app_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "restrict", "onDelete": "restrict",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -443,19 +424,12 @@
"indexes": { "indexes": {
"connections_account": { "connections_account": {
"name": "connections_account", "name": "connections_account",
"columns": [ "columns": ["platform", "origin", "account_id"],
"platform",
"origin",
"account_id"
],
"isUnique": true "isUnique": true
}, },
"connections_relay_profile": { "connections_relay_profile": {
"name": "connections_relay_profile", "name": "connections_relay_profile",
"columns": [ "columns": ["origin", "relay_profile"],
"origin",
"relay_profile"
],
"isUnique": true "isUnique": true
} }
}, },
@@ -513,10 +487,7 @@
"indexes": { "indexes": {
"deck_columns_position": { "deck_columns_position": {
"name": "deck_columns_position", "name": "deck_columns_position",
"columns": [ "columns": ["deck_id", "position"],
"deck_id",
"position"
],
"isUnique": true "isUnique": true
} }
}, },
@@ -525,12 +496,8 @@
"name": "deck_columns_deck_id_decks_id_fk", "name": "deck_columns_deck_id_decks_id_fk",
"tableFrom": "deck_columns", "tableFrom": "deck_columns",
"tableTo": "decks", "tableTo": "decks",
"columnsFrom": [ "columnsFrom": ["deck_id"],
"deck_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@@ -538,22 +505,15 @@
"name": "deck_columns_connection_id_connections_id_fk", "name": "deck_columns_connection_id_connections_id_fk",
"tableFrom": "deck_columns", "tableFrom": "deck_columns",
"tableTo": "connections", "tableTo": "connections",
"columnsFrom": [ "columnsFrom": ["connection_id"],
"connection_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "restrict", "onDelete": "restrict",
"onUpdate": "no action" "onUpdate": "no action"
} }
}, },
"compositePrimaryKeys": { "compositePrimaryKeys": {
"deck_columns_deck_id_id_pk": { "deck_columns_deck_id_id_pk": {
"columns": [ "columns": ["deck_id", "id"],
"deck_id",
"id"
],
"name": "deck_columns_deck_id_id_pk" "name": "deck_columns_deck_id_id_pk"
} }
}, },
@@ -732,17 +692,12 @@
"indexes": { "indexes": {
"mcp_event_outbox_identity": { "mcp_event_outbox_identity": {
"name": "mcp_event_outbox_identity", "name": "mcp_event_outbox_identity",
"columns": [ "columns": ["subscription_id", "event_id"],
"subscription_id",
"event_id"
],
"isUnique": true "isUnique": true
}, },
"mcp_event_outbox_due": { "mcp_event_outbox_due": {
"name": "mcp_event_outbox_due", "name": "mcp_event_outbox_due",
"columns": [ "columns": ["next_attempt_at"],
"next_attempt_at"
],
"isUnique": false "isUnique": false
} }
}, },
@@ -751,12 +706,8 @@
"name": "mcp_event_outbox_subscription_id_mcp_event_subscriptions_id_fk", "name": "mcp_event_outbox_subscription_id_mcp_event_subscriptions_id_fk",
"tableFrom": "mcp_event_outbox", "tableFrom": "mcp_event_outbox",
"tableTo": "mcp_event_subscriptions", "tableTo": "mcp_event_subscriptions",
"columnsFrom": [ "columnsFrom": ["subscription_id"],
"subscription_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
@@ -915,11 +866,7 @@
"indexes": { "indexes": {
"oauth_apps_configuration": { "oauth_apps_configuration": {
"name": "oauth_apps_configuration", "name": "oauth_apps_configuration",
"columns": [ "columns": ["origin", "redirect_uri", "scopes"],
"origin",
"redirect_uri",
"scopes"
],
"isUnique": true "isUnique": true
} }
}, },
@@ -987,12 +934,8 @@
"name": "oauth_attempts_app_id_oauth_apps_id_fk", "name": "oauth_attempts_app_id_oauth_apps_id_fk",
"tableFrom": "oauth_attempts", "tableFrom": "oauth_attempts",
"tableTo": "oauth_apps", "tableTo": "oauth_apps",
"columnsFrom": [ "columnsFrom": ["app_id"],
"app_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
}, },
@@ -1000,12 +943,8 @@
"name": "oauth_attempts_connection_id_connections_id_fk", "name": "oauth_attempts_connection_id_connections_id_fk",
"tableFrom": "oauth_attempts", "tableFrom": "oauth_attempts",
"tableTo": "connections", "tableTo": "connections",
"columnsFrom": [ "columnsFrom": ["connection_id"],
"connection_id" "columnsTo": ["id"],
],
"columnsTo": [
"id"
],
"onDelete": "cascade", "onDelete": "cascade",
"onUpdate": "no action" "onUpdate": "no action"
} }
+1
View File
@@ -39,6 +39,7 @@ test("recovers from empty search and shares navigation with Research", async ({
await expect(readerPage.editInterestsButton).toHaveCount(0); await expect(readerPage.editInterestsButton).toHaveCount(0);
await workspacePage.navigateTo("Research"); await workspacePage.navigateTo("Research");
await expect(page).toHaveURL(/\/deck$/); await expect(page).toHaveURL(/\/deck$/);
await expect(workspacePage.researchReady).toBeEnabled();
await workspacePage.navigateTo("Reader"); await workspacePage.navigateTo("Reader");
await expect(page).toHaveURL(/\/inbox$/); await expect(page).toHaveURL(/\/inbox$/);
await expect(readerPage.rows).toHaveCount(9); await expect(readerPage.rows).toHaveCount(9);
+1 -1
View File
@@ -32,7 +32,7 @@ export class HomePage {
return this.page.getByRole("button", { name: `Record weight Recorded · ${weight} kg` }); return this.page.getByRole("button", { name: `Record weight Recorded · ${weight} kg` });
} }
greeting(name: string) { greeting(name: string) {
return this.page.getByRole("heading", { name: `Good morning, ${name}` }); return this.page.getByRole("heading", { level: 1 }).filter({ hasText: `, ${name}` });
} }
async goTo(search = "") { async goTo(search = "") {
await this.page.goto(`/${search}`); await this.page.goto(`/${search}`);
+7 -1
View File
@@ -1,6 +1,12 @@
import type { KnipConfig } from "knip"; import type { KnipConfig } from "knip";
export default { export default {
entry: ["playwright.config.ts", "e2e/**/*.test.ts", "e2e/mock-relay.mjs"], entry: [
"playwright.config.ts",
"e2e/**/*.{test,spec}.ts",
"e2e/mock-relay.mjs",
"public/sw.js",
"scripts/events-worker.ts",
],
playwright: false, playwright: false,
} satisfies KnipConfig; } satisfies KnipConfig;
+5 -2
View File
@@ -1,4 +1,4 @@
import { Link, useLocation, useRouteContext } from "@tanstack/react-router"; import { Link, useHydrated, useLocation, useRouteContext } from "@tanstack/react-router";
import { Activity, House, Inbox, Layers3, MessagesSquare, NotebookPen, Search } from "lucide-react"; import { Activity, House, Inbox, Layers3, MessagesSquare, NotebookPen, Search } from "lucide-react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Avatar, AvatarFallback } from "#/components/ui/avatar"; import { Avatar, AvatarFallback } from "#/components/ui/avatar";
@@ -29,6 +29,7 @@ export function WorkspaceNavigation({
hasTemporaryDecks, hasTemporaryDecks,
}: WorkspaceNavigationProps) { }: WorkspaceNavigationProps) {
const { auth } = useRouteContext({ from: "__root__" }); const { auth } = useRouteContext({ from: "__root__" });
const hydrated = useHydrated();
const [search, updateSearch] = useUrlState(); const [search, updateSearch] = useUrlState();
const managingAccounts = search.accounts; const managingAccounts = search.accounts;
const setManagingAccounts = (open: boolean) => updateSearch({ accounts: open || undefined }); const setManagingAccounts = (open: boolean) => updateSearch({ accounts: open || undefined });
@@ -116,7 +117,9 @@ export function WorkspaceNavigation({
<SidebarMenu className="mt-2"> <SidebarMenu className="mt-2">
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton <SidebarMenuButton
aria-label="Manage connected accounts" render={
<button type="button" aria-label="Manage connected accounts" disabled={!hydrated} />
}
tooltip={{ children: "Manage connected accounts", hidden: false }} tooltip={{ children: "Manage connected accounts", hidden: false }}
className="size-10 justify-center p-0" className="size-10 justify-center p-0"
onClick={() => setManagingAccounts(true)} onClick={() => setManagingAccounts(true)}
+3 -3
View File
@@ -9,7 +9,7 @@ const required = (max: number) =>
.max(max) .max(max)
.refine((value) => value.trim().length > 0); .refine((value) => value.trim().length > 0);
const section = z.enum(["focus", "optional"]); const section = z.enum(["focus", "optional"]);
export const activityState = z.enum([ const activityState = z.enum([
"available", "available",
"inProgress", "inProgress",
"waiting", "waiting",
@@ -48,7 +48,7 @@ const base = {
recommendation: z.strictObject({ section, reason: z.string().max(2000).optional() }).optional(), recommendation: z.strictObject({ section, reason: z.string().max(2000).optional() }).optional(),
sources: z.array(z.discriminatedUnion("kind", [messageSource, lifeSource])).max(20), sources: z.array(z.discriminatedUnion("kind", [messageSource, lifeSource])).max(20),
}; };
export const activityProposal = z const activityProposal = z
.discriminatedUnion("kind", [ .discriminatedUnion("kind", [
z.strictObject({ ...base, kind: z.literal("task") }), z.strictObject({ ...base, kind: z.literal("task") }),
z.strictObject({ z.strictObject({
@@ -89,7 +89,7 @@ export const publishActivitiesInput = z
new Set(value.activities.map((activity) => activity.id)).size === value.activities.length, new Set(value.activities.map((activity) => activity.id)).size === value.activities.length,
"Activity IDs must be unique.", "Activity IDs must be unique.",
); );
export const activityOverrides = z.strictObject({ const activityOverrides = z.strictObject({
title: required(300).optional(), title: required(300).optional(),
nextAction: required(2000).optional(), nextAction: required(2000).optional(),
detail: z.string().max(8000).optional(), detail: z.string().max(8000).optional(),
+43
View File
@@ -0,0 +1,43 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { renderToString } from "react-dom/server";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { Greeting } from "./greeting";
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it.each([
[0, "Good evening"],
[4, "Good evening"],
[5, "Good morning"],
[11, "Good morning"],
[12, "Good afternoon"],
[17, "Good afternoon"],
[18, "Good evening"],
[23, "Good evening"],
])("greets using the device's local hour %i", (hour, greeting) => {
vi.setSystemTime(new Date(2026, 9, 8, hour));
render(<Greeting name="Yuta" />);
expect(screen.getByText(`${greeting}, Yuta`)).toBeVisible();
});
it("updates when an open page crosses noon", async () => {
vi.setSystemTime(new Date(2026, 9, 8, 11, 59, 50));
render(<Greeting name="Yuta" />);
expect(screen.getByText("Good morning, Yuta")).toBeVisible();
await act(() => vi.advanceTimersByTime(30_000));
expect(screen.getByText("Good afternoon, Yuta")).toBeVisible();
});
it("refreshes after returning to a suspended page", () => {
vi.setSystemTime(new Date(2026, 9, 8, 10));
render(<Greeting name="Yuta" />);
vi.setSystemTime(new Date(2026, 9, 8, 20));
fireEvent(document, new Event("visibilitychange"));
expect(screen.getByText("Good evening, Yuta")).toBeVisible();
});
it("does not use the server's timezone in the initial HTML", () => {
vi.setSystemTime(new Date(2026, 9, 8, 10));
expect(renderToString(<Greeting name="Yuta" />)).toContain("Hello");
});
+28
View File
@@ -0,0 +1,28 @@
import { useEffect, useState } from "react";
function currentGreeting() {
const hour = new Date().getHours();
if (hour >= 5 && hour < 12) return "Good morning";
if (hour >= 12 && hour < 18) return "Good afternoon";
return "Good evening";
}
export function Greeting({ name }: { name: string | undefined }) {
// The server cannot know the device's timezone. Keep hydration consistent.
const [greeting, setGreeting] = useState("Hello");
useEffect(() => {
const update = () => setGreeting(currentGreeting());
update();
const interval = window.setInterval(update, 30_000);
document.addEventListener("visibilitychange", update);
return () => {
window.clearInterval(interval);
document.removeEventListener("visibilitychange", update);
};
}, []);
return (
<>
{greeting}, {name}
</>
);
}
+2 -1
View File
@@ -13,6 +13,7 @@ import { useActivities } from "#/features/activities/activity-state";
import { entryKindLabels, useWorkspaceState } from "#/features/workspace/workspace-state"; import { entryKindLabels, useWorkspaceState } from "#/features/workspace/workspace-state";
import { DailyBrief } from "./daily-brief"; import { DailyBrief } from "./daily-brief";
import { ExecutionWorkbench } from "./execution-workbench"; import { ExecutionWorkbench } from "./execution-workbench";
import { Greeting } from "./greeting";
import { type HomeAction, interpretAction } from "./interpret-action"; import { type HomeAction, interpretAction } from "./interpret-action";
export function HomePage() { export function HomePage() {
@@ -191,7 +192,7 @@ export function HomePage() {
<header className="mb-px-32"> <header className="mb-px-32">
<p className="text-sm text-muted-foreground">Today</p> <p className="text-sm text-muted-foreground">Today</p>
<h1 className="mt-px-12 text-home-greeting font-book tracking-home leading-normal max-md:text-ui-25"> <h1 className="mt-px-12 text-home-greeting font-book tracking-home leading-normal max-md:text-ui-25">
Good morning, {auth.owner?.name} <Greeting name={auth.owner?.name} />
</h1> </h1>
<DailyBrief onOpenContact={openContact} /> <DailyBrief onOpenContact={openContact} />
</header> </header>
+1 -1
View File
@@ -13,7 +13,7 @@ import {
type WebhookSender, type WebhookSender,
} from "./webhook.server"; } from "./webhook.server";
export const BEEPER_MESSAGE_EVENT = "beeper.message.received"; const BEEPER_MESSAGE_EVENT = "beeper.message.received";
// Explicit single-owner prototype trust boundary: callers can reach the private workspace MCP tunnel. // Explicit single-owner prototype trust boundary: callers can reach the private workspace MCP tunnel.
// This is a storage namespace, not an authenticated user identity. // This is a storage namespace, not an authenticated user identity.
const OWNER = "private-workspace"; const OWNER = "private-workspace";
+1 -1
View File
@@ -83,7 +83,7 @@ export function isPublicAddress(address: string) {
} }
return false; return false;
} }
export type WebhookResponse = { status: number; body: string }; type WebhookResponse = { status: number; body: string };
export type WebhookSender = ( export type WebhookSender = (
url: string, url: string,
body: string, body: string,