Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
9af1a3e
docs: the Market Domination Strategy, audited item by item against th…
claude Sep 7, 2026
b724220
The Daily Brief leads with what needs deciding, keeps money from chil…
claude Sep 7, 2026
abc0d3b
Every decision waiting on a person has one uncapped page, and three m…
claude Sep 7, 2026
0bddaf4
wip: partial work on U3 (agent interrupted before finishing; unreviewed)
claude Sep 7, 2026
af53cdf
wip: partial work on A1 (agent interrupted before finishing; unreviewed)
claude Sep 7, 2026
158b701
wip: partial work on S1 (agent interrupted before finishing; unreviewed)
claude Sep 7, 2026
8958177
wip: partial work on R1 (agent interrupted before finishing; unreviewed)
claude Sep 7, 2026
a4f4282
wip: partial work on R2 (agent interrupted before finishing; unreviewed)
claude Sep 7, 2026
1c51c53
Merge prior partial U3 schedule-intelligence work for review
claude Sep 7, 2026
2ab8756
Merge partial A1 work (interrupted agent) for verification
claude Sep 7, 2026
5c5751d
Merge partial R1 work from interrupted agent for review
claude Sep 7, 2026
705f522
Merge partial R2 work (unreviewed) for verification
claude Sep 7, 2026
f227c64
Merge prior partial S1 work (unreviewed) for verification
claude Sep 7, 2026
04ddb7d
wip: A1 household autopilot — catalogue keys for learned policies, ev…
claude Sep 7, 2026
528ecfc
A recommendation with an off-app link no longer takes the whole Daily…
claude Sep 7, 2026
4a1ba72
wip: concierge call queue says only what the row persists; pure compa…
claude Sep 7, 2026
4243b89
wip: wire replanRun into the executor port, chief_of_staff intent tests
claude Sep 7, 2026
fae659a
wip: S1 review fixes — handledNote through the catalogue, familiesNot…
claude Sep 7, 2026
5f52775
One per-event schedule model: prep, departure, driver, car, dinner an…
claude Sep 7, 2026
311c202
Household Autopilot learns narrow policies from what the family alrea…
claude Sep 7, 2026
c50d718
wip: log failed public-stats reads; pin the logging in the honesty test
claude Sep 7, 2026
65153e1
wip: compareQuotes tool + projects comparison block, catalogue keys, …
claude Sep 7, 2026
9eb725e
wip: inventory and moving services and tools
claude Sep 7, 2026
1d48991
Merge branch 'worktree-wf_c031046b-98e-1' into claude/roadmap-impleme…
claude Sep 7, 2026
004eb40
Merge branch 'worktree-wf_c031046b-98e-2' into claude/roadmap-impleme…
claude Sep 7, 2026
713c1b1
The call queue says only what the row persists, and quotes compare fr…
claude Sep 7, 2026
049c827
wip: Family CFO plan-aware forecast, coverage, affordability scenario
claude Sep 7, 2026
0a7a014
Wire re-planning into the run executor and give broad requests a chie…
claude Sep 7, 2026
00c425b
Security as a product surface: two-step sign-in, step-up on money/doc…
claude Sep 7, 2026
7bd0f0b
wip: plan_move intent, moving slice and template, confirm-it's-here, …
claude Sep 7, 2026
cf7f302
The Handled ledger says which tool acted and why; a family run histor…
claude Sep 7, 2026
7caa1bf
Load the planner lazily from the re-planning port
claude Sep 7, 2026
49289ee
build: raise the build heap to 6 GB, and delete a setting that never …
claude Sep 7, 2026
ae298f0
Revert "build: raise the build heap to 6 GB, and delete a setting tha…
claude Sep 7, 2026
50eca22
Merge branch 'worktree-wf_30a084e9-871-1' into claude/roadmap-impleme…
claude Sep 7, 2026
81ba3af
Merge branch 'worktree-wf_fa149639-0f3-1' into claude/roadmap-impleme…
claude Sep 7, 2026
97b82b7
Merge branch 'worktree-wf_fa149639-0f3-2' into claude/roadmap-impleme…
claude Sep 7, 2026
aab1016
Merge branch 'worktree-wf_30a084e9-871-2' into claude/roadmap-impleme…
claude Sep 7, 2026
62e81a4
wip: uncommitted work saved when the agent was interrupted (unreviewed)
claude Sep 7, 2026
d470829
wip: uncommitted work saved when the agent was interrupted (unreviewed)
claude Sep 7, 2026
901cec1
merge: partial Family CFO work from interrupted agent
claude Sep 7, 2026
54112c8
merge: bring in the interrupted inventory and moving work for review
claude Sep 7, 2026
32ccf75
Fix the merge fallout from four items landing together
claude Sep 7, 2026
77e0c5c
Inventory and moving: fix the plan_move step-key collision, harden th…
claude Sep 7, 2026
d70f9f4
Family CFO: plan-source badges link to the modules that exist, and th…
claude Sep 7, 2026
51ed673
Merge prior S1 work for verification and completion
claude Sep 7, 2026
dc44aa8
Cover the confirm-it's-here write boundary and the move slice's manag…
claude Sep 7, 2026
4084afc
Money timeline: stop counting money the forecast cannot carry, and ke…
claude Sep 7, 2026
dbfde71
Keep the instantiateTemplate doc comment on its function
claude Sep 7, 2026
a1e93f3
Affordability answer echoes the date it was computed for
claude Sep 7, 2026
7cdcdf3
wip: trust activity loader, tab, based-on plumbing and catalogue keys
claude Sep 7, 2026
8a71b5c
wip: resolve the merge onto main's footer keys and badge the rail's s…
claude Sep 7, 2026
ad91e85
Outcome-first homepage: Bubaly Handled proof band, six-outcome rail, …
claude Sep 7, 2026
d67000b
Trust Center shows what Bubaly did, may do, and was allowed to read
claude Sep 7, 2026
e4cce40
The type-check moves out of the build worker instead of sharing its heap
claude Sep 7, 2026
8991e29
Every notification can be acted on, and the quiet ones stop shouting
claude Sep 7, 2026
54991f8
Merge remote-tracking branch 'origin/main' into claude/roadmap-implem…
claude Sep 7, 2026
9bf79c5
Merge branch 'worktree-wf_67303136-dd9-1' into claude/roadmap-impleme…
claude Sep 7, 2026
126eea4
Merge branch 'worktree-wf_67303136-dd9-2' into claude/roadmap-impleme…
claude Sep 7, 2026
ebe92b3
Merge branch 'worktree-wf_1d216cac-323-1' into claude/roadmap-impleme…
claude Sep 7, 2026
9084cd4
Reconcile the Daily Brief's two new slices where they met
claude Sep 7, 2026
fabdca4
Merge branch 'worktree-wf_1d216cac-323-2' into claude/roadmap-impleme…
claude Sep 7, 2026
fdca150
Merge branch 'worktree-wf_0792d5dc-6b2-1' into claude/roadmap-impleme…
claude Sep 7, 2026
459c99c
A run finished before 0250 belongs under Done, not under In progress
claude Sep 7, 2026
5816bbc
A run says a routine started it only when the row says so
claude Sep 7, 2026
f5868f5
Drop the homepage proof band's dead "live demo" CTA
claude Sep 7, 2026
6baf29e
Say which queue sections are already being worked, so nobody duplicat…
claude Sep 7, 2026
a93dd82
Write down the four forks M13 and M14 created
claude Sep 7, 2026
5ff4292
Restore the precondition the short-screen menu test depends on
claude Sep 7, 2026
55aba73
Merge remote-tracking branch 'origin/main' into claude/roadmap-implem…
claude Sep 7, 2026
d3608c6
Merge main: a ServiceResult cannot ride inside settleAll
claude Sep 7, 2026
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
26 changes: 16 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,24 @@ jobs:
- name: Marketing asset audit
run: npm run marketing:audit:assets

