Skip to content

Commit 9b4e3a8

Browse files
committed
Format dates that can render on the server in one zone and record the new owners
Dates from server-prefetched reads no longer depend on the server's zone. Route entries share one name, Discard keeps legacy's Keep editing, a failed workspace list no longer reads as a dead deep link, and the contract, ownership map and parity ledger describe what now exists.
1 parent 76916e2 commit 9b4e3a8

20 files changed

Lines changed: 62 additions & 53 deletions

File tree

‎.agents/skills/add-settings-page/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Each grep lists candidates; review every match against the expected ones named b
6868
- Editable pages: confirm Save/Discard go through `saveDiscardActions()` and
6969
dirty is wired via `useSettingsUnsavedGuard` (called before early-return
7070
gates) — flag any hand-rolled Save button, `beforeunload`, or unsaved modal.
71-
`git grep -n "beforeunload" -- 'apps/sim/**/settings/**' 'apps/sim/ee/' 'apps/sim/components/settings/' ':(exclude)apps/sim/platform/**' ':(exclude,glob)**/*.test.*'`
71+
`git grep -n "beforeunload" -- 'apps/sim/**/settings/**' 'apps/sim/ee/' 'apps/sim/components/settings/' ':(exclude)apps/sim/platform/**' 'apps/sim/hooks/use-settings-browser-navigation.ts' ':(exclude,glob)**/*.test.*'`
7272
should only hit the centralized `use-settings-browser-navigation.ts`.
7373
5. Fix each finding with the smallest structural change that satisfies the checklist;
7474
do not touch handlers, state, queries, or gate returns. A pixel-size fix swaps

‎apps/sim/platform/AGENTS.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Use `ActivityRow` for a first-line glyph or avatar, title/detail and centered tr
3131

3232
Measure the actual scroll viewport with `ScrollArea`, or attach `useScrollEdges` directly to a native or Base UI scroll owner. Use the shared 16px mask only on edges with more content, updating after scroll, resize, input, reset or filtering. Platform and Settings navigation hide the native scrollbar and keep their edge fades, keyboard, wheel and touch scrolling. Other scroll areas keep their existing scrollbar behavior. Insets establish resting layout; they cannot substitute for an edge fade. Home widgets and notification panels use `--ui-widget-max-size` as a ceiling, not a forced height. Keep the dashboard free to scroll and let each overflowing list or schedule scroll independently. Plain tooltips preview at most three lines; retain the complete accessible text. A structured people popover owns its content and uses the dropdown’s popup padding and row insets.
3333

34-
Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag.
34+
Form dialogs use `Modal` or `MultiStepModal`. Pass a title, content, action labels and handlers. A confirm handler that returns a promise holds the dialog open as pending, closes it on success, and keeps the draft with an inline message on failure; nothing dismisses it while pending. Returning `false` keeps it open at the dialog's next stage, and `cancelLabel`/`onCancel` turn the quiet footer action into a step back. The shared borderless surface positions its title at the logical start and supplies one quiet Cancel/Back. Command search and previews use the same shadow and dismissal layer. Form controls cannot construct or style a form header, footer or Cancel button. Opening a dialog or advancing its step focuses the first available field. Confirmation dialogs focus Cancel, and read-only previews focus Close. A nested dialog handles Escape one layer at a time. While a form is open, the global search shortcut leaves its draft and keyboard focus in place. Closing returns focus to the invoking control. Textareas scroll or grow within their owner and never show a manual resize handle. Recipient fields use the textarea’s height, draft typography and inset. Committed addresses use compact caption text and an always-visible neutral ×, like a panel tab. The remove button sits inside the tag beside its label; long addresses yield space to the button. Comma or Enter commits a draft, pasting a list commits its addresses, and Backspace from an empty draft removes the last tag.
3535

3636
Use `useClipboard` / `CopyButton` for successful copy feedback and `useFeedbackExpiry` for temporary status. Copied indicators and transient notices dwell for four seconds; repeated events restart the timer. Pause an actionable notice during hover or focus and keep its action available in the stack afterward. Icon color changes use `--ui-motion-color` (120ms, zero under reduced motion). Do not put literal timers or CSS durations in product components.
3737

@@ -58,13 +58,13 @@ Use [the shared Platform time formatters](lib/time.ts) for displayed ages, clock
5858

5959
Only `lib/data/**` may import from outside this project: query hooks under `@/hooks/queries/<domain>`, contracts and clients under `@/lib/api/<module>`, and the named helpers listed in `scripts/check-platform-design.ts`. `@/app/<route>`, `@/components/<name>` and the legacy `@sim/emcn` barrel stay banned there too, apart from named modules on that list, such as the chat hook. Add a helper to that list only after reading its runtime import closure.
6060

