feat(#37-41): Add domains, ideas, relationships, agents, widgets schema

Schema additions for issues #37-41:

New models:
- Domain (#37): Life domains (work, marriage, homelab, etc.)
- Idea (#38): Brain dumps with pgvector embeddings
- Relationship (#39): Generic entity linking (blocks, depends_on)
- Agent (#40): ClawdBot agent tracking with metrics
- AgentSession (#40): Conversation session tracking
- WidgetDefinition (#41): HUD widget registry
- UserLayout (#41): Per-user dashboard configuration

Updated models:
- Task, Event, Project: Added domainId foreign key
- User, Workspace: Added new relations

New enums:
- IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED
- RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc.
- AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED
- EntityType: Added IDEA, DOMAIN

Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
Jason Woltje
2026-01-29 12:29:21 -06:00
parent a220c2dc0a
commit 973502f26e
308 changed files with 18374 additions and 113 deletions

View File

@@ -0,0 +1,111 @@
import { describe, it, expect } from "vitest";
import {
formatDate,
formatTime,
getDateGroupLabel,
isPastTarget,
isApproachingTarget,
} from "./date-format";
describe("date-format utils", () => {
describe("formatDate", () => {
it("should format date in readable format", () => {
// Use explicit time to avoid timezone issues
const date = new Date("2026-01-29T12:00:00");
const result = formatDate(date);
expect(result).toMatch(/Jan/);
expect(result).toMatch(/2026/);
// Note: Day might be 28 or 29 depending on timezone
expect(result).toMatch(/\d{1,2}/);
});
it("should handle invalid dates", () => {
const result = formatDate(new Date("invalid"));
expect(result).toBe("Invalid Date");
});
});
describe("formatTime", () => {
it("should format time in 12-hour format", () => {
const date = new Date("2026-01-29T14:30:00");
const result = formatTime(date);
expect(result).toMatch(/\d{1,2}:\d{2} [AP]M/i);
});
it("should handle invalid time", () => {
const result = formatTime(new Date("invalid"));
expect(result).toBe("Invalid Time");
});
});
describe("getDateGroupLabel", () => {
const today = new Date("2026-01-28T12:00:00");
it("should return 'Today' for today's date", () => {
const date = new Date("2026-01-28T14:00:00");
const result = getDateGroupLabel(date, today);
expect(result).toBe("Today");
});
it("should return 'Tomorrow' for tomorrow's date", () => {
const date = new Date("2026-01-29T10:00:00");
const result = getDateGroupLabel(date, today);
expect(result).toBe("Tomorrow");
});
it("should return 'This Week' for dates within 7 days", () => {
const date = new Date("2026-02-02T10:00:00");
const result = getDateGroupLabel(date, today);
expect(result).toBe("This Week");
});
it("should return 'Next Week' for dates 7-14 days out", () => {
const date = new Date("2026-02-08T10:00:00");
const result = getDateGroupLabel(date, today);
expect(result).toBe("Next Week");
});
it("should return 'Later' for dates beyond 2 weeks", () => {
const date = new Date("2026-03-15T10:00:00");
const result = getDateGroupLabel(date, today);
expect(result).toBe("Later");
});
});
describe("isPastTarget", () => {
const now = new Date("2026-01-28T12:00:00");
it("should return true for past dates", () => {
const pastDate = new Date("2026-01-27T10:00:00");
expect(isPastTarget(pastDate, now)).toBe(true);
});
it("should return false for future dates", () => {
const futureDate = new Date("2026-01-29T10:00:00");
expect(isPastTarget(futureDate, now)).toBe(false);
});
it("should return false for current time", () => {
expect(isPastTarget(now, now)).toBe(false);
});
});
describe("isApproachingTarget", () => {
const now = new Date("2026-01-28T12:00:00");
it("should return true for dates within 24 hours", () => {
const soonDate = new Date("2026-01-29T10:00:00");
expect(isApproachingTarget(soonDate, now)).toBe(true);
});
it("should return false for dates beyond 24 hours", () => {
const laterDate = new Date("2026-01-30T14:00:00");
expect(isApproachingTarget(laterDate, now)).toBe(false);
});
it("should return false for past dates", () => {
const pastDate = new Date("2026-01-27T10:00:00");
expect(isApproachingTarget(pastDate, now)).toBe(false);
});
});
});

View File

@@ -0,0 +1,69 @@
/**
* Date formatting utilities
* Provides PDA-friendly date formatting and grouping
*/
import { format, isToday, isTomorrow, differenceInDays, isBefore } from "date-fns";
/**
* Format a date in a readable format
*/
export function formatDate(date: Date): string {
try {
return format(date, "MMM d, yyyy");
} catch (error) {
return "Invalid Date";
}
}
/**
* Format time in 12-hour format
*/
export function formatTime(date: Date): string {
try {
return format(date, "h:mm a");
} catch (error) {
return "Invalid Time";
}
}
/**
* Get a PDA-friendly label for date grouping
* Returns: "Today", "Tomorrow", "This Week", "Next Week", "Later"
*/
export function getDateGroupLabel(date: Date, referenceDate: Date = new Date()): string {
if (isToday(date)) {
return "Today";
}
if (isTomorrow(date)) {
return "Tomorrow";
}
const daysUntil = differenceInDays(date, referenceDate);
if (daysUntil >= 0 && daysUntil <= 7) {
return "This Week";
}
if (daysUntil > 7 && daysUntil <= 14) {
return "Next Week";
}
return "Later";
}
/**
* Check if a date has passed (PDA-friendly: "target passed" instead of "overdue")
*/
export function isPastTarget(targetDate: Date, now: Date = new Date()): boolean {
return isBefore(targetDate, now);
}
/**
* Check if a date is approaching (within 24 hours)
*/
export function isApproachingTarget(targetDate: Date, now: Date = new Date()): boolean {
const hoursUntil = differenceInDays(targetDate, now);
return hoursUntil >= 0 && hoursUntil <= 1;
}