- name: Typecheck
run: npm run typecheck
env:
# The standalone check needs the same schema/type heap as next build.
NODE_OPTIONS: --max-old-space-size=4096

- name: Lint
run: npm run lint

- name: Unit tests
run: npm test

# Build BEFORE Typecheck, deliberately. next.config.mjs turns off the
# type-check and lint passes inside `next build` — they were a duplicate
# of the two steps here, and the one worker that ran them peaked at
# 4,471 MB against the 4,096 MB cap below, which is where a Vercel
# deployment died. Building first still generates .next/types/**, which
# tsconfig.json includes, so the Typecheck step that follows validates
# every route signature as well as the app — the same coverage the build
# used to give, in its own process instead of alongside webpack's.
- name: Build
run: npm run build
env:
# `next build` type-checks every route plus the generated .next/types
# in one worker; the 435-table types file pushes that past Node's
# default ~2 GB heap on the runner (OOM at "checking validity of
# types"). 4 GB fits the 7 GB runner with room for webpack.
# Webpack alone; the type-check moved to its own step below.
NODE_OPTIONS: --max-old-space-size=4096
# Dummy values so static generation doesn't crash on missing config.
# Real data is fetched at runtime; build-time fetches fail closed
Expand All @@ -62,6 +61,13 @@ jobs:
SUPABASE_SERVICE_ROLE_KEY: dummy-service-role-key
NEXT_PUBLIC_APP_URL: https://example.com

