Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ next-env.d.ts
.claude/
.claude/settings.local.json
.cursor/
.codex/environments/environment.toml
*.code-workspace

# local exports
Expand Down
3 changes: 2 additions & 1 deletion agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ npm run db:push # Push schema to DB (dev shortcut)
5. **Soft Deletes**: Tasks, notes, projects use `deletedAt` field. Never hard-delete these.
6. **Task Completion**: Optimistic UI + undo toast (`toast.undo()`) pattern across all task pages.
7. **Tests**: New API routes should have integration tests mocking `@/auth` and `@/db`.
8. **Env Vars**: `.env.local` (gitignored) — `AUTH_SECRET`, `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`.
8. **TDD by Default**: Prefer red/green TDD. Write or update the failing test first when feasible, then implement the code change, then rerun the focused tests before broader verification.
9. **Env Vars**: `.env.local` (gitignored) — `AUTH_SECRET`, `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`.

## Files You Should Know

Expand Down
23 changes: 2 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dispatch",
"version": "0.5.1",
"version": "0.5.6",
"private": true,
"scripts": {
"dev": "concurrently -k -n NEXT,MCP -c cyan,magenta \"npm run next:dev\" \"npm run mcp:dev\"",
Expand Down
Empty file modified scripts/launchers/dispatch-dev.sh
100644 → 100755
Empty file.
1 change: 1 addition & 0 deletions src/app/api/dispatches/[id]/tasks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const GET = withAuth(async (req, session, ctx) => {
status: tasks.status,
priority: tasks.priority,
dueDate: tasks.dueDate,
dueTime: tasks.dueTime,
recurrenceType: tasks.recurrenceType,
recurrenceBehavior: tasks.recurrenceBehavior,
recurrenceRule: tasks.recurrenceRule,
Expand Down
27 changes: 27 additions & 0 deletions src/app/api/me/__tests__/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const TEST_USER = {
role: "admin" as const,
showAdminQuickAccess: true,
assistantEnabled: true,
dashboardDueTimesEnabled: false,
timeZone: null,
};

Expand Down Expand Up @@ -65,6 +66,7 @@ describe("Me API", () => {
expect(await res.json()).toEqual({
showAdminQuickAccess: false,
assistantEnabled: true,
dashboardDueTimesEnabled: false,
timeZone: null,
templatePresets: EMPTY_TEMPLATE_PRESETS,
});
Expand All @@ -86,6 +88,7 @@ describe("Me API", () => {
expect(await res.json()).toEqual({
showAdminQuickAccess: true,
assistantEnabled: false,
dashboardDueTimesEnabled: false,
timeZone: null,
templatePresets: EMPTY_TEMPLATE_PRESETS,
});
Expand All @@ -98,6 +101,28 @@ describe("Me API", () => {
expect(updated.assistantEnabled).toBe(false);
});

it("PUT updates dashboard due time visibility", async () => {
const res = await PUT(
jsonReq("http://localhost/api/me", { dashboardDueTimesEnabled: true }),
{},
);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({
showAdminQuickAccess: true,
assistantEnabled: true,
dashboardDueTimesEnabled: true,
timeZone: null,
templatePresets: EMPTY_TEMPLATE_PRESETS,
});

const [updated] = testDb.db
.select({ dashboardDueTimesEnabled: users.dashboardDueTimesEnabled })
.from(users)
.where(eq(users.id, TEST_USER.id))
.all();
expect(updated.dashboardDueTimesEnabled).toBe(true);
});

it("PUT rejects invalid payload values", async () => {
const res = await PUT(
jsonReq("http://localhost/api/me", { showAdminQuickAccess: "nope" }),
Expand All @@ -115,6 +140,7 @@ describe("Me API", () => {
expect(await res.json()).toEqual({
showAdminQuickAccess: true,
assistantEnabled: true,
dashboardDueTimesEnabled: false,
timeZone: "America/Los_Angeles",
templatePresets: EMPTY_TEMPLATE_PRESETS,
});
Expand Down Expand Up @@ -173,6 +199,7 @@ describe("Me API", () => {
expect(await res.json()).toEqual({
showAdminQuickAccess: true,
assistantEnabled: true,
dashboardDueTimesEnabled: false,
timeZone: null,
templatePresets,
});
Expand Down
19 changes: 18 additions & 1 deletion src/app/api/me/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const GET = withAuth(async (_req, session) => {
.select({
showAdminQuickAccess: users.showAdminQuickAccess,
assistantEnabled: users.assistantEnabled,
dashboardDueTimesEnabled: users.dashboardDueTimesEnabled,
timeZone: users.timeZone,
templatePresets: users.templatePresets,
})
Expand All @@ -25,6 +26,7 @@ export const GET = withAuth(async (_req, session) => {
user: session.user,
showAdminQuickAccess: preferences?.showAdminQuickAccess ?? true,
assistantEnabled: preferences?.assistantEnabled ?? true,
dashboardDueTimesEnabled: preferences?.dashboardDueTimesEnabled ?? false,
timeZone: preferences?.timeZone ?? null,
templatePresets: parseStoredTemplatePresets(preferences?.templatePresets),
});
Expand All @@ -38,7 +40,13 @@ export const PUT = withAuth(async (req, session) => {
return errorResponse("Invalid JSON body", 400);
}

const { showAdminQuickAccess, assistantEnabled, timeZone, templatePresets } = body as Record<string, unknown>;
const {
showAdminQuickAccess,
assistantEnabled,
dashboardDueTimesEnabled,
timeZone,
templatePresets,
} = body as Record<string, unknown>;

if (showAdminQuickAccess !== undefined && typeof showAdminQuickAccess !== "boolean") {
return errorResponse("showAdminQuickAccess must be a boolean", 400);
Expand All @@ -48,6 +56,10 @@ export const PUT = withAuth(async (req, session) => {
return errorResponse("assistantEnabled must be a boolean", 400);
}

if (dashboardDueTimesEnabled !== undefined && typeof dashboardDueTimesEnabled !== "boolean") {
return errorResponse("dashboardDueTimesEnabled must be a boolean", 400);
}

if (timeZone !== undefined && timeZone !== null && typeof timeZone !== "string") {
return errorResponse("timeZone must be a string or null", 400);
}
Expand All @@ -74,6 +86,7 @@ export const PUT = withAuth(async (req, session) => {
if (
showAdminQuickAccess === undefined
&& assistantEnabled === undefined
&& dashboardDueTimesEnabled === undefined
&& timeZone === undefined
&& templatePresets === undefined
) {
Expand All @@ -83,6 +96,7 @@ export const PUT = withAuth(async (req, session) => {
const updates: Record<string, unknown> = {};
if (showAdminQuickAccess !== undefined) updates.showAdminQuickAccess = showAdminQuickAccess;
if (assistantEnabled !== undefined) updates.assistantEnabled = assistantEnabled;
if (dashboardDueTimesEnabled !== undefined) updates.dashboardDueTimesEnabled = dashboardDueTimesEnabled;
if (timeZone !== undefined) updates.timeZone = typeof timeZone === "string" ? timeZone.trim() : null;
if (validatedTemplatePresets !== undefined) {
updates.templatePresets = serializeTemplatePresets(validatedTemplatePresets);
Expand All @@ -95,6 +109,7 @@ export const PUT = withAuth(async (req, session) => {
.returning({
showAdminQuickAccess: users.showAdminQuickAccess,
assistantEnabled: users.assistantEnabled,
dashboardDueTimesEnabled: users.dashboardDueTimesEnabled,
timeZone: users.timeZone,
templatePresets: users.templatePresets,
});
Expand All @@ -104,6 +119,8 @@ export const PUT = withAuth(async (req, session) => {
updated?.showAdminQuickAccess ?? (showAdminQuickAccess as boolean | undefined) ?? true,
assistantEnabled:
updated?.assistantEnabled ?? (assistantEnabled as boolean | undefined) ?? true,
dashboardDueTimesEnabled:
updated?.dashboardDueTimesEnabled ?? (dashboardDueTimesEnabled as boolean | undefined) ?? false,
timeZone: updated?.timeZone ?? (typeof timeZone === "string" ? timeZone.trim() : null),
templatePresets: parseStoredTemplatePresets(updated?.templatePresets),
});
Expand Down
Loading