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
9 changes: 9 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,15 @@ app.post("/api/people/import", async (c) => {
}
if (!rows.length) return c.json({ error: "No rows to import" }, 400);
if (rows.length > 500) return c.json({ error: "Max 500 rows per import" }, 400);
if (!em) {
return c.json(
{
error:
"Create an EM (a person with no manager) before importing directs, or pass defaultManagerId.",
},
400,
);
}

const existing = db.select().from(people).all();
const byEmail = new Map(
Expand Down
47 changes: 34 additions & 13 deletions apps/api/src/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,27 @@ export async function seedWorkspace(opts: {
throw new Error("Workspace already initialized");
}

const day = (offsetDays: number) => {
const d = new Date();
d.setUTCHours(12, 0, 0, 0);
d.setUTCDate(d.getUTCDate() + offsetDays);
return d.toISOString().slice(0, 10);
};
// Keep the open cycle and "recent" evidence relative to today so demos stay in-window.
const openStart = day(-150);
const openEnd = day(30);
const priorStart = day(-330);
const priorEnd = day(-151);
const midCycle = day(-90);
const midFeedback = day(-60);
const recentA = day(-12);
const recentB = day(-10);
const recentC = day(-9);
const recentD = day(-11);
const roleFrom = openStart;
const priorCycleName = `Prior cycle (${priorStart.slice(0, 4)})`;
const openCycleName = `Current cycle (${openStart.slice(0, 7)} → ${openEnd.slice(0, 7)})`;

const wsId = id("ws");
const accountId = id("acct");
const createdAt = nowIso();
Expand Down Expand Up @@ -224,7 +245,7 @@ export async function seedWorkspace(opts: {
roleDefinitionId,
frameworkVersionId: fwvId,
targetNextRoleDefinitionId: nextKey ? roleIds[nextKey] : null,
effectiveFrom: "2026-01-01",
effectiveFrom: roleFrom,
assignedByAccountId: accountId,
}).run();
}
Expand All @@ -235,7 +256,7 @@ export async function seedWorkspace(opts: {
db.insert(goals).values({
id: id("goal"),
personId: pid,
title: `Deliver H1 roadmap outcomes for ${member.levelKey}`,
title: `Deliver roadmap outcomes for ${member.levelKey}`,
description: "Measurable delivery against squad commitments.",
status: "on_track",
createdAt,
Expand All @@ -245,7 +266,7 @@ export async function seedWorkspace(opts: {
personId: pid,
title: "Shipped checkout reliability improvements",
description: "Reduced timeout errors; partnered with SRE.",
occurredAt: "2026-03-15",
occurredAt: midCycle,
createdAt,
}).run();
db.insert(feedbackItems).values({
Expand All @@ -255,7 +276,7 @@ export async function seedWorkspace(opts: {
kind: "praise",
body: "Clear communication in cross-functional design reviews.",
visibility: "em_and_subject",
occurredAt: "2026-04-02",
occurredAt: midFeedback,
themesJson: JSON.stringify(["collaboration", "delivery"]),
createdAt,
}).run();
Expand All @@ -270,7 +291,7 @@ export async function seedWorkspace(opts: {
personId: pid,
title: i === 0 ? "Closed checkout latency epic" : "Merged payment retry hardening",
description: "Synced-style achievement for the current coaching window.",
occurredAt: i === 0 ? "2026-07-18" : "2026-07-20",
occurredAt: i === 0 ? recentA : recentB,
createdAt,
sourceProvider: i === 0 ? "jira" : "bitbucket",
externalId: i === 0 ? "CHK-241" : "pr-1882",
Expand All @@ -283,7 +304,7 @@ export async function seedWorkspace(opts: {
kind: "coaching",
body: "Discussed pacing and stakeholder updates for the current sprint.",
visibility: "em_only",
occurredAt: "2026-07-21",
occurredAt: recentC,
themesJson: JSON.stringify(["pacing", "stakeholders"]),
createdAt,
}).run();
Expand All @@ -292,7 +313,7 @@ export async function seedWorkspace(opts: {
db.insert(concernLogs).values({
id: id("concern"),
personId: firstDirect,
occurredAt: "2026-07-19",
occurredAt: recentD,
title: "Missed partner handoff",
body: "Late handoff to design delayed review by a day.",
expectationText: "Confirm handoffs in Slack the morning of the due date.",
Expand All @@ -306,19 +327,19 @@ export async function seedWorkspace(opts: {
const priorCycleId = id("cycle");
db.insert(cycles).values({
id: priorCycleId,
name: "H2 2025",
windowStart: "2025-07-01",
windowEnd: "2025-12-31",
name: priorCycleName,
windowStart: priorStart,
windowEnd: priorEnd,
status: "closed",
createdAt,
}).run();

const cycleId = id("cycle");
db.insert(cycles).values({
id: cycleId,
name: "H1 2026",
windowStart: "2026-01-01",
windowEnd: "2026-06-30",
name: openCycleName,
windowStart: openStart,
windowEnd: openEnd,
status: "manager_open",
createdAt,
}).run();
Expand Down
17 changes: 1 addition & 16 deletions apps/ui/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,7 @@ export function HomePage() {
})
.catch(() => setDueWeek([]));

// Scheduled integration sync while the app is open (session-scoped; mirrors deadline nudge UX).
let lastSyncAt = 0;
const runScheduledSync = () => {
const now = Date.now();
if (now - lastSyncAt < 30 * 60_000) return;
lastSyncAt = now;
api<{ count: number }>("/api/integrations/sync-all", { method: "POST", body: "{}" }).catch(
() => undefined,
);
};
const syncTimer = window.setTimeout(runScheduledSync, 45_000);
const syncInterval = window.setInterval(runScheduledSync, 30 * 60_000);
return () => {
window.clearTimeout(syncTimer);
window.clearInterval(syncInterval);
};
return () => undefined;
}, []);

useEffect(() => {
Expand Down
48 changes: 15 additions & 33 deletions apps/ui/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,24 +104,26 @@ export function SettingsPage({
!settings.hasApiKey &&
!apiKey;

async function testAiConnection() {
try {
const res = await api<{ ok: boolean; error?: string; model?: string }>("/api/ai/test", {
method: "POST",
body: "{}",
});
setMsg(res.ok ? `Connection OK (${res.model})` : `Failed: ${res.error}`);
} catch (err) {
setMsg(err instanceof Error ? err.message : "Test failed");
}
}

return (
<div className="stack">
<PageHeader
title="Settings"
job="Appearance, AI, integrations, and private-tier egress. Keys stay encrypted with the workspace secret."
primary={<button className="btn" form="ai-settings-form" type="submit">Save AI settings</button>}
secondary={
<button
type="button"
className="btn secondary"
onClick={async () => {
const res = await api<{ ok: boolean; error?: string; model?: string }>("/api/ai/test", {
method: "POST",
body: "{}",
});
setMsg(res.ok ? `Connection OK (${res.model})` : `Failed: ${res.error}`);
}}
>
<button type="button" className="btn secondary" onClick={() => void testAiConnection()}>
Test AI connection
</button>
}
Expand Down Expand Up @@ -341,17 +343,7 @@ export function SettingsPage({
label="AI ready for writing desk"
detail="Digest and section drafts pull from dossier + imports. Private notes stay off cloud unless you opt in below."
action={
<button
type="button"
className="btn"
onClick={async () => {
const res = await api<{ ok: boolean; error?: string; model?: string }>("/api/ai/test", {
method: "POST",
body: "{}",
});
setMsg(res.ok ? `Connection OK (${res.model})` : `Failed: ${res.error}`);
}}
>
<button type="button" className="btn" onClick={() => void testAiConnection()}>
Test AI connection
</button>
}
Expand Down Expand Up @@ -518,17 +510,7 @@ export function SettingsPage({
))}
<div className="cta-primary-row">
<button className="btn">Save AI settings</button>
<button
type="button"
className="btn secondary"
onClick={async () => {
const res = await api<{ ok: boolean; error?: string; model?: string }>("/api/ai/test", {
method: "POST",
body: "{}",
});
setMsg(res.ok ? `Connection OK (${res.model})` : `Failed: ${res.error}`);
}}
>
<button type="button" className="btn secondary" onClick={() => void testAiConnection()}>
Test AI connection
</button>
</div>
Expand Down
21 changes: 11 additions & 10 deletions apps/ui/src/pages/TeamPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ export function TeamPage() {
<tr><th>Name</th><th>Title</th><th>Level</th><th>Role</th><th>Next</th><th>Your move</th></tr>
</thead>
<tbody>
{em && (
<tr>
<td data-label="Name">{em.name}</td>
<td data-label="Title" className="muted">{em.title}</td>
<td data-label="Level">{em.levelKey}</td>
<td data-label="Role" className="muted">EM account</td>
<td data-label="Next" className="muted">—</td>
<td data-label="Your move"><Link className="btn" to={`/team/${em.id}`}>Open dossier</Link></td>
</tr>
)}
{directs.map((p) => (
<tr key={p.id}>
<td data-label="Name">{p.name}</td>
Expand All @@ -133,15 +143,6 @@ export function TeamPage() {
</td>
</tr>
))}
{em && directs.length === 0 && (
<tr>
<td data-label="Name">{em.name}</td>
<td data-label="Title" className="muted">{em.title}</td>
<td data-label="Level">{em.levelKey}</td>
<td data-label="Role" colSpan={2} className="muted">EM account</td>
<td data-label="Your move"><Link className="btn" to={`/team/${em.id}`}>Open dossier</Link></td>
</tr>
)}
</tbody>
</table>
)}
Expand Down Expand Up @@ -176,7 +177,7 @@ export function TeamPage() {
<h2>Import CSV</h2>
<p className="muted">
Columns: name (required), email, title, levelKey, hireDate. Duplicates by email or exact name are skipped.
New people report to the workspace EM.
Requires an EM on the roster first — new people report to that EM (or `defaultManagerId`).
</p>
<div className="field">
<label htmlFor="csv-paste">Paste CSV</label>
Expand Down
Loading