- name: Typecheck
run: npm run typecheck
env:
# Checks app + tests + the .next/types route types the Build step
# just generated; needs the same schema/type heap as before.
NODE_OPTIONS: --max-old-space-size=4096

mobile:
# The Expo app (mobile/) is its own npm project sharing design/tokens.json
# with the web app. Typecheck + config validation is enough to gate PRs;
Expand Down
45 changes: 45 additions & 0 deletions app/(app)/auth/step-up/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Metadata } from 'next';
import { redirect } from 'next/navigation';
import { requireUserContext } from '@/lib/supabase/auth';
import { createServer } from '@/lib/supabase/server';
import { readAssurance } from '@/lib/auth/require-aal2';
import { isSafeReturnPath, sessionStrength } from '@/lib/auth/mfa';
import { Logo } from '@/components/brand/logo';
import { StepUpForm } from '@/components/auth/step-up-form';

export const metadata: Metadata = { title: 'Confirm it’s you', robots: { index: false, follow: false } };
export const dynamic = 'force-dynamic';

/**
* Where an `aal1` session lands when a money/documents/trust page needs
* `aal2`. Sits under `(app)` for the App Lock gate but outside `/dashboard`,
* so it renders without the sidebar — a person mid-step-up should not be
* offered forty other places to go.
*
* A session that no longer needs a code (already `aal2`, or no factor at all)
* is sent straight back: the page never asks for something it cannot use.
*/
export default async function StepUpPage({ searchParams }: { searchParams: Promise<{ next?: string | string[] }> }) {
const ctx = await requireUserContext();
const params = await searchParams;
const rawNext = Array.isArray(params.next) ? params.next[0] : params.next;
const next = isSafeReturnPath(rawNext) ? rawNext : '/dashboard';

const supabase = await createServer();
const read = await readAssurance(supabase);
if (read.ok && sessionStrength(read.assurance) !== 'needs_step_up') redirect(next);
if (!read.ok) console.error('[auth/step-up] assurance level read failed', { userId: ctx.user.id, error: read.error });

return (
<div className="flex min-h-dvh flex-col">
<header className="flex items-center px-5 py-5 pt-[calc(1.25rem+var(--safe-top))] sm:px-8">
<Logo />
</header>
<main className="flex flex-1 items-start justify-center px-5 py-8">
<div className="w-full max-w-md">
<StepUpForm next={next} serverReadFailed={!read.ok} />
</div>
</main>
</div>
);
}
3 changes: 2 additions & 1 deletion app/(app)/dashboard/autopay/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import { requireUserContext } from '@/lib/supabase/auth';
import { requireAal2 } from '@/lib/auth/require-aal2';
import { BillsView } from '@/components/finance/bills-view';
export const metadata: Metadata = { title: 'Auto Pay' };
export default async function Page() { await requireUserContext(); return <BillsView mode="autopay" />; }
export default async function Page() { const ctx = await requireUserContext(); await requireAal2(ctx, 'money', '/dashboard/autopay'); return <BillsView mode="autopay" />; }
3 changes: 2 additions & 1 deletion app/(app)/dashboard/bills/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import { requireUserContext } from '@/lib/supabase/auth';
import { requireAal2 } from '@/lib/auth/require-aal2';
import { BillsView } from '@/components/finance/bills-view';
export const metadata: Metadata = { title: 'Bill Manager' };
export default async function Page() { await requireUserContext(); return <BillsView mode="all" />; }
export default async function Page() { const ctx = await requireUserContext(); await requireAal2(ctx, 'money', '/dashboard/bills'); return <BillsView mode="all" />; }
4 changes: 3 additions & 1 deletion app/(app)/dashboard/binder/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Metadata } from 'next';
import { requireFeature } from '@/lib/supabase/auth';
import { requireAal2 } from '@/lib/auth/require-aal2';
import { BinderModule } from '@/components/modules/binder-module';