61-
The chat hook runs here unchanged. Views read a `ChatRuntime` (`(shell)/_components/chat/model.ts`), filled by `lib/data/chat.ts` on live data and by the session on fixtures. What the hook reaches for a canvas this app does not have, `canvasShims` in `next.config.ts` replaces with a named module under `lib/data/shims/` saying what it drops.
61+
The chat hook runs here unchanged. Views read a `ChatRuntime` (`(shell)/_components/chat/model.ts`), filled by `lib/data/chat.ts` on live data and by the session on fixtures. What the hook reaches for a canvas this app does not have, `canvasShims` in `next.config.ts` replaces with a named module under `lib/data/shims/` saying what it drops. A shimmed store exposes only the fields the hook reads: subscribing to it or reading anything else throws. `check:platform-design` walks the seam's runtime closure and fails on any legacy view it bundles.
6262

63-
Each `lib/data/<domain>.ts` wraps the existing hook and maps its contract shape to a Platform view model with a pure adapter. Never fork a hook, key factory, invalidation or contract; a hook that lacks a needed option gains it additively. A module's first import of a query hook that toasts resolves through `lib/data/emcn-shim.ts`, which webpack substitutes for `@sim/emcn` and which exports only `toast`, bridged to the notification tray. Any other name from that package fails the build, which is what proves no legacy UI entered.
63+
Each `lib/data/<domain>.ts` wraps the existing hook and maps its contract shape to a Platform view model with a pure adapter. A domain whose views take the contract shape as it is has nothing to adapt: its `lib/data/<domain>/index.ts` is a barrel naming what crosses, with any hook of its own in a sibling file. A non-barrel module never re-exports. Never fork a hook, key factory, invalidation or contract; a hook that lacks a needed option gains it additively. A module's first import of a query hook that toasts resolves through `lib/data/emcn-shim.ts`, which webpack substitutes for `@sim/emcn` and which exports only `toast`, bridged to the notification tray. Any other name from that package fails the build, which is what proves no legacy UI entered.
6464

65-
Views stay prop-driven. A route composes `data` and a named `actions` interface, filled by session state on fixtures and by mutations on live data. No fixture branch inside a view. Hide what has no backend and record it in the [parity ledger](docs/org-parity.md); never fake it. Distinguish loading, empty and failure. A list reads `status` rather than treating empty as loading.
65+
Views that fixtures also render stay prop-driven. A route composes `data` and a named `actions` interface, filled by session state on fixtures and by mutations on live data. A surface that exists only on live data, such as a Settings section, People or Search, reads its domain's hooks from `lib/data` directly. No fixture branch inside a view. Hide what has no backend and record it in the [parity ledger](docs/org-parity.md); never fake it. Distinguish loading, empty and failure. A list reads `status` rather than treating empty as loading.
6666

67-
`resolvePlatformMode` decides each server entry once: `fixture` with `SIM_PLATFORM_FIXTURES=1`, `live` with `SIM_PLATFORM_API_ORIGIN`, otherwise `none`, today's empty rendering. Load the seam with a dynamic import behind the `live` check, so fixture servers never evaluate it. The viewer's organization comes from membership on the server, read once per request by `readOrganizationSurface`. Platform URLs carry no organization.
67+
A server read goes through `serverRequestJson`, which gives every read a deadline. A first-paint read lives in `lib/data/prefetch-<domain>.ts` under the hook's own key and `staleTime`, with `prefetchSignal()`; it may never block or fail the page, so the client fetches what it gave up. `resolvePlatformMode` decides each server entry once: `fixture` with `SIM_PLATFORM_FIXTURES=1`, `live` with `SIM_PLATFORM_API_ORIGIN`, otherwise `none`, today's empty rendering. Load the seam with a dynamic import behind the `live` check, so fixture servers never evaluate it. The viewer's organization comes from membership on the server, read once per request by `readOrganizationSurface`. Platform URLs carry no organization.
6868

6969
The legacy organization layout redirects flagged organizations here through `lib/platform/routes.ts`. Link out to a legacy route only with `legacyRoutes`, which builds an absolute URL, and only to a route that mapper leaves unmapped. Linking to a mapped route makes the two surfaces redirect to each other.
7070

‎apps/sim/platform/app/(shell)/_components/session-state/settings-leave-guard.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function SettingsLeaveGuard() {
1515
variant='confirm'
1616
title='Unsaved changes'
1717
confirmLabel='Discard changes'
18+
cancelLabel='Keep editing'
1819
destructive
1920
onConfirm={confirm}
2021
>

‎apps/sim/platform/app/(shell)/home/_components/activity-chart/activity-chart.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ChartTooltip } from '@/platform/design-system/components/chart/chart-to
1212
import { usePlotInspection } from '@/platform/design-system/components/chart/use-plot-inspection'
1313
import { RangePicker } from '@/platform/design-system/components/menu/range-picker'
1414
import { useScrollEdges } from '@/platform/design-system/components/scroll-area/use-scroll-edges'
15-
import { formatCalendarDate } from '@/platform/lib/time'
15+
import { DAY_MS, formatCalendarDate } from '@/platform/lib/time'
1616