export const metadata: Metadata = { title: 'Household Binder' };

export default async function BinderPage() {
await requireFeature('/dashboard/binder');
const ctx = await requireFeature('/dashboard/binder');
await requireAal2(ctx, 'documents', '/dashboard/binder');
return <BinderModule />;
}
3 changes: 2 additions & 1 deletion app/(app)/dashboard/budgets/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import { requireUserContext } from '@/lib/supabase/auth';
import { requireAal2 } from '@/lib/auth/require-aal2';
import { BudgetsView } from '@/components/finance/budgets-view';
export const metadata: Metadata = { title: 'Budget Planner' };
export default async function Page() { await requireUserContext(); return <BudgetsView />; }
export default async function Page() { const ctx = await requireUserContext(); await requireAal2(ctx, 'money', '/dashboard/budgets'); return <BudgetsView />; }
49 changes: 49 additions & 0 deletions app/(app)/dashboard/calendar/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { makeKey } from '@/lib/services/idempotency';
import { scopeFromUserContext } from '@/lib/services/scope';
import { isSubmissionId } from '@/lib/utils/submission-id';
import { describeActionError } from '@/lib/supabase/errors';
import { loadScheduleIntelligence } from '@/lib/schedule/intelligence-server';
import type { ScheduleInsight } from '@/lib/schedule/intelligence';
import type { EventCategory, RecurrenceFreq } from '@/lib/database.types';

const PATH = '/dashboard/calendar';
Expand Down Expand Up @@ -293,3 +295,50 @@ export async function undoCalendarEventsAction(eventIds: string[]): Promise<Undo
return { ok: false, error: describeActionError(err, t('actions.couldNotUndoThoseEvents')) };
}
}

export type EventScheduleInsightsResult =
| { ok: true; insights: ScheduleInsight[]; leaveBy: { at: string; source: 'drive_time' | 'category_buffer'; travelMinutes: number } | null }
| { ok: false; error: string };

/**
* The composed schedule model for ONE event (lib/schedule/intelligence): its
* leave-by, and every driver, car, dinner, care and double-booking constraint
* the rest of the family's day puts on it. Read-only.
*
* Fail-closed: a read failure is an error the modal shows with a retry, never
* an empty list — "no conflicts" is a claim, and one computed from missing
* rows would be false.
*/
export async function eventScheduleInsightsAction(eventId: string): Promise<EventScheduleInsightsResult> {
const t = await getTranslations();
if (!eventId) return { ok: false, error: t('actions.thatEventCouldNotBe') };
const ctx = await requireUserContext();
const supabase = await createServer();
const familyId = ctx.active.familyId;
try {
const { data: event, error } = await supabase
.from('calendar_events').select('id, starts_at, ends_at, all_day')
.eq('id', eventId).eq('family_id', familyId).maybeSingle();
if (error) {
console.error('[schedule] event read failed', error);
return { ok: false, error: t('actions.couldNotLoadScheduleInsights') };
}
if (!event) return { ok: false, error: t('actions.thatEventCouldNotBe') };
if (event.all_day) return { ok: true, insights: [], leaveBy: null };

const startMs = Date.parse(event.starts_at);
if (!Number.isFinite(startMs)) return { ok: true, insights: [], leaveBy: null };
// Everything that can constrain the event lives on the same family day,
// give or take: a day either side covers overnight rides and late dinners.
const result = await loadScheduleIntelligence(supabase, {
familyId, tz: ctx.active.family.timezone || 'UTC', now: new Date(),
fromMs: startMs - 86_400_000, toMs: startMs + 86_400_000,
});
if (!result.ok) return { ok: false, error: t('actions.couldNotLoadScheduleInsights') };
const sched = result.data.events.find((e) => e.eventId === eventId);
return { ok: true, insights: result.data.byEvent[eventId] ?? [], leaveBy: sched?.leaveBy ?? null };
} catch (err) {
console.error('[schedule] event insights read failed', err);
return { ok: false, error: describeActionError(err, t('actions.couldNotLoadScheduleInsights')) };
}
}
25 changes: 25 additions & 0 deletions app/(app)/dashboard/concierge-calls/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,28 @@ export async function requeueCallAction(id: string, phone?: string): Promise<Res
if (error) return { ok: false, error: error.message };
return { ok: true };
}

/**
* A parent made the call themselves and is recording what happened. This is
* the only way a row reaches 'completed' today: Bubaly has no outbound voice
* integration, so no provider ever writes `provider_ref`, and the UI shows such
* a row as "Done — logged by hand" rather than "Called" (see
* `callDisplayState` in lib/concierge-calls/brief.ts).
*/
export async function logCallOutcomeAction(id: string, outcome: string): Promise<Result> {
const t = await getTranslations();
const ctx = await requireUserContext();
if (!isManager(ctx.active.role)) return { ok: false, error: t('actions.onlyParentsGuardiansCanManage') };
const text = (outcome ?? '').trim();
if (text.length < 2) return { ok: false, error: t('actions.sayWhatHappenedOnThe') };
const supabase = await createServer();
const { error } = await supabase.from('concierge_calls')
.update({ status: 'completed', outcome: text.slice(0, 2000), completed_at: new Date().toISOString() })
.eq('id', id).eq('family_id', ctx.active.familyId).in('status', ['draft', 'queued', 'failed', 'action_needed']);
if (error) return { ok: false, error: error.message };
await logAudit(supabase, {
familyId: ctx.active.familyId, actorId: ctx.user.id, action: 'update',
resource: 'concierge_calls', resourceId: id, metadata: { loggedByHand: true },
});
return { ok: true };
}
17 changes: 14 additions & 3 deletions app/(app)/dashboard/concierge-calls/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import type { Metadata } from 'next';
import { requireUserContext } from '@/lib/supabase/auth';
import { createServer } from '@/lib/supabase/server';
import { getTranslations } from '@/lib/i18n/server';
import { ErrorState } from '@/components/ui/states';
import { ConciergeCallsModule } from '@/components/modules/concierge-calls-module';
import type { Tables } from '@/lib/database.types';

export const metadata: Metadata = { title: 'AI Calls' };
export const dynamic = 'force-dynamic';