1717
interface ActivityPoint {
1818
date: string
@@ -95,7 +95,6 @@ function ActivitySeries({ title, points, initialPoint }: ActivitySeriesProps) {
9595

9696
const WINDOW_DAYS = [7, 30, 90] as const
9797
const RANGE_LABELS = WINDOW_DAYS.map((days) => `Last ${days} days`)
98-
const DAY_MS = 86_400_000
9998

10099
function calendarWindow(history: readonly ActivityPoint[] | undefined, days: number) {
101100
if (!history?.length) return []

apps/sim/platform/app/(shell)/home/_components/live-home/live-home-page.tsx renamed to apps/sim/platform/app/(shell)/home/_components/live-home/live-home-route.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { prefetchHome } from '@/platform/lib/data/prefetch-home'
66
import { readOrganizationSurface } from '@/platform/lib/data/surface'
77

88
/** The live Home entry: the legacy page's gate, then its cheap reads for first paint. */
9-
export async function LiveHomePage() {
9+
export async function LiveHomeRoute() {
1010
const surface = await readOrganizationSurface()
1111
// The layout already rendered this outcome's login, denial or error state.
1212
if (surface.status !== 'ok') return null

‎apps/sim/platform/app/(shell)/home/page.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { resolvePlatformMode } from '@/platform/lib/data/mode'
66
export default async function Page({ searchParams }: PageProps<'/home'>) {
77
if (resolvePlatformMode() === 'live') {
88
// Loaded only here, so a fixture server never evaluates the data seam.
9-
const { LiveHomePage } = await import(
10-
'@/platform/app/(shell)/home/_components/live-home/live-home-page'
9+
const { LiveHomeRoute } = await import(
10+
'@/platform/app/(shell)/home/_components/live-home/live-home-route'
1111
)
12-
return <LiveHomePage />
12+
return <LiveHomeRoute />
1313
}
1414
const query = await searchParams
1515
const scenario = readScenario(query.state)

‎apps/sim/platform/app/(shell)/settings/[[...section]]/page.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export default async function Page({
1111
}: PageProps<'/settings/[[...section]]'>) {
1212
const [query, { section: path }] = await Promise.all([searchParams, params])
1313
if (resolvePlatformMode() === 'live') {
14-
const { LiveSettings } = await import(
15-
'@/platform/app/(shell)/settings/_components/live-settings/live-settings'
14+
const { LiveSettingsRoute } = await import(
15+
'@/platform/app/(shell)/settings/_components/live-settings/live-settings-route'
1616
)
17-
return <LiveSettings path={path ?? []} query={query} />
17+
return <LiveSettingsRoute path={path ?? []} query={query} />
1818
}
1919
const section = path?.[0] ?? 'preferences'
2020
const destination = SETTINGS_DESTINATIONS.find((item) => item.key === section)

apps/sim/platform/app/(shell)/settings/_components/live-settings/live-settings.tsx renamed to apps/sim/platform/app/(shell)/settings/_components/live-settings/live-settings-route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { SETTINGS_SECTIONS } from '@/platform/app/(shell)/settings/_components/s
44
import { SettingsScreen } from '@/platform/app/(shell)/settings/_components/settings-screen/settings-screen'
55
import { readOrganizationSurface } from '@/platform/lib/data/surface'
66

7-
interface LiveSettingsProps {
7+
interface LiveSettingsRouteProps {
88
path: readonly string[]
99
query: Record<string, string | string[] | undefined>
1010
}
1111

1212
/** The shell layout has already answered any surface failure. */
13-
export async function LiveSettings({ path, query }: LiveSettingsProps) {
13+
export async function LiveSettingsRoute({ path, query }: LiveSettingsRouteProps) {
1414
const surface = await readOrganizationSurface()
1515
if (surface.status !== 'ok') return null
1616
const { organization } = surface

‎apps/sim/platform/app/(shell)/settings/_components/sections/access-control/group-detail.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,7 @@ export function GroupDetail({
639639
variant='primary'
640640
size='compact'
641641
icon='I074'
642+
disabled={members.data === undefined}
642643
onClick={() => setAddingMembers(true)}
643644
>
644645
Add

‎apps/sim/platform/app/(shell)/settings/_components/sections/usage/usage-monitoring.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ export function UsageMonitoring({ organization }: Pick<SettingsSectionProps, 'or
5656
expanded: true,
5757
})
5858
const workspaceName = workspaceList.data?.rows.find((row) => row.id === workspace)?.label
59-
/** Only once the list has loaded, so a deep link does not flash the Workspaces tab first. */
59+
/** A dead id falls back to the Workspaces tab, once the list can actually say it is dead. */
6060
const isWorkspaceDetail =
61-
isWorkspaceSelected && (workspaceList.loading || workspaceName !== undefined)
61+
isWorkspaceSelected &&
62+
(workspaceList.loading || workspaceList.failed || workspaceName !== undefined)
6263
const dimension: UsageBreakdownDimension = isWorkspaceDetail
6364
? 'workflow'
6465
: isOverview || tab === 'activity'

0 commit comments

Comments
 (0)