/** "Bubaly calls for you" — outbound AI concierge calls. Any family member can
* request a call; the module shows the AI brief, live status, and outcomes. */
/** Outbound concierge calls: the family writes the request, Bubaly writes the
* call plan, and the row carries what is actually known about the call. Any
* family member can see the queue; the module shows the brief, the persisted
* state and the outcome. */
export default async function ConciergeCallsPage() {
const t = await getTranslations();
const ctx = await requireUserContext();
const supabase = await createServer();

const { data } = await supabase
const { data, error } = await supabase
.from('concierge_calls')
.select('*')
.eq('family_id', ctx.active.familyId)
.order('created_at', { ascending: false })
.limit(100);
if (error) {
// Fail closed: an empty queue after a failed read would tell a parent
// nothing is waiting when something might be.
console.error('[dashboard/concierge-calls] calls read failed', error);
return <ErrorState message={t('conciergeCalls.couldNotLoadYourCallRequests')} />;
}

return (
<ConciergeCallsModule
Expand Down
13 changes: 13 additions & 0 deletions app/(app)/dashboard/concierge/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { History } from 'lucide-react';
import { getTranslations } from '@/lib/i18n/server';
import { requireUserContext } from '@/lib/supabase/auth';
import { createServer } from '@/lib/supabase/server';
Expand Down Expand Up @@ -29,6 +31,17 @@ export default async function ConciergePage() {

return (
<>
{/* M35: the one chronological list of everything Bubaly has done and is
doing for this family. Linked here and from Home, never the sidebar. */}
<div className="mx-auto max-w-5xl px-4 pt-6">
<Link
href="/dashboard/concierge/runs"
className="inline-flex min-h-11 items-center gap-1.5 rounded-xl border border-border bg-surface/40 px-3.5 text-sm font-medium text-fg transition hover:bg-elevated focus-ring"
>
<History className="h-4 w-4 text-brand-text" aria-hidden /> {t('concierge.runHistory')}
<span className="hidden text-xs text-muted sm:inline">{t('concierge.everythingBubalyHasDoneAnd')}</span>
</Link>
</div>
{reasoningError && (
<div className="mx-auto mb-4 max-w-5xl px-4 pt-6">
<ErrorState message={t('concierge.relationshipInsightsAreTemporarilyUnavailable')} />
Expand Down
13 changes: 11 additions & 2 deletions app/(app)/dashboard/concierge/runs/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ import { editStepInput } from '@/lib/ai/runs/controls';
import { kickRun } from '@/lib/ai/runs/continue';
import type { StepState } from '@/lib/ai/runs/states';
import { runPagePath } from '@/lib/ai/chat-request';
import { toolDomain } from '@/lib/ai/tool-domains';
import { editableFieldsFor } from '@/lib/approvals/card-data';
import type { RunActionResult } from '@/app/(app)/dashboard/concierge/run-actions';
import { ErrorState } from '@/components/ui/states';
import { ApprovalCard } from '@/components/approvals/approval-card';
import { StatusBadge } from '@/components/concierge/status-badge';
import { RunTimeline } from '@/components/concierge/run-timeline';
import { RunTimeline, type StepSources } from '@/components/concierge/run-timeline';
import { RunControls, type EditableStep, type FailedStep } from '@/components/concierge/run-controls';
import { ClarificationCard } from '@/components/concierge/clarification-card';
import { getTranslations } from '@/lib/i18n/server';
Expand Down Expand Up @@ -76,6 +77,14 @@ export default async function RunDetailPage({ params }: { params: Promise<{ id:
const requestId = detail.data.run.request_id;
const canControl = manager || detail.data.run.requested_by_member_id === ctx.active.member.id;
const view = toRunView(detail.data, familyId, manager);
// M6: which tool each step runs, from the persisted `tool_name` — the source
// shown beside the step. Passed apart from the view so the read boundary
// stays exactly what tests/run-detail-read-boundary.test.ts pins.
const stepSources: StepSources = Object.fromEntries(
detail.data.steps
.filter((s) => !!s.tool_name?.trim())
.map((s) => [s.id, { tool: (s.tool_name as string).trim(), domain: toolDomain(s.tool_name as string) }]),
);

const failedSteps: FailedStep[] = view.steps
.filter((s) => RERUNNABLE_STEP_STATES.includes(s.status))
Expand Down Expand Up @@ -179,7 +188,7 @@ export default async function RunDetailPage({ params }: { params: Promise<{ id:
)}

<section aria-label={t('dashboardConciergeRuns.progress')} className="rounded-2xl border border-border bg-surface/40 p-4 sm:p-5">
<RunTimeline view={view} showActivity={showActivity} />
<RunTimeline view={view} showActivity={showActivity} stepSources={stepSources} />
</section>
</div>
);
Expand Down
Loading
Loading