diff --git a/.claude/settings.json b/.claude/settings.json index 8f6349b5..e2e82f74 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -110,6 +110,7 @@ "code-review@claude-plugins-official": true, "commit-commands@claude-plugins-official": true, "supabase@claude-plugins-official": true, - "vercel@claude-plugins-official": true + "vercel@claude-plugins-official": true, + "code-simplifier@claude-plugins-official": true } } diff --git a/docs/PATTERNS.md b/docs/PATTERNS.md index e7c771e6..b639bfb0 100644 --- a/docs/PATTERNS.md +++ b/docs/PATTERNS.md @@ -19,6 +19,7 @@ trigger: always_on - Cached Data Fetching (React `cache()`) - [Mutations](./patterns/mutations.md) - Server Action + Zod Validation + Redirect + - Server Action Error Handling - [Authentication](./patterns/authentication.md) - Auth Check in Server Components - Auth Check in Server Actions @@ -39,6 +40,7 @@ trigger: always_on - [Testing Patterns](./patterns/testing-patterns.md) - Playwright E2E Tests - Integration Tests with PGlite + - Integration Tests with Module Mocking - Unit Tests - Vitest Deep Mocks (vitest-mock-extended) - [Logging](./patterns/logging.md) @@ -50,6 +52,14 @@ trigger: always_on - Consistent Site URL Resolution - [Database Migrations](./patterns/database-migrations.md) - Drizzle Migrations + Test Schema Export +- [Config-Driven Enums](./patterns/config-driven-enums.md) ⭐ **NEW** + - Single Source of Truth for Domain Enums + - Rich Metadata (labels, icons, styles, descriptions) +- [Service Layer Transactions](./patterns/service-layer-transactions.md) ⭐ **NEW** + - Transactional Updates with Timeline Events + - Notification Error Handling +- [Discriminated Union Props](./patterns/ui-patterns/discriminated-union-props.md) ⭐ **NEW** + - Type-Safe Component Props for Multi-Type Components ## Adding New Patterns diff --git a/docs/patterns/config-driven-enums.md b/docs/patterns/config-driven-enums.md new file mode 100644 index 00000000..dd377bd1 --- /dev/null +++ b/docs/patterns/config-driven-enums.md @@ -0,0 +1,169 @@ +# Config-Driven Enum with Rich Metadata + +**Status**: Established Pattern +**Version**: 1.0 +**Last Updated**: January 10, 2026 + +--- + +## Overview + +Use centralized configuration objects for domain enums that need UI metadata (labels, styles, icons, descriptions). This pattern provides type safety, runtime validation, and a single source of truth for all enum-related data. + +## Example: Issue Status System + +[src/lib/issues/status.ts](file:///home/froeht/Code/PinPoint/src/lib/issues/status.ts) + +## Pattern Structure + +```typescript +import { Icon1, Icon2 } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; + +// 1. Named constants for type-safe access and IDE autocomplete +export const MY_ENUM = { + VALUE_A: "value_a", + VALUE_B: "value_b", + VALUE_C: "value_c", +} as const; + +// 2. Runtime array for validation (Object.values) +export const ALL_MY_ENUM_VALUES = Object.values(MY_ENUM); + +// 3. Type-safe array with literal types (for Drizzle schema) +export const MY_ENUM_VALUES = ["value_a", "value_b", "value_c"] as const; + +// 4. Derive the canonical type from the const array +export type MyEnum = (typeof MY_ENUM_VALUES)[number]; + +// 5. Configuration object with all metadata +export const MY_ENUM_CONFIG: Record< + MyEnum, + { label: string; description: string; styles: string; icon: LucideIcon } +> = { + value_a: { + label: "Value A", + description: "Description for A", + styles: "bg-blue-500/20 text-blue-400 border-blue-500", + icon: Icon1, + }, + value_b: { + label: "Value B", + description: "Description for B", + styles: "bg-green-500/20 text-green-400 border-green-500", + icon: Icon2, + }, + value_c: { + label: "Value C", + description: "Description for C", + styles: "bg-red-500/20 text-red-400 border-red-500", + icon: Icon1, + }, +}; + +// 6. Getter functions for type-safe access +export function getMyEnumLabel(value: MyEnum): string { + return MY_ENUM_CONFIG[value].label; +} + +export function getMyEnumIcon(value: MyEnum): LucideIcon { + return MY_ENUM_CONFIG[value].icon; +} + +export function getMyEnumStyles(value: MyEnum): string { + return MY_ENUM_CONFIG[value].styles; +} + +// 7. Optional: Grouping exports +export const MY_ENUM_GROUPS = { + groupA: [MY_ENUM.VALUE_A, MY_ENUM.VALUE_B], + groupB: [MY_ENUM.VALUE_C], +} as const; +``` + +## Usage in Database Schema + +```typescript +// src/server/db/schema.ts +import { pgTable, text } from "drizzle-orm/pg-core"; +import { MY_ENUM_VALUES } from "~/lib/enums/my-enum"; + +export const myTable = pgTable("my_table", { + status: text("status", { + enum: MY_ENUM_VALUES as unknown as [string, ...string[]], + }) + .notNull() + .default("value_a"), +}); +``` + +## Usage in Zod Validation + +```typescript +// src/app/actions.ts +import { z } from "zod"; +import { MY_ENUM_VALUES } from "~/lib/enums/my-enum"; + +const mySchema = z.object({ + status: z.enum(MY_ENUM_VALUES), +}); +``` + +## Usage in Components + +```typescript +// src/components/MyBadge.tsx +import { getMyEnumLabel, getMyEnumIcon, getMyEnumStyles } from "~/lib/enums/my-enum"; +import type { MyEnum } from "~/lib/types"; +import { Badge } from "~/components/ui/badge"; + +export function MyBadge({ value }: { value: MyEnum }) { + const label = getMyEnumLabel(value); + const Icon = getMyEnumIcon(value); + const styles = getMyEnumStyles(value); + + return ( + + + {label} + + ); +} +``` + +## Benefits + +1. **Single Source of Truth**: All enum metadata in one place +2. **Type Safety**: TypeScript ensures all values have configuration +3. **Runtime Validation**: Can validate string inputs against `ALL_MY_ENUM_VALUES` +4. **IDE Autocomplete**: Named constants (`MY_ENUM.VALUE_A`) provide excellent DX +5. **UI Consistency**: Styles, labels, icons centrally managed +6. **Easy to Extend**: Add new value = add to array and config object +7. **No Component Logic**: Components delegate to getter functions + +## When to Use + +✅ Domain enums with UI metadata (status, severity, priority, etc.) +✅ Enums that appear in dropdowns/selects with labels +✅ Enums with associated colors, icons, or descriptions +✅ Enums that need grouping (e.g., "open" vs "closed" statuses) + +❌ Simple string unions without metadata (use plain type) +❌ Enums that never appear in UI (use plain const) + +## Related Patterns + +- [Discriminated Union Component Props](./ui-patterns/discriminated-union-props.md) +- [Database Schema Enums](./database-migrations.md#enum-types) +- [Form Validation with Zod](./mutations.md#zod-validation) + +## Real-World Example + +See [src/lib/issues/status.ts](file:///home/froeht/Code/PinPoint/src/lib/issues/status.ts) for full implementation of: + +- `IssueStatus` (11 values, 3 groups) +- `IssueSeverity` (4 values) +- `IssuePriority` (3 values) +- `IssueConsistency` (3 values) + +All with labels, descriptions, Tailwind styles, and Lucide icons. diff --git a/docs/patterns/service-layer-transactions.md b/docs/patterns/service-layer-transactions.md new file mode 100644 index 00000000..18bb5afb --- /dev/null +++ b/docs/patterns/service-layer-transactions.md @@ -0,0 +1,326 @@ +# Service Layer Transaction Pattern + +**Status**: Established Pattern +**Version**: 1.0 +**Last Updated**: January 10, 2026 + +--- + +## Overview + +Consistent structure for service layer functions that perform database updates with transactions, timeline events, and notifications. + +## Pattern Structure + +```typescript +import { db } from "~/server/db"; +import { createTimelineEvent } from "~/lib/timeline/events"; +import { createNotification } from "~/lib/notifications"; +import { log } from "~/lib/logger"; + +export async function updateSomething({ + id, + newValue, + userId, +}: { + id: string; + newValue: string; + userId: string; +}): Promise<{ id: string; oldValue: string; newValue: string }> { + return await db.transaction(async (tx) => { + // 1. Get current state (with needed relations) + const current = await tx.query.myTable.findFirst({ + where: eq(myTable.id, id), + columns: { value: true /* other needed fields */ }, + with: { relatedEntity: true }, + }); + + if (!current) { + throw new Error("Entity not found"); + } + + const oldValue = current.value; + + // 2. Perform update + await tx + .update(myTable) + .set({ + value: newValue, + updatedAt: new Date(), + }) + .where(eq(myTable.id, id)); + + // 3. Create timeline event (inside transaction for atomicity) + await createTimelineEvent( + id, + `Value changed from ${oldValue} to ${newValue}`, + tx + ); + + // 4. Structured logging + log.info( + { + id, + oldValue, + newValue, + userId, + action: "updateSomething", + }, + "Entity value updated" + ); + + // 5. Trigger notifications (with error handling) + try { + await createNotification( + { + type: "value_changed", + resourceId: id, + resourceType: "entity", + actorId: userId, + // ... notification metadata ... + }, + tx + ); + } catch (error) { + log.error( + { error, action: "updateSomething.notifications" }, + "Failed to send notification" + ); + // Don't fail the transaction if notifications fail + } + + // 6. Return explicit result + return { id, oldValue, newValue }; + }); +} +``` + +## Key Principles + +### 1. Transaction Boundaries + +**✅ Inside Transaction**: + +- Database updates +- Timeline events (for atomicity) +- Related entity updates + +**✅ Outside Transaction** (or with error handling): + +- External notifications (email, webhooks) +- Non-critical side effects +- Cache invalidation + +### 2. Timeline Events + +Always create timeline events for audit trails: + +```typescript +await createTimelineEvent( + entityId, + `Human-readable description of change`, + tx // Pass transaction for atomicity +); +``` + +### 3. Notification Error Handling + +Notifications should never cause transaction rollback: + +```typescript +try { + await createNotification(/*...*/); +} catch (error) { + log.error( + { error, action: "functionName.notifications" }, + "Failed to send notification" + ); + // Don't re-throw - let transaction complete +} +``` + +### 4. Structured Logging + +Use consistent log structure: + +```typescript +log.info( + { + entityId, + oldValue, + newValue, + userId, + action: "functionName", // Consistent naming + }, + "Human-readable summary" +); +``` + +### 5. Explicit Return Types + +Always specify return type for clarity: + +```typescript +export async function updateSomething({...}): Promise<{ + entityId: string; + oldValue: string; + newValue: string; +}> { + // ... +} +``` + +## Real-World Examples + +### Update Issue Status + +[src/services/issues.ts:212-299](file:///home/froeht/Code/PinPoint/src/services/issues.ts#L212-L299) + +```typescript +export async function updateIssueStatus({ + issueId, + status, + userId, +}: UpdateIssueStatusParams): Promise<{ + issueId: string; + oldStatus: string; + newStatus: string; +}> { + return await db.transaction(async (tx) => { + // Get current state + const currentIssue = await tx.query.issues.findFirst({ + where: eq(issues.id, issueId), + with: { machine: true }, + }); + + if (!currentIssue) throw new Error("Issue not found"); + + const oldStatus = currentIssue.status; + const isClosed = CLOSED_STATUSES.includes(status); + + // Update + await tx + .update(issues) + .set({ + status, + updatedAt: new Date(), + closedAt: isClosed ? new Date() : null, + }) + .where(eq(issues.id, issueId)); + + // Timeline event + const oldLabel = getIssueStatusLabel(oldStatus); + const newLabel = getIssueStatusLabel(status); + await createTimelineEvent( + issueId, + `Status changed from ${oldLabel} to ${newLabel}`, + tx + ); + + // Logging + log.info({ issueId, oldStatus, newStatus: status }, "Issue status updated"); + + // Notifications (with error handling) + try { + await createNotification( + { + /*...*/ + }, + tx + ); + } catch (error) { + log.error({ error }, "Failed to send notification"); + } + + return { issueId, oldStatus, newStatus: status }; + }); +} +``` + +### Create Issue with Auto-Watchers + +[src/services/issues.ts:84-206](file:///home/froeht/Code/PinPoint/src/services/issues.ts#L84-L206) + +Demonstrates: + +- Sequential number generation (atomic) +- Multiple insert operations +- Conditional watcher logic +- Notification creation + +## Anti-Patterns + +### ❌ Missing Transaction + +```typescript +// BAD: No transaction, non-atomic +export async function updateValue(id, newValue) { + await db.update(myTable).set({ value: newValue }); + await createTimelineEvent(id, "changed"); // Could fail after update +} +``` + +### ❌ Failing on Notification Error + +```typescript +// BAD: Notification failure causes rollback +export async function updateValue(id, newValue) { + return await db.transaction(async (tx) => { + await tx.update(myTable).set({ value: newValue }); + await createNotification({ + /*...*/ + }); // If this fails, update rolls back + }); +} +``` + +### ❌ Missing Logging + +```typescript +// BAD: No audit trail +export async function updateValue(id, newValue) { + return await db.transaction(async (tx) => { + await tx.update(myTable).set({ value: newValue }); + // No log.info() - hard to debug issues + }); +} +``` + +### ❌ Implicit Return Type + +```typescript +// BAD: Return type unclear +export async function updateValue(id, newValue) { + return await db.transaction(async (tx) => { + const result = await tx.update(/*...*/).returning(); + return result[0]; // What shape is this? + }); +} +``` + +## Checklist + +Use this checklist for all service layer update functions: + +- [ ] Wrapped in `db.transaction()` +- [ ] Current state fetched within transaction +- [ ] Null/error checks for entity not found +- [ ] Update operation performed +- [ ] Timeline event created (inside transaction) +- [ ] Structured logging added +- [ ] Notifications wrapped in try/catch +- [ ] Explicit return type specified +- [ ] Integration test written + +## Related Patterns + +- [Server Actions with Error Handling](./mutations.md#error-handling) +- [Timeline Events](./timeline-events.md) +- [Structured Logging](./logging.md) +- [Integration Testing](./testing-patterns.md#integration-tests) + +## References + +- [src/services/issues.ts](file:///home/froeht/Code/PinPoint/src/services/issues.ts) - Full implementation +- [CORE-PERF-001](file:///home/froeht/Code/PinPoint/docs/NON_NEGOTIABLES.md#performance--caching) - Cache service fetchers +- [Drizzle Transactions Docs](https://orm.drizzle.team/docs/transactions) diff --git a/docs/patterns/testing-patterns.md b/docs/patterns/testing-patterns.md index 08dec826..e4db8af8 100644 --- a/docs/patterns/testing-patterns.md +++ b/docs/patterns/testing-patterns.md @@ -72,6 +72,99 @@ describe("Machine CRUD Operations (PGlite)", () => { - Test database operations, not Server Components - Integration tests in `src/test/integration/` +### Integration Tests with Module Mocking + +For testing service layer functions that import `~/server/db`, use module mocking to redirect to PGlite: + +```typescript +// src/test/integration/supabase/issue-services.test.ts +import { describe, it, expect, beforeEach, vi, beforeAll } from "vitest"; +import { getTestDb, setupTestDb } from "~/test/setup/pglite"; +import { updateIssueStatus } from "~/services/issues"; + +// Mock the database module to use PGlite instance +vi.mock("~/server/db", () => ({ + db: { + insert: vi.fn((...args: any[]) => + (globalThis as any).testDb.insert(...args) + ), + update: vi.fn((...args: any[]) => + (globalThis as any).testDb.update(...args) + ), + query: { + issues: { + findFirst: vi.fn((...args: any[]) => + (globalThis as any).testDb.query.issues.findFirst(...args) + ), + }, + }, + transaction: vi.fn((cb: any) => cb((globalThis as any).testDb)), + }, +})); + +describe("Issue Service Functions", () => { + setupTestDb(); + + let testIssue: any; + + beforeAll(async () => { + (globalThis as any).testDb = await getTestDb(); + }); + + beforeEach(async () => { + const db = await getTestDb(); + // Set up test data + const [issue] = await db + .insert(issues) + .values({ + /*...*/ + }) + .returning(); + testIssue = issue; + }); + + it("should update status and create timeline event", async () => { + await updateIssueStatus({ + issueId: testIssue.id, + status: "in_progress", + userId: "test-user", + }); + + const db = await getTestDb(); + const updated = await db.query.issues.findFirst({ + where: eq(issues.id, testIssue.id), + }); + + expect(updated?.status).toBe("in_progress"); + + // Verify timeline event was created + const events = await db.query.issueComments.findMany({ + where: eq(issueComments.issueId, testIssue.id), + }); + expect(events.some((e) => e.content.includes("Status changed"))).toBe(true); + }); +}); +``` + +**Key points**: + +- Mock `~/server/db` module at file level with `vi.mock()` +- Forward all calls to `(globalThis as any).testDb` +- Set `globalThis.testDb` in `beforeAll` +- Allows testing actual service functions (not mocked logic) +- Verifies transactions and side effects (timeline events, notifications) +- **Use `any` types in mock setup** (acceptable for test infrastructure) + +**When to use**: + +✅ Testing service layer functions that import `db` from `~/server/db` +✅ Verifying transaction behavior +✅ Testing timeline events and notifications +✅ Integration tests that need full database interaction + +❌ Don't mock individual database methods (prefer this full module mock) +❌ Don't use for unit tests (those should test pure functions) + ## Unit Tests ```typescript diff --git a/docs/patterns/ui-patterns/discriminated-union-props.md b/docs/patterns/ui-patterns/discriminated-union-props.md new file mode 100644 index 00000000..858987b3 --- /dev/null +++ b/docs/patterns/ui-patterns/discriminated-union-props.md @@ -0,0 +1,249 @@ +# Discriminated Union Component Props + +**Status**: Established Pattern +**Version**: 1.0 +**Last Updated**: January 10, 2026 + +--- + +## Overview + +Use TypeScript discriminated unions for component props when a single component handles multiple related types. This ensures type safety by linking the `type` prop to the correct `value` type. + +## Pattern Structure + +```typescript +// Base props shared across all variants +type BaseProps = { + variant?: "normal" | "compact"; + className?: string; + showTooltip?: boolean; +}; + +// Discriminated union for type-specific props +type ComponentProps = BaseProps & + ( + | { type: "status"; value: StatusEnum } + | { type: "priority"; value: PriorityEnum } + | { type: "severity"; value: SeverityEnum } + ); + +export function MyComponent({ + type, + value, + variant, + className, +}: ComponentProps) { + // TypeScript knows value type based on type prop + // e.g., if type is "status", value must be StatusEnum +} +``` + +## Real-World Example: IssueBadge + +[src/components/issues/IssueBadge.tsx](file:///home/froeht/Code/PinPoint/src/components/issues/IssueBadge.tsx) + +```typescript +type IssueBadgeProps = { + variant?: "normal" | "strip"; + className?: string; + showTooltip?: boolean; +} & ( + | { type: "status"; value: IssueStatus } + | { type: "severity"; value: IssueSeverity } + | { type: "priority"; value: IssuePriority } + | { type: "consistency"; value: IssueConsistency } +); + +export function IssueBadge({ type, value, variant, className, showTooltip }: IssueBadgeProps) { + let label = ""; + let styles = ""; + let Icon: React.ElementType | null = null; + + // Switch on discriminant to get correct config + switch (type) { + case "status": + label = getIssueStatusLabel(value); // value is IssueStatus + styles = STATUS_STYLES[value]; + Icon = getIssueStatusIcon(value); + break; + case "severity": + label = getIssueSeverityLabel(value); // value is IssueSeverity + styles = SEVERITY_STYLES[value]; + Icon = ISSUE_FIELD_ICONS.severity; + break; + // ... other cases + } + + return ( + + + {label} + + ); +} +``` + +## Usage + +```tsx +// ✅ Correct: Type and value match + + + +// ❌ TypeScript error: value doesn't match type + // Error: "major" is not IssueStatus + // Error: "new" is not IssueSeverity +``` + +## Benefits + +1. **Type Safety**: TypeScript enforces that `value` matches `type` +2. **Single Component**: One component handles multiple related types +3. **Exhaustive Checking**: Switch statements ensure all types are handled +4. **Clear API**: Users can't pass invalid type/value combinations +5. **Good DX**: IDE autocomplete shows correct values for each type + +## When to Use + +✅ Component renders different content based on a `type` prop +✅ Each type requires a different value type +✅ Types share common rendering logic (colors, icons, labels) +✅ Types are conceptually related (all badges, all inputs, etc.) + +❌ Types have completely different rendering logic (use separate components) +❌ Only one type exists (use simple props) +❌ Types don't share any props (use separate components) + +## Pattern Variations + +### With Optional Discriminant + +```typescript +type Props = { + value: string; +} & ( + | { showIcon: true; icon: LucideIcon } + | { showIcon?: false; icon?: never } +); + +// ✅ If showIcon is true, icon is required + + +// ✅ If showIcon is false/undefined, icon is not allowed + +``` + +### With Nested Discriminants + +```typescript +type Props = + | { type: "text"; value: string; placeholder?: string } + | { type: "number"; value: number; min?: number; max?: number } + | { + type: "select"; + value: string; + options: Array<{ label: string; value: string }>; + }; +``` + +## Complementary Patterns + +Discriminated union props work well with: + +- [Config-Driven Enums](./config-driven-enums.md) - Use enum configs to get metadata +- [Pick\u003cType, Keys\u003e](./type-boundaries.md) - Narrow prop types for grid/list components + +## Real-World Examples + +### IssueBadge + +[src/components/issues/IssueBadge.tsx](file:///home/froeht/Code/PinPoint/src/components/issues/IssueBadge.tsx) + +Single component handles 4 badge types (status, severity, priority, consistency) with type-safe values. + +### IssueBadgeGrid + +[src/components/issues/IssueBadgeGrid.tsx](file:///home/froeht/Code/PinPoint/src/components/issues/IssueBadgeGrid.tsx) + +Uses `Pick` to accept minimal props and composes `IssueBadge` components. + +## Anti-Patterns + +### ❌ String Union Without Discrimination + +```typescript +// BAD: No type safety between category and value +type Props = { + category: "status" | "severity"; + value: string; // Could be anything! +}; +``` + +### ❌ Separate Boolean Props + +```typescript +// BAD: Can set multiple booleans to true +type Props = { + isStatus?: boolean; + isSeverity?: boolean; + value: string; +}; +``` + +### ❌ Any Type Value + +```typescript +// BAD: Loses type safety +type Props = { + type: "status" | "severity"; + value: any; // Defeats the purpose! +}; +``` + +## TypeScript Tips + +### Narrowing in Switch + +```typescript +function MyComponent(props: ComponentProps) { + switch (props.type) { + case "status": + // TypeScript knows props.value is IssueStatus here + const label = getIssueStatusLabel(props.value); + break; + case "severity": + // TypeScript knows props.value is IssueSeverity here + const label = getIssueSeverityLabel(props.value); + break; + } +} +``` + +### Exhaustive Checking + +```typescript +function MyComponent(props: ComponentProps) { + switch (props.type) { + case "status": + return ; + case "severity": + return ; + default: + // TypeScript error if we miss a case + const exhaustive: never = props.type; + throw new Error(`Unhandled type: ${exhaustive}`); + } +} +``` + +## Related Patterns + +- [Config-Driven Enums](./config-driven-enums.md) +- [Type Boundaries](./type-boundaries.md) +- [Component Composition](./ui-patterns/component-composition.md) + +## References + +- [TypeScript Handbook: Discriminated Unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) +- [src/components/issues/IssueBadge.tsx](file:///home/froeht/Code/PinPoint/src/components/issues/IssueBadge.tsx) diff --git a/drizzle/0000_init-schema.sql b/drizzle/0000_initv2.sql similarity index 67% rename from drizzle/0000_init-schema.sql rename to drizzle/0000_initv2.sql index e24a2181..35e9eb74 100644 --- a/drizzle/0000_init-schema.sql +++ b/drizzle/0000_initv2.sql @@ -1,4 +1,3 @@ - CREATE TABLE "issue_comments" ( "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "issue_id" uuid NOT NULL, @@ -22,14 +21,17 @@ CREATE TABLE "issues" ( "title" text NOT NULL, "description" text, "status" text DEFAULT 'new' NOT NULL, - "severity" text DEFAULT 'playable' NOT NULL, - "priority" text DEFAULT 'low' NOT NULL, + "severity" text DEFAULT 'minor' NOT NULL, + "priority" text DEFAULT 'medium' NOT NULL, + "consistency" text DEFAULT 'intermittent' NOT NULL, "reported_by" uuid, + "unconfirmed_reported_by" uuid, "assigned_to" uuid, - "resolved_at" timestamp with time zone, + "closed_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, - CONSTRAINT "unique_issue_number" UNIQUE("machine_initials","issue_number") + CONSTRAINT "unique_issue_number" UNIQUE("machine_initials","issue_number"), + CONSTRAINT "reporter_check" CHECK ((reported_by IS NULL OR unconfirmed_reported_by IS NULL)) ); --> statement-breakpoint CREATE TABLE "machines" ( @@ -38,10 +40,12 @@ CREATE TABLE "machines" ( "next_issue_number" integer DEFAULT 1 NOT NULL, "name" text NOT NULL, "owner_id" uuid, + "unconfirmed_owner_id" uuid, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "machines_initials_unique" UNIQUE("initials"), - CONSTRAINT "initials_check" CHECK (initials ~ '^[A-Z0-9]{2,6}$') + CONSTRAINT "initials_check" CHECK (initials ~ '^[A-Z0-9]{2,6}$'), + CONSTRAINT "owner_check" CHECK ((owner_id IS NULL OR unconfirmed_owner_id IS NULL)) ); --> statement-breakpoint CREATE TABLE "notification_preferences" ( @@ -70,15 +74,29 @@ CREATE TABLE "notifications" ( "created_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint +CREATE TABLE "unconfirmed_users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "first_name" text NOT NULL, + "last_name" text NOT NULL, + "name" text GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED NOT NULL, + "email" text NOT NULL, + "role" text DEFAULT 'guest' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "invite_sent_at" timestamp with time zone, + CONSTRAINT "unconfirmed_users_email_unique" UNIQUE("email") +); +--> statement-breakpoint CREATE TABLE "user_profiles" ( "id" uuid PRIMARY KEY NOT NULL, + "email" text NOT NULL, "first_name" text NOT NULL, "last_name" text NOT NULL, "name" text GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED NOT NULL, "avatar_url" text, "role" text DEFAULT 'member' NOT NULL, "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "user_profiles_email_unique" UNIQUE("email") ); --> statement-breakpoint ALTER TABLE "issue_comments" ADD CONSTRAINT "issue_comments_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint @@ -87,10 +105,21 @@ ALTER TABLE "issue_watchers" ADD CONSTRAINT "issue_watchers_issue_id_issues_id_f ALTER TABLE "issue_watchers" ADD CONSTRAINT "issue_watchers_user_id_user_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "issues" ADD CONSTRAINT "issues_machine_initials_machines_initials_fk" FOREIGN KEY ("machine_initials") REFERENCES "public"."machines"("initials") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "issues" ADD CONSTRAINT "issues_reported_by_user_profiles_id_fk" FOREIGN KEY ("reported_by") REFERENCES "public"."user_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "issues" ADD CONSTRAINT "issues_unconfirmed_reported_by_unconfirmed_users_id_fk" FOREIGN KEY ("unconfirmed_reported_by") REFERENCES "public"."unconfirmed_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "issues" ADD CONSTRAINT "issues_assigned_to_user_profiles_id_fk" FOREIGN KEY ("assigned_to") REFERENCES "public"."user_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "machines" ADD CONSTRAINT "machines_owner_id_user_profiles_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."user_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "machines" ADD CONSTRAINT "machines_unconfirmed_owner_id_unconfirmed_users_id_fk" FOREIGN KEY ("unconfirmed_owner_id") REFERENCES "public"."unconfirmed_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "notification_preferences" ADD CONSTRAINT "notification_preferences_user_id_user_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_user_profiles_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user_profiles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -CREATE INDEX "idx_issue_watchers_issue_id" ON "issue_watchers" USING btree ("issue_id");--> statement-breakpoint +CREATE INDEX "idx_issue_comments_issue_id" ON "issue_comments" USING btree ("issue_id");--> statement-breakpoint +CREATE INDEX "idx_issue_comments_author_id" ON "issue_comments" USING btree ("author_id");--> statement-breakpoint +CREATE INDEX "idx_issue_watchers_user_id" ON "issue_watchers" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "idx_issues_assigned_to" ON "issues" USING btree ("assigned_to");--> statement-breakpoint +CREATE INDEX "idx_issues_reported_by" ON "issues" USING btree ("reported_by");--> statement-breakpoint +CREATE INDEX "idx_issues_status" ON "issues" USING btree ("status");--> statement-breakpoint +CREATE INDEX "idx_issues_created_at" ON "issues" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "idx_issues_unconfirmed_reported_by" ON "issues" USING btree ("unconfirmed_reported_by");--> statement-breakpoint +CREATE INDEX "idx_machines_owner_id" ON "machines" USING btree ("owner_id");--> statement-breakpoint +CREATE INDEX "idx_machines_unconfirmed_owner_id" ON "machines" USING btree ("unconfirmed_owner_id");--> statement-breakpoint CREATE INDEX "idx_notif_prefs_global_watch_email" ON "notification_preferences" USING btree ("email_watch_new_issues_global");--> statement-breakpoint CREATE INDEX "idx_notifications_user_unread" ON "notifications" USING btree ("user_id","read_at","created_at"); diff --git a/drizzle/0001_add_performance_indexes.sql b/drizzle/0001_add_performance_indexes.sql deleted file mode 100644 index c70896b0..00000000 --- a/drizzle/0001_add_performance_indexes.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX "idx_issue_comments_issue_id" ON "issue_comments" USING btree ("issue_id");--> statement-breakpoint -CREATE INDEX "idx_issues_assigned_to" ON "issues" USING btree ("assigned_to");--> statement-breakpoint -CREATE INDEX "idx_issues_reported_by" ON "issues" USING btree ("reported_by"); \ No newline at end of file diff --git a/drizzle/0002_add_more_performance_indexes.sql b/drizzle/0002_add_more_performance_indexes.sql deleted file mode 100644 index be878c65..00000000 --- a/drizzle/0002_add_more_performance_indexes.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE INDEX "idx_issues_status" ON "issues" USING btree ("status");--> statement-breakpoint -CREATE INDEX "idx_machines_owner_id" ON "machines" USING btree ("owner_id"); \ No newline at end of file diff --git a/drizzle/0003_add_unconfirmed_users.sql b/drizzle/0003_add_unconfirmed_users.sql deleted file mode 100644 index c341422e..00000000 --- a/drizzle/0003_add_unconfirmed_users.sql +++ /dev/null @@ -1,51 +0,0 @@ -CREATE TABLE "unconfirmed_users" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "first_name" text NOT NULL, - "last_name" text NOT NULL, - "name" text GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED NOT NULL, - "email" text NOT NULL, - "role" text DEFAULT 'guest' NOT NULL, - "created_at" timestamp with time zone DEFAULT now() NOT NULL, - "invite_sent_at" timestamp with time zone, - CONSTRAINT "unconfirmed_users_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "issues" ADD COLUMN "unconfirmed_reported_by" uuid;--> statement-breakpoint -ALTER TABLE "machines" ADD COLUMN "unconfirmed_owner_id" uuid;--> statement-breakpoint -ALTER TABLE "issues" ADD CONSTRAINT "issues_unconfirmed_reported_by_unconfirmed_users_id_fk" FOREIGN KEY ("unconfirmed_reported_by") REFERENCES "public"."unconfirmed_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "machines" ADD CONSTRAINT "machines_unconfirmed_owner_id_unconfirmed_users_id_fk" FOREIGN KEY ("unconfirmed_owner_id") REFERENCES "public"."unconfirmed_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "issues" ADD CONSTRAINT "reporter_check" CHECK ((reported_by IS NULL OR unconfirmed_reported_by IS NULL));--> statement-breakpoint -ALTER TABLE "machines" ADD CONSTRAINT "owner_check" CHECK ((owner_id IS NULL OR unconfirmed_owner_id IS NULL)); --- Custom trigger to auto-link machines and issues when unconfirmed user signs up -CREATE OR REPLACE FUNCTION public.handle_unconfirmed_user_signup() -RETURNS TRIGGER AS $$ -BEGIN - -- Update machines where the unconfirmed user was the owner - UPDATE public.machines - SET - owner_id = NEW.id, - unconfirmed_owner_id = NULL - WHERE unconfirmed_owner_id = ( - SELECT id FROM public.unconfirmed_users WHERE email = NEW.email - ); - - -- Update issues where the unconfirmed user was the reporter - UPDATE public.issues - SET - reported_by = NEW.id, - unconfirmed_reported_by = NULL - WHERE unconfirmed_reported_by = ( - SELECT id FROM public.unconfirmed_users WHERE email = NEW.email - ); - - -- Delete the unconfirmed user record - DELETE FROM public.unconfirmed_users WHERE email = NEW.email; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE TRIGGER on_unconfirmed_user_signup - AFTER INSERT ON public.user_profiles - FOR EACH ROW - EXECUTE FUNCTION public.handle_unconfirmed_user_signup(); diff --git a/drizzle/0004_whole_kate_bishop.sql b/drizzle/0004_whole_kate_bishop.sql deleted file mode 100644 index 5d1ba3ac..00000000 --- a/drizzle/0004_whole_kate_bishop.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP INDEX "idx_issue_watchers_issue_id";--> statement-breakpoint -CREATE INDEX "idx_issue_comments_author_id" ON "issue_comments" USING btree ("author_id");--> statement-breakpoint -CREATE INDEX "idx_issue_watchers_user_id" ON "issue_watchers" USING btree ("user_id"); \ No newline at end of file diff --git a/drizzle/0005_add_email_to_profiles.sql b/drizzle/0005_add_email_to_profiles.sql deleted file mode 100644 index 76dbcca9..00000000 --- a/drizzle/0005_add_email_to_profiles.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Step 1: Add column as nullable -ALTER TABLE "user_profiles" ADD COLUMN "email" text; - --- Step 2: Backfill data from auth.users --- Note: This is an internal Supabase migration pattern. We use the join on id. -UPDATE "user_profiles" SET "email" = (SELECT email FROM auth.users WHERE auth.users.id = "user_profiles".id); - --- Step 3: Add NOT NULL and UNIQUE constraints -ALTER TABLE "user_profiles" ALTER COLUMN "email" SET NOT NULL; -ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_email_unique" UNIQUE("email"); diff --git a/drizzle/0006_add_performance_indexes.sql b/drizzle/0006_add_performance_indexes.sql deleted file mode 100644 index dc7e90d7..00000000 --- a/drizzle/0006_add_performance_indexes.sql +++ /dev/null @@ -1,3 +0,0 @@ -CREATE INDEX "idx_issues_created_at" ON "issues" USING btree ("created_at");--> statement-breakpoint -CREATE INDEX "idx_issues_unconfirmed_reported_by" ON "issues" USING btree ("unconfirmed_reported_by");--> statement-breakpoint -CREATE INDEX "idx_machines_unconfirmed_owner_id" ON "machines" USING btree ("unconfirmed_owner_id"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index def921aa..06564589 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "37e0cbd4-92a2-4504-9431-7979c4fe6eda", + "id": "9419c8f2-2c0d-41ab-ac7c-16230472cec9", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -80,7 +80,38 @@ "default": "now()" } }, - "indexes": {}, + "indexes": { + "idx_issue_comments_issue_id": { + "name": "idx_issue_comments_issue_id", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issue_comments_author_id": { + "name": "idx_issue_comments_author_id", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, "foreignKeys": { "issue_comments_issue_id_issues_id_fk": { "name": "issue_comments_issue_id_issues_id_fk", @@ -125,11 +156,11 @@ } }, "indexes": { - "idx_issue_watchers_issue_id": { - "name": "idx_issue_watchers_issue_id", + "idx_issue_watchers_user_id": { + "name": "idx_issue_watchers_user_id", "columns": [ { - "expression": "issue_id", + "expression": "user_id", "isExpression": false, "asc": true, "nulls": "last" @@ -219,14 +250,21 @@ "type": "text", "primaryKey": false, "notNull": true, - "default": "'playable'" + "default": "'minor'" }, "priority": { "name": "priority", "type": "text", "primaryKey": false, "notNull": true, - "default": "'low'" + "default": "'medium'" + }, + "consistency": { + "name": "consistency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'intermittent'" }, "reported_by": { "name": "reported_by", @@ -234,14 +272,20 @@ "primaryKey": false, "notNull": false }, + "unconfirmed_reported_by": { + "name": "unconfirmed_reported_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "assigned_to": { "name": "assigned_to", "type": "uuid", "primaryKey": false, "notNull": false }, - "resolved_at": { - "name": "resolved_at", + "closed_at": { + "name": "closed_at", "type": "timestamp with time zone", "primaryKey": false, "notNull": false @@ -261,7 +305,83 @@ "default": "now()" } }, - "indexes": {}, + "indexes": { + "idx_issues_assigned_to": { + "name": "idx_issues_assigned_to", + "columns": [ + { + "expression": "assigned_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issues_reported_by": { + "name": "idx_issues_reported_by", + "columns": [ + { + "expression": "reported_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issues_status": { + "name": "idx_issues_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issues_created_at": { + "name": "idx_issues_created_at", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_issues_unconfirmed_reported_by": { + "name": "idx_issues_unconfirmed_reported_by", + "columns": [ + { + "expression": "unconfirmed_reported_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, "foreignKeys": { "issues_machine_initials_machines_initials_fk": { "name": "issues_machine_initials_machines_initials_fk", @@ -281,6 +401,15 @@ "onDelete": "no action", "onUpdate": "no action" }, + "issues_unconfirmed_reported_by_unconfirmed_users_id_fk": { + "name": "issues_unconfirmed_reported_by_unconfirmed_users_id_fk", + "tableFrom": "issues", + "tableTo": "unconfirmed_users", + "columnsFrom": ["unconfirmed_reported_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, "issues_assigned_to_user_profiles_id_fk": { "name": "issues_assigned_to_user_profiles_id_fk", "tableFrom": "issues", @@ -300,7 +429,12 @@ } }, "policies": {}, - "checkConstraints": {}, + "checkConstraints": { + "reporter_check": { + "name": "reporter_check", + "value": "(reported_by IS NULL OR unconfirmed_reported_by IS NULL)" + } + }, "isRLSEnabled": false }, "public.machines": { @@ -339,6 +473,12 @@ "primaryKey": false, "notNull": false }, + "unconfirmed_owner_id": { + "name": "unconfirmed_owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, "created_at": { "name": "created_at", "type": "timestamp with time zone", @@ -354,7 +494,38 @@ "default": "now()" } }, - "indexes": {}, + "indexes": { + "idx_machines_owner_id": { + "name": "idx_machines_owner_id", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_machines_unconfirmed_owner_id": { + "name": "idx_machines_unconfirmed_owner_id", + "columns": [ + { + "expression": "unconfirmed_owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, "foreignKeys": { "machines_owner_id_user_profiles_id_fk": { "name": "machines_owner_id_user_profiles_id_fk", @@ -364,6 +535,15 @@ "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" + }, + "machines_unconfirmed_owner_id_unconfirmed_users_id_fk": { + "name": "machines_unconfirmed_owner_id_unconfirmed_users_id_fk", + "tableFrom": "machines", + "tableTo": "unconfirmed_users", + "columnsFrom": ["unconfirmed_owner_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -379,6 +559,10 @@ "initials_check": { "name": "initials_check", "value": "initials ~ '^[A-Z0-9]{2,6}$'" + }, + "owner_check": { + "name": "owner_check", + "value": "(owner_id IS NULL OR unconfirmed_owner_id IS NULL)" } }, "isRLSEnabled": false @@ -607,6 +791,80 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.unconfirmed_users": { + "name": "unconfirmed_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "generated": { + "as": "first_name || ' ' || last_name", + "type": "stored" + } + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'guest'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invite_sent_at": { + "name": "invite_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unconfirmed_users_email_unique": { + "name": "unconfirmed_users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.user_profiles": { "name": "user_profiles", "schema": "", @@ -617,6 +875,12 @@ "primaryKey": true, "notNull": true }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, "first_name": { "name": "first_name", "type": "text", @@ -670,7 +934,13 @@ "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, - "uniqueConstraints": {}, + "uniqueConstraints": { + "user_profiles_email_unique": { + "name": "user_profiles_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 285e0357..00000000 --- a/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,737 +0,0 @@ -{ - "id": "277d032f-4a11-401a-a0cf-56f0e64df34f", - "prevId": "37e0cbd4-92a2-4504-9431-7979c4fe6eda", - "version": "7", - "dialect": "postgresql", - "tables": { - "auth.users": { - "name": "users", - "schema": "auth", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_comments": { - "name": "issue_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "author_id": { - "name": "author_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "name": "is_system", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issue_comments_issue_id": { - "name": "idx_issue_comments_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_comments_issue_id_issues_id_fk": { - "name": "issue_comments_issue_id_issues_id_fk", - "tableFrom": "issue_comments", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_comments_author_id_user_profiles_id_fk": { - "name": "issue_comments_author_id_user_profiles_id_fk", - "tableFrom": "issue_comments", - "tableTo": "user_profiles", - "columnsFrom": ["author_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_watchers": { - "name": "issue_watchers", - "schema": "", - "columns": { - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_issue_watchers_issue_id": { - "name": "idx_issue_watchers_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_watchers_issue_id_issues_id_fk": { - "name": "issue_watchers_issue_id_issues_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_watchers_user_id_user_profiles_id_fk": { - "name": "issue_watchers_user_id_user_profiles_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "issue_watchers_issue_id_user_id_pk": { - "name": "issue_watchers_issue_id_user_id_pk", - "columns": ["issue_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issues": { - "name": "issues", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "machine_initials": { - "name": "machine_initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "issue_number": { - "name": "issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'new'" - }, - "severity": { - "name": "severity", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'playable'" - }, - "priority": { - "name": "priority", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'low'" - }, - "reported_by": { - "name": "reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "assigned_to": { - "name": "assigned_to", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issues_assigned_to": { - "name": "idx_issues_assigned_to", - "columns": [ - { - "expression": "assigned_to", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_reported_by": { - "name": "idx_issues_reported_by", - "columns": [ - { - "expression": "reported_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issues_machine_initials_machines_initials_fk": { - "name": "issues_machine_initials_machines_initials_fk", - "tableFrom": "issues", - "tableTo": "machines", - "columnsFrom": ["machine_initials"], - "columnsTo": ["initials"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issues_reported_by_user_profiles_id_fk": { - "name": "issues_reported_by_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_assigned_to_user_profiles_id_fk": { - "name": "issues_assigned_to_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["assigned_to"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_issue_number": { - "name": "unique_issue_number", - "nullsNotDistinct": false, - "columns": ["machine_initials", "issue_number"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.machines": { - "name": "machines", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "initials": { - "name": "initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "next_issue_number": { - "name": "next_issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "owner_id": { - "name": "owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "machines_owner_id_user_profiles_id_fk": { - "name": "machines_owner_id_user_profiles_id_fk", - "tableFrom": "machines", - "tableTo": "user_profiles", - "columnsFrom": ["owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "machines_initials_unique": { - "name": "machines_initials_unique", - "nullsNotDistinct": false, - "columns": ["initials"] - } - }, - "policies": {}, - "checkConstraints": { - "initials_check": { - "name": "initials_check", - "value": "initials ~ '^[A-Z0-9]{2,6}$'" - } - }, - "isRLSEnabled": false - }, - "public.notification_preferences": { - "name": "notification_preferences", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email_enabled": { - "name": "email_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_enabled": { - "name": "in_app_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_assigned": { - "name": "email_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_assigned": { - "name": "in_app_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_status_change": { - "name": "email_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_status_change": { - "name": "in_app_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_comment": { - "name": "email_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_comment": { - "name": "in_app_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_issue": { - "name": "email_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_issue": { - "name": "in_app_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_watch_new_issues_global": { - "name": "email_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "in_app_watch_new_issues_global": { - "name": "in_app_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "idx_notif_prefs_global_watch_email": { - "name": "idx_notif_prefs_global_watch_email", - "columns": [ - { - "expression": "email_watch_new_issues_global", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notification_preferences_user_id_user_profiles_id_fk": { - "name": "notification_preferences_user_id_user_profiles_id_fk", - "tableFrom": "notification_preferences", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.notifications": { - "name": "notifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "read_at": { - "name": "read_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_notifications_user_unread": { - "name": "idx_notifications_user_unread", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "read_at", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notifications_user_id_user_profiles_id_fk": { - "name": "notifications_user_id_user_profiles_id_fk", - "tableFrom": "notifications", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_profiles": { - "name": "user_profiles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json deleted file mode 100644 index c550ffc8..00000000 --- a/drizzle/meta/0002_snapshot.json +++ /dev/null @@ -1,768 +0,0 @@ -{ - "id": "75cbbb20-95fa-4137-a951-fdd6c7008ea6", - "prevId": "277d032f-4a11-401a-a0cf-56f0e64df34f", - "version": "7", - "dialect": "postgresql", - "tables": { - "auth.users": { - "name": "users", - "schema": "auth", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_comments": { - "name": "issue_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "author_id": { - "name": "author_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "name": "is_system", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issue_comments_issue_id": { - "name": "idx_issue_comments_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_comments_issue_id_issues_id_fk": { - "name": "issue_comments_issue_id_issues_id_fk", - "tableFrom": "issue_comments", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_comments_author_id_user_profiles_id_fk": { - "name": "issue_comments_author_id_user_profiles_id_fk", - "tableFrom": "issue_comments", - "tableTo": "user_profiles", - "columnsFrom": ["author_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_watchers": { - "name": "issue_watchers", - "schema": "", - "columns": { - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_issue_watchers_issue_id": { - "name": "idx_issue_watchers_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_watchers_issue_id_issues_id_fk": { - "name": "issue_watchers_issue_id_issues_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_watchers_user_id_user_profiles_id_fk": { - "name": "issue_watchers_user_id_user_profiles_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "issue_watchers_issue_id_user_id_pk": { - "name": "issue_watchers_issue_id_user_id_pk", - "columns": ["issue_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issues": { - "name": "issues", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "machine_initials": { - "name": "machine_initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "issue_number": { - "name": "issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'new'" - }, - "severity": { - "name": "severity", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'playable'" - }, - "priority": { - "name": "priority", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'low'" - }, - "reported_by": { - "name": "reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "assigned_to": { - "name": "assigned_to", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issues_assigned_to": { - "name": "idx_issues_assigned_to", - "columns": [ - { - "expression": "assigned_to", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_reported_by": { - "name": "idx_issues_reported_by", - "columns": [ - { - "expression": "reported_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_status": { - "name": "idx_issues_status", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issues_machine_initials_machines_initials_fk": { - "name": "issues_machine_initials_machines_initials_fk", - "tableFrom": "issues", - "tableTo": "machines", - "columnsFrom": ["machine_initials"], - "columnsTo": ["initials"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issues_reported_by_user_profiles_id_fk": { - "name": "issues_reported_by_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_assigned_to_user_profiles_id_fk": { - "name": "issues_assigned_to_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["assigned_to"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_issue_number": { - "name": "unique_issue_number", - "nullsNotDistinct": false, - "columns": ["machine_initials", "issue_number"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.machines": { - "name": "machines", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "initials": { - "name": "initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "next_issue_number": { - "name": "next_issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "owner_id": { - "name": "owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_machines_owner_id": { - "name": "idx_machines_owner_id", - "columns": [ - { - "expression": "owner_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "machines_owner_id_user_profiles_id_fk": { - "name": "machines_owner_id_user_profiles_id_fk", - "tableFrom": "machines", - "tableTo": "user_profiles", - "columnsFrom": ["owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "machines_initials_unique": { - "name": "machines_initials_unique", - "nullsNotDistinct": false, - "columns": ["initials"] - } - }, - "policies": {}, - "checkConstraints": { - "initials_check": { - "name": "initials_check", - "value": "initials ~ '^[A-Z0-9]{2,6}$'" - } - }, - "isRLSEnabled": false - }, - "public.notification_preferences": { - "name": "notification_preferences", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email_enabled": { - "name": "email_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_enabled": { - "name": "in_app_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_assigned": { - "name": "email_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_assigned": { - "name": "in_app_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_status_change": { - "name": "email_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_status_change": { - "name": "in_app_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_comment": { - "name": "email_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_comment": { - "name": "in_app_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_issue": { - "name": "email_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_issue": { - "name": "in_app_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_watch_new_issues_global": { - "name": "email_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "in_app_watch_new_issues_global": { - "name": "in_app_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "idx_notif_prefs_global_watch_email": { - "name": "idx_notif_prefs_global_watch_email", - "columns": [ - { - "expression": "email_watch_new_issues_global", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notification_preferences_user_id_user_profiles_id_fk": { - "name": "notification_preferences_user_id_user_profiles_id_fk", - "tableFrom": "notification_preferences", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.notifications": { - "name": "notifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "read_at": { - "name": "read_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_notifications_user_unread": { - "name": "idx_notifications_user_unread", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "read_at", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notifications_user_id_user_profiles_id_fk": { - "name": "notifications_user_id_user_profiles_id_fk", - "tableFrom": "notifications", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_profiles": { - "name": "user_profiles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json deleted file mode 100644 index d8f3d0b7..00000000 --- a/drizzle/meta/0003_snapshot.json +++ /dev/null @@ -1,881 +0,0 @@ -{ - "id": "05bed52c-fdef-484f-bf20-73d4cc0108fa", - "prevId": "75cbbb20-95fa-4137-a951-fdd6c7008ea6", - "version": "7", - "dialect": "postgresql", - "tables": { - "auth.users": { - "name": "users", - "schema": "auth", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_comments": { - "name": "issue_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "author_id": { - "name": "author_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "name": "is_system", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issue_comments_issue_id": { - "name": "idx_issue_comments_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_comments_issue_id_issues_id_fk": { - "name": "issue_comments_issue_id_issues_id_fk", - "tableFrom": "issue_comments", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_comments_author_id_user_profiles_id_fk": { - "name": "issue_comments_author_id_user_profiles_id_fk", - "tableFrom": "issue_comments", - "tableTo": "user_profiles", - "columnsFrom": ["author_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_watchers": { - "name": "issue_watchers", - "schema": "", - "columns": { - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_issue_watchers_issue_id": { - "name": "idx_issue_watchers_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_watchers_issue_id_issues_id_fk": { - "name": "issue_watchers_issue_id_issues_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_watchers_user_id_user_profiles_id_fk": { - "name": "issue_watchers_user_id_user_profiles_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "issue_watchers_issue_id_user_id_pk": { - "name": "issue_watchers_issue_id_user_id_pk", - "columns": ["issue_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issues": { - "name": "issues", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "machine_initials": { - "name": "machine_initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "issue_number": { - "name": "issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'new'" - }, - "severity": { - "name": "severity", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'playable'" - }, - "priority": { - "name": "priority", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'low'" - }, - "reported_by": { - "name": "reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_reported_by": { - "name": "unconfirmed_reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "assigned_to": { - "name": "assigned_to", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issues_assigned_to": { - "name": "idx_issues_assigned_to", - "columns": [ - { - "expression": "assigned_to", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_reported_by": { - "name": "idx_issues_reported_by", - "columns": [ - { - "expression": "reported_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_status": { - "name": "idx_issues_status", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issues_machine_initials_machines_initials_fk": { - "name": "issues_machine_initials_machines_initials_fk", - "tableFrom": "issues", - "tableTo": "machines", - "columnsFrom": ["machine_initials"], - "columnsTo": ["initials"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issues_reported_by_user_profiles_id_fk": { - "name": "issues_reported_by_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_unconfirmed_reported_by_unconfirmed_users_id_fk": { - "name": "issues_unconfirmed_reported_by_unconfirmed_users_id_fk", - "tableFrom": "issues", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_assigned_to_user_profiles_id_fk": { - "name": "issues_assigned_to_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["assigned_to"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_issue_number": { - "name": "unique_issue_number", - "nullsNotDistinct": false, - "columns": ["machine_initials", "issue_number"] - } - }, - "policies": {}, - "checkConstraints": { - "reporter_check": { - "name": "reporter_check", - "value": "(reported_by IS NULL OR unconfirmed_reported_by IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.machines": { - "name": "machines", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "initials": { - "name": "initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "next_issue_number": { - "name": "next_issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "owner_id": { - "name": "owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_owner_id": { - "name": "unconfirmed_owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_machines_owner_id": { - "name": "idx_machines_owner_id", - "columns": [ - { - "expression": "owner_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "machines_owner_id_user_profiles_id_fk": { - "name": "machines_owner_id_user_profiles_id_fk", - "tableFrom": "machines", - "tableTo": "user_profiles", - "columnsFrom": ["owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "machines_unconfirmed_owner_id_unconfirmed_users_id_fk": { - "name": "machines_unconfirmed_owner_id_unconfirmed_users_id_fk", - "tableFrom": "machines", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "machines_initials_unique": { - "name": "machines_initials_unique", - "nullsNotDistinct": false, - "columns": ["initials"] - } - }, - "policies": {}, - "checkConstraints": { - "initials_check": { - "name": "initials_check", - "value": "initials ~ '^[A-Z0-9]{2,6}$'" - }, - "owner_check": { - "name": "owner_check", - "value": "(owner_id IS NULL OR unconfirmed_owner_id IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.notification_preferences": { - "name": "notification_preferences", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email_enabled": { - "name": "email_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_enabled": { - "name": "in_app_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_assigned": { - "name": "email_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_assigned": { - "name": "in_app_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_status_change": { - "name": "email_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_status_change": { - "name": "in_app_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_comment": { - "name": "email_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_comment": { - "name": "in_app_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_issue": { - "name": "email_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_issue": { - "name": "in_app_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_watch_new_issues_global": { - "name": "email_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "in_app_watch_new_issues_global": { - "name": "in_app_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "idx_notif_prefs_global_watch_email": { - "name": "idx_notif_prefs_global_watch_email", - "columns": [ - { - "expression": "email_watch_new_issues_global", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notification_preferences_user_id_user_profiles_id_fk": { - "name": "notification_preferences_user_id_user_profiles_id_fk", - "tableFrom": "notification_preferences", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.notifications": { - "name": "notifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "read_at": { - "name": "read_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_notifications_user_unread": { - "name": "idx_notifications_user_unread", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "read_at", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notifications_user_id_user_profiles_id_fk": { - "name": "notifications_user_id_user_profiles_id_fk", - "tableFrom": "notifications", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.unconfirmed_users": { - "name": "unconfirmed_users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'guest'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "invite_sent_at": { - "name": "invite_sent_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unconfirmed_users_email_unique": { - "name": "unconfirmed_users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_profiles": { - "name": "user_profiles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json deleted file mode 100644 index 45d456f0..00000000 --- a/drizzle/meta/0004_snapshot.json +++ /dev/null @@ -1,896 +0,0 @@ -{ - "id": "1cdfad92-de66-436c-a84c-15c7366f6d45", - "prevId": "05bed52c-fdef-484f-bf20-73d4cc0108fa", - "version": "7", - "dialect": "postgresql", - "tables": { - "auth.users": { - "name": "users", - "schema": "auth", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_comments": { - "name": "issue_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "author_id": { - "name": "author_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "name": "is_system", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issue_comments_issue_id": { - "name": "idx_issue_comments_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issue_comments_author_id": { - "name": "idx_issue_comments_author_id", - "columns": [ - { - "expression": "author_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_comments_issue_id_issues_id_fk": { - "name": "issue_comments_issue_id_issues_id_fk", - "tableFrom": "issue_comments", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_comments_author_id_user_profiles_id_fk": { - "name": "issue_comments_author_id_user_profiles_id_fk", - "tableFrom": "issue_comments", - "tableTo": "user_profiles", - "columnsFrom": ["author_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_watchers": { - "name": "issue_watchers", - "schema": "", - "columns": { - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_issue_watchers_user_id": { - "name": "idx_issue_watchers_user_id", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_watchers_issue_id_issues_id_fk": { - "name": "issue_watchers_issue_id_issues_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_watchers_user_id_user_profiles_id_fk": { - "name": "issue_watchers_user_id_user_profiles_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "issue_watchers_issue_id_user_id_pk": { - "name": "issue_watchers_issue_id_user_id_pk", - "columns": ["issue_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issues": { - "name": "issues", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "machine_initials": { - "name": "machine_initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "issue_number": { - "name": "issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'new'" - }, - "severity": { - "name": "severity", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'playable'" - }, - "priority": { - "name": "priority", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'low'" - }, - "reported_by": { - "name": "reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_reported_by": { - "name": "unconfirmed_reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "assigned_to": { - "name": "assigned_to", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issues_assigned_to": { - "name": "idx_issues_assigned_to", - "columns": [ - { - "expression": "assigned_to", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_reported_by": { - "name": "idx_issues_reported_by", - "columns": [ - { - "expression": "reported_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_status": { - "name": "idx_issues_status", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issues_machine_initials_machines_initials_fk": { - "name": "issues_machine_initials_machines_initials_fk", - "tableFrom": "issues", - "tableTo": "machines", - "columnsFrom": ["machine_initials"], - "columnsTo": ["initials"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issues_reported_by_user_profiles_id_fk": { - "name": "issues_reported_by_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_unconfirmed_reported_by_unconfirmed_users_id_fk": { - "name": "issues_unconfirmed_reported_by_unconfirmed_users_id_fk", - "tableFrom": "issues", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_assigned_to_user_profiles_id_fk": { - "name": "issues_assigned_to_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["assigned_to"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_issue_number": { - "name": "unique_issue_number", - "nullsNotDistinct": false, - "columns": ["machine_initials", "issue_number"] - } - }, - "policies": {}, - "checkConstraints": { - "reporter_check": { - "name": "reporter_check", - "value": "(reported_by IS NULL OR unconfirmed_reported_by IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.machines": { - "name": "machines", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "initials": { - "name": "initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "next_issue_number": { - "name": "next_issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "owner_id": { - "name": "owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_owner_id": { - "name": "unconfirmed_owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_machines_owner_id": { - "name": "idx_machines_owner_id", - "columns": [ - { - "expression": "owner_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "machines_owner_id_user_profiles_id_fk": { - "name": "machines_owner_id_user_profiles_id_fk", - "tableFrom": "machines", - "tableTo": "user_profiles", - "columnsFrom": ["owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "machines_unconfirmed_owner_id_unconfirmed_users_id_fk": { - "name": "machines_unconfirmed_owner_id_unconfirmed_users_id_fk", - "tableFrom": "machines", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "machines_initials_unique": { - "name": "machines_initials_unique", - "nullsNotDistinct": false, - "columns": ["initials"] - } - }, - "policies": {}, - "checkConstraints": { - "initials_check": { - "name": "initials_check", - "value": "initials ~ '^[A-Z0-9]{2,6}$'" - }, - "owner_check": { - "name": "owner_check", - "value": "(owner_id IS NULL OR unconfirmed_owner_id IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.notification_preferences": { - "name": "notification_preferences", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email_enabled": { - "name": "email_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_enabled": { - "name": "in_app_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_assigned": { - "name": "email_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_assigned": { - "name": "in_app_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_status_change": { - "name": "email_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_status_change": { - "name": "in_app_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_comment": { - "name": "email_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_comment": { - "name": "in_app_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_issue": { - "name": "email_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_issue": { - "name": "in_app_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_watch_new_issues_global": { - "name": "email_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "in_app_watch_new_issues_global": { - "name": "in_app_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "idx_notif_prefs_global_watch_email": { - "name": "idx_notif_prefs_global_watch_email", - "columns": [ - { - "expression": "email_watch_new_issues_global", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notification_preferences_user_id_user_profiles_id_fk": { - "name": "notification_preferences_user_id_user_profiles_id_fk", - "tableFrom": "notification_preferences", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.notifications": { - "name": "notifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "read_at": { - "name": "read_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_notifications_user_unread": { - "name": "idx_notifications_user_unread", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "read_at", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notifications_user_id_user_profiles_id_fk": { - "name": "notifications_user_id_user_profiles_id_fk", - "tableFrom": "notifications", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.unconfirmed_users": { - "name": "unconfirmed_users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'guest'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "invite_sent_at": { - "name": "invite_sent_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unconfirmed_users_email_unique": { - "name": "unconfirmed_users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_profiles": { - "name": "user_profiles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json deleted file mode 100644 index e2cd3574..00000000 --- a/drizzle/meta/0005_snapshot.json +++ /dev/null @@ -1,908 +0,0 @@ -{ - "id": "5dc5c966-cf77-418e-9fc8-cdfadc8eb47f", - "prevId": "1cdfad92-de66-436c-a84c-15c7366f6d45", - "version": "7", - "dialect": "postgresql", - "tables": { - "auth.users": { - "name": "users", - "schema": "auth", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_comments": { - "name": "issue_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "author_id": { - "name": "author_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "name": "is_system", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issue_comments_issue_id": { - "name": "idx_issue_comments_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issue_comments_author_id": { - "name": "idx_issue_comments_author_id", - "columns": [ - { - "expression": "author_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_comments_issue_id_issues_id_fk": { - "name": "issue_comments_issue_id_issues_id_fk", - "tableFrom": "issue_comments", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_comments_author_id_user_profiles_id_fk": { - "name": "issue_comments_author_id_user_profiles_id_fk", - "tableFrom": "issue_comments", - "tableTo": "user_profiles", - "columnsFrom": ["author_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_watchers": { - "name": "issue_watchers", - "schema": "", - "columns": { - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_issue_watchers_user_id": { - "name": "idx_issue_watchers_user_id", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_watchers_issue_id_issues_id_fk": { - "name": "issue_watchers_issue_id_issues_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_watchers_user_id_user_profiles_id_fk": { - "name": "issue_watchers_user_id_user_profiles_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "issue_watchers_issue_id_user_id_pk": { - "name": "issue_watchers_issue_id_user_id_pk", - "columns": ["issue_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issues": { - "name": "issues", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "machine_initials": { - "name": "machine_initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "issue_number": { - "name": "issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'new'" - }, - "severity": { - "name": "severity", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'playable'" - }, - "priority": { - "name": "priority", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'low'" - }, - "reported_by": { - "name": "reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_reported_by": { - "name": "unconfirmed_reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "assigned_to": { - "name": "assigned_to", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issues_assigned_to": { - "name": "idx_issues_assigned_to", - "columns": [ - { - "expression": "assigned_to", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_reported_by": { - "name": "idx_issues_reported_by", - "columns": [ - { - "expression": "reported_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_status": { - "name": "idx_issues_status", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issues_machine_initials_machines_initials_fk": { - "name": "issues_machine_initials_machines_initials_fk", - "tableFrom": "issues", - "tableTo": "machines", - "columnsFrom": ["machine_initials"], - "columnsTo": ["initials"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issues_reported_by_user_profiles_id_fk": { - "name": "issues_reported_by_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_unconfirmed_reported_by_unconfirmed_users_id_fk": { - "name": "issues_unconfirmed_reported_by_unconfirmed_users_id_fk", - "tableFrom": "issues", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_assigned_to_user_profiles_id_fk": { - "name": "issues_assigned_to_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["assigned_to"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_issue_number": { - "name": "unique_issue_number", - "nullsNotDistinct": false, - "columns": ["machine_initials", "issue_number"] - } - }, - "policies": {}, - "checkConstraints": { - "reporter_check": { - "name": "reporter_check", - "value": "(reported_by IS NULL OR unconfirmed_reported_by IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.machines": { - "name": "machines", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "initials": { - "name": "initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "next_issue_number": { - "name": "next_issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "owner_id": { - "name": "owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_owner_id": { - "name": "unconfirmed_owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_machines_owner_id": { - "name": "idx_machines_owner_id", - "columns": [ - { - "expression": "owner_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "machines_owner_id_user_profiles_id_fk": { - "name": "machines_owner_id_user_profiles_id_fk", - "tableFrom": "machines", - "tableTo": "user_profiles", - "columnsFrom": ["owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "machines_unconfirmed_owner_id_unconfirmed_users_id_fk": { - "name": "machines_unconfirmed_owner_id_unconfirmed_users_id_fk", - "tableFrom": "machines", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "machines_initials_unique": { - "name": "machines_initials_unique", - "nullsNotDistinct": false, - "columns": ["initials"] - } - }, - "policies": {}, - "checkConstraints": { - "initials_check": { - "name": "initials_check", - "value": "initials ~ '^[A-Z0-9]{2,6}$'" - }, - "owner_check": { - "name": "owner_check", - "value": "(owner_id IS NULL OR unconfirmed_owner_id IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.notification_preferences": { - "name": "notification_preferences", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email_enabled": { - "name": "email_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_enabled": { - "name": "in_app_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_assigned": { - "name": "email_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_assigned": { - "name": "in_app_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_status_change": { - "name": "email_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_status_change": { - "name": "in_app_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_comment": { - "name": "email_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_comment": { - "name": "in_app_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_issue": { - "name": "email_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_issue": { - "name": "in_app_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_watch_new_issues_global": { - "name": "email_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "in_app_watch_new_issues_global": { - "name": "in_app_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "idx_notif_prefs_global_watch_email": { - "name": "idx_notif_prefs_global_watch_email", - "columns": [ - { - "expression": "email_watch_new_issues_global", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notification_preferences_user_id_user_profiles_id_fk": { - "name": "notification_preferences_user_id_user_profiles_id_fk", - "tableFrom": "notification_preferences", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.notifications": { - "name": "notifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "read_at": { - "name": "read_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_notifications_user_unread": { - "name": "idx_notifications_user_unread", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "read_at", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notifications_user_id_user_profiles_id_fk": { - "name": "notifications_user_id_user_profiles_id_fk", - "tableFrom": "notifications", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.unconfirmed_users": { - "name": "unconfirmed_users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'guest'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "invite_sent_at": { - "name": "invite_sent_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unconfirmed_users_email_unique": { - "name": "unconfirmed_users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_profiles": { - "name": "user_profiles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_profiles_email_unique": { - "name": "user_profiles_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/0006_snapshot.json b/drizzle/meta/0006_snapshot.json deleted file mode 100644 index b415137c..00000000 --- a/drizzle/meta/0006_snapshot.json +++ /dev/null @@ -1,953 +0,0 @@ -{ - "id": "454b291e-ad5f-4fd5-9dcf-c8b6f03a354c", - "prevId": "5dc5c966-cf77-418e-9fc8-cdfadc8eb47f", - "version": "7", - "dialect": "postgresql", - "tables": { - "auth.users": { - "name": "users", - "schema": "auth", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_comments": { - "name": "issue_comments", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "author_id": { - "name": "author_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "content": { - "name": "content", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "is_system": { - "name": "is_system", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issue_comments_issue_id": { - "name": "idx_issue_comments_issue_id", - "columns": [ - { - "expression": "issue_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issue_comments_author_id": { - "name": "idx_issue_comments_author_id", - "columns": [ - { - "expression": "author_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_comments_issue_id_issues_id_fk": { - "name": "issue_comments_issue_id_issues_id_fk", - "tableFrom": "issue_comments", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_comments_author_id_user_profiles_id_fk": { - "name": "issue_comments_author_id_user_profiles_id_fk", - "tableFrom": "issue_comments", - "tableTo": "user_profiles", - "columnsFrom": ["author_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issue_watchers": { - "name": "issue_watchers", - "schema": "", - "columns": { - "issue_id": { - "name": "issue_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_issue_watchers_user_id": { - "name": "idx_issue_watchers_user_id", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issue_watchers_issue_id_issues_id_fk": { - "name": "issue_watchers_issue_id_issues_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "issues", - "columnsFrom": ["issue_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issue_watchers_user_id_user_profiles_id_fk": { - "name": "issue_watchers_user_id_user_profiles_id_fk", - "tableFrom": "issue_watchers", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "issue_watchers_issue_id_user_id_pk": { - "name": "issue_watchers_issue_id_user_id_pk", - "columns": ["issue_id", "user_id"] - } - }, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.issues": { - "name": "issues", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "machine_initials": { - "name": "machine_initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "issue_number": { - "name": "issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'new'" - }, - "severity": { - "name": "severity", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'playable'" - }, - "priority": { - "name": "priority", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'low'" - }, - "reported_by": { - "name": "reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_reported_by": { - "name": "unconfirmed_reported_by", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "assigned_to": { - "name": "assigned_to", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "resolved_at": { - "name": "resolved_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_issues_assigned_to": { - "name": "idx_issues_assigned_to", - "columns": [ - { - "expression": "assigned_to", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_reported_by": { - "name": "idx_issues_reported_by", - "columns": [ - { - "expression": "reported_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_status": { - "name": "idx_issues_status", - "columns": [ - { - "expression": "status", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_created_at": { - "name": "idx_issues_created_at", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_issues_unconfirmed_reported_by": { - "name": "idx_issues_unconfirmed_reported_by", - "columns": [ - { - "expression": "unconfirmed_reported_by", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "issues_machine_initials_machines_initials_fk": { - "name": "issues_machine_initials_machines_initials_fk", - "tableFrom": "issues", - "tableTo": "machines", - "columnsFrom": ["machine_initials"], - "columnsTo": ["initials"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "issues_reported_by_user_profiles_id_fk": { - "name": "issues_reported_by_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_unconfirmed_reported_by_unconfirmed_users_id_fk": { - "name": "issues_unconfirmed_reported_by_unconfirmed_users_id_fk", - "tableFrom": "issues", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_reported_by"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "issues_assigned_to_user_profiles_id_fk": { - "name": "issues_assigned_to_user_profiles_id_fk", - "tableFrom": "issues", - "tableTo": "user_profiles", - "columnsFrom": ["assigned_to"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unique_issue_number": { - "name": "unique_issue_number", - "nullsNotDistinct": false, - "columns": ["machine_initials", "issue_number"] - } - }, - "policies": {}, - "checkConstraints": { - "reporter_check": { - "name": "reporter_check", - "value": "(reported_by IS NULL OR unconfirmed_reported_by IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.machines": { - "name": "machines", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "initials": { - "name": "initials", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "next_issue_number": { - "name": "next_issue_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "owner_id": { - "name": "owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "unconfirmed_owner_id": { - "name": "unconfirmed_owner_id", - "type": "uuid", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_machines_owner_id": { - "name": "idx_machines_owner_id", - "columns": [ - { - "expression": "owner_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_machines_unconfirmed_owner_id": { - "name": "idx_machines_unconfirmed_owner_id", - "columns": [ - { - "expression": "unconfirmed_owner_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "machines_owner_id_user_profiles_id_fk": { - "name": "machines_owner_id_user_profiles_id_fk", - "tableFrom": "machines", - "tableTo": "user_profiles", - "columnsFrom": ["owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - }, - "machines_unconfirmed_owner_id_unconfirmed_users_id_fk": { - "name": "machines_unconfirmed_owner_id_unconfirmed_users_id_fk", - "tableFrom": "machines", - "tableTo": "unconfirmed_users", - "columnsFrom": ["unconfirmed_owner_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "machines_initials_unique": { - "name": "machines_initials_unique", - "nullsNotDistinct": false, - "columns": ["initials"] - } - }, - "policies": {}, - "checkConstraints": { - "initials_check": { - "name": "initials_check", - "value": "initials ~ '^[A-Z0-9]{2,6}$'" - }, - "owner_check": { - "name": "owner_check", - "value": "(owner_id IS NULL OR unconfirmed_owner_id IS NULL)" - } - }, - "isRLSEnabled": false - }, - "public.notification_preferences": { - "name": "notification_preferences", - "schema": "", - "columns": { - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email_enabled": { - "name": "email_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_enabled": { - "name": "in_app_enabled", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_assigned": { - "name": "email_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_assigned": { - "name": "in_app_notify_on_assigned", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_status_change": { - "name": "email_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_status_change": { - "name": "in_app_notify_on_status_change", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_comment": { - "name": "email_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_comment": { - "name": "in_app_notify_on_new_comment", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_notify_on_new_issue": { - "name": "email_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "in_app_notify_on_new_issue": { - "name": "in_app_notify_on_new_issue", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "email_watch_new_issues_global": { - "name": "email_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "in_app_watch_new_issues_global": { - "name": "in_app_watch_new_issues_global", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - } - }, - "indexes": { - "idx_notif_prefs_global_watch_email": { - "name": "idx_notif_prefs_global_watch_email", - "columns": [ - { - "expression": "email_watch_new_issues_global", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notification_preferences_user_id_user_profiles_id_fk": { - "name": "notification_preferences_user_id_user_profiles_id_fk", - "tableFrom": "notification_preferences", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.notifications": { - "name": "notifications", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "resource_id": { - "name": "resource_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "resource_type": { - "name": "resource_type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "read_at": { - "name": "read_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_notifications_user_unread": { - "name": "idx_notifications_user_unread", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "read_at", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "notifications_user_id_user_profiles_id_fk": { - "name": "notifications_user_id_user_profiles_id_fk", - "tableFrom": "notifications", - "tableTo": "user_profiles", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.unconfirmed_users": { - "name": "unconfirmed_users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'guest'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "invite_sent_at": { - "name": "invite_sent_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "unconfirmed_users_email_unique": { - "name": "unconfirmed_users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_profiles": { - "name": "user_profiles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "first_name": { - "name": "first_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "last_name": { - "name": "last_name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "generated": { - "as": "first_name || ' ' || last_name", - "type": "stored" - } - }, - "avatar_url": { - "name": "avatar_url", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'member'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_profiles_email_unique": { - "name": "user_profiles_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3decc63b..af811aa8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,50 +5,8 @@ { "idx": 0, "version": "7", - "when": 1765036190971, - "tag": "0000_init-schema", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1766428516608, - "tag": "0001_add_performance_indexes", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1766770560460, - "tag": "0002_add_more_performance_indexes", - "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1766950334432, - "tag": "0003_add_unconfirmed_users", - "breakpoints": true - }, - { - "idx": 4, - "version": "7", - "when": 1767119720539, - "tag": "0004_whole_kate_bishop", - "breakpoints": true - }, - { - "idx": 5, - "version": "7", - "when": 1767195135624, - "tag": "0005_add_email_to_profiles", - "breakpoints": true - }, - { - "idx": 6, - "version": "7", - "when": 1767581078756, - "tag": "0006_add_performance_indexes", + "when": 1767922624191, + "tag": "0000_initv2", "breakpoints": true } ] diff --git a/e2e/full/email-notifications.spec.ts b/e2e/full/email-notifications.spec.ts index c4fce4a1..43075a54 100644 --- a/e2e/full/email-notifications.spec.ts +++ b/e2e/full/email-notifications.spec.ts @@ -50,8 +50,8 @@ test.describe("Email Notifications", () => { await page.goto("/report?machine=MM"); await page.getByLabel("Issue Title *").fill("Test Issue for Email"); await page.getByLabel("Description").fill("Testing email notifications"); - await page.getByLabel("Severity *").selectOption("playable"); - await page.getByLabel("Priority *").selectOption("low"); + await page.getByLabel("Select Severity").selectOption("minor"); + await page.getByLabel("Select Priority").selectOption("low"); // Submit form and wait for Server Action redirect (Safari-defensive) await submitFormAndWaitForRedirect( @@ -104,8 +104,8 @@ test.describe("Email Notifications", () => { // Create issue for a specific machine (e.g., MM) await page.goto("/report?machine=MM"); await page.getByLabel("Issue Title *").fill("Status Change Test"); - await page.getByLabel("Severity *").selectOption("playable"); - await page.getByLabel("Priority *").selectOption("low"); + await page.getByLabel("Select Severity").selectOption("minor"); + await page.getByLabel("Select Priority").selectOption("low"); // Submit form and wait for Server Action redirect (Safari-defensive) await submitFormAndWaitForRedirect( @@ -129,7 +129,6 @@ test.describe("Email Notifications", () => { // Update status await page.getByTestId("issue-status-select").selectOption("in_progress"); - await page.getByRole("button", { name: "Update Status" }).click(); await expect(page.getByTestId("status-update-success")).toBeVisible(); const url = page.url(); diff --git a/e2e/full/notifications.spec.ts b/e2e/full/notifications.spec.ts index 57962d90..214c0a7a 100644 --- a/e2e/full/notifications.spec.ts +++ b/e2e/full/notifications.spec.ts @@ -70,8 +70,8 @@ test.describe("Notifications", () => { await expect(publicPage.getByLabel("Issue Title *")).toHaveValue( issueTitle ); - await publicPage.getByLabel("Severity *").selectOption("minor"); - await expect(publicPage.getByLabel("Severity *")).toHaveValue("minor"); + await publicPage.getByLabel("Select Severity").selectOption("minor"); + await expect(publicPage.getByLabel("Select Severity")).toHaveValue("minor"); await publicPage .getByRole("button", { name: "Submit Issue Report" }) @@ -134,9 +134,9 @@ test.describe("Notifications", () => { const issueTitle = `Status Change ${timestamp}`; await page.getByLabel("Issue Title *").fill(issueTitle); // Explicitly select severity (required) - await page.getByLabel("Severity *").selectOption("minor"); + await page.getByLabel("Select Severity").selectOption("minor"); // Explicitly select priority (required for logged-in users) - await page.getByLabel("Priority *").selectOption("low"); + await page.getByLabel("Select Priority").selectOption("low"); await page.getByRole("button", { name: "Submit Issue Report" }).click(); @@ -170,7 +170,6 @@ test.describe("Notifications", () => { await adminPage .getByTestId("issue-status-select") .selectOption("in_progress"); - await adminPage.getByRole("button", { name: "Update Status" }).click(); await expect(adminPage.getByTestId("status-update-success")).toBeVisible(); @@ -237,8 +236,10 @@ test.describe("Notifications", () => { issueTitle ); - await publicPage.getByLabel("Severity *").selectOption("unplayable"); - await expect(publicPage.getByLabel("Severity *")).toHaveValue("unplayable"); + await publicPage.getByLabel("Select Severity").selectOption("unplayable"); + await expect(publicPage.getByLabel("Select Severity")).toHaveValue( + "unplayable" + ); await publicPage .getByRole("button", { name: "Submit Issue Report" }) @@ -299,8 +300,8 @@ test.describe("Notifications", () => { ); // Explicitly select severity (required) - await publicPage.getByLabel("Severity *").selectOption("minor"); - await expect(publicPage.getByLabel("Severity *")).toHaveValue("minor"); + await publicPage.getByLabel("Select Severity").selectOption("minor"); + await expect(publicPage.getByLabel("Select Severity")).toHaveValue("minor"); await publicPage .getByRole("button", { name: "Submit Issue Report" }) @@ -353,7 +354,7 @@ test.describe("Notifications", () => { await memberPage.getByLabel("Issue Title *").fill("Email Test Issue"); // Explicitly select severity (required) - await memberPage.getByLabel("Severity *").selectOption("minor"); + await memberPage.getByLabel("Select Severity").selectOption("minor"); await expect(memberPage.getByTestId("machine-select")).toHaveValue( machine.id @@ -361,7 +362,7 @@ test.describe("Notifications", () => { await expect(memberPage.getByLabel("Issue Title *")).toHaveValue( "Email Test Issue" ); - await expect(memberPage.getByLabel("Severity *")).toHaveValue("minor"); + await expect(memberPage.getByLabel("Select Severity")).toHaveValue("minor"); await memberPage .getByRole("button", { name: "Submit Issue Report" }) diff --git a/e2e/smoke/issues-crud.spec.ts b/e2e/smoke/issues-crud.spec.ts index a46659e8..3eba6ee3 100644 --- a/e2e/smoke/issues-crud.spec.ts +++ b/e2e/smoke/issues-crud.spec.ts @@ -49,8 +49,8 @@ test.describe("Issues System", () => { await page .getByLabel("Description") .fill("The right flipper does not respond when button is pressed"); - await page.getByLabel("Severity *").selectOption("playable"); - await page.getByLabel("Priority *").selectOption("medium"); + await page.getByTestId("severity-select").selectOption("minor"); + await page.getByTestId("priority-select").selectOption("medium"); // Submit form await page.getByRole("button", { name: "Submit Issue Report" }).click(); @@ -64,7 +64,6 @@ test.describe("Issues System", () => { .getByRole("main") .getByRole("heading", { level: 1, name: /Test flipper not working/ }) ).toBeVisible(); - await expect(page.getByText("Issue created")).toBeVisible(); rememberIssueId(page); }); @@ -101,8 +100,8 @@ test.describe("Issues System", () => { // Fill out remaining fields await page.getByLabel("Issue Title *").fill("Display flickering"); - await page.getByLabel("Severity *").selectOption("minor"); - await page.getByLabel("Priority *").selectOption("low"); + await page.getByTestId("severity-select").selectOption("minor"); + await page.getByTestId("priority-select").selectOption("low"); // Submit await page.getByRole("button", { name: "Submit Issue Report" }).click(); @@ -132,8 +131,8 @@ test.describe("Issues System", () => { issueTitle = `Test Issue for Details ${Date.now()}`; await page.goto(`/report?machine=${machineInitials}`); await page.getByLabel("Issue Title *").fill(issueTitle); - await page.locator("#severity").selectOption("playable"); - await page.locator("#priority").selectOption("medium"); + await page.getByTestId("severity-select").selectOption("minor"); + await page.getByTestId("priority-select").selectOption("medium"); await page.getByRole("button", { name: "Submit Issue Report" }).click(); await expect(page).toHaveURL(/\/m\/[A-Z0-9]{2,6}\/i\/[0-9]+/); @@ -174,14 +173,13 @@ test.describe("Issues System", () => { // Should show status and severity badges await expect(page.getByTestId("issue-status-badge")).toHaveText(/New/i); await expect(page.getByTestId("issue-severity-badge")).toHaveText( - /Playable/i + /Minor/i ); // Should show timeline await expect( page.getByRole("heading", { name: "Activity" }) ).toBeVisible(); - await expect(page.getByText("Issue created")).toBeVisible(); }); // Update tests moved to integration/full suite diff --git a/e2e/smoke/navigation.spec.ts b/e2e/smoke/navigation.spec.ts index 5eb2de7b..c305cbca 100644 --- a/e2e/smoke/navigation.spec.ts +++ b/e2e/smoke/navigation.spec.ts @@ -23,9 +23,6 @@ test.describe.serial("Navigation", () => { // Verify Report Issue shortcut is available to guests await expect(page.getByTestId("nav-report-issue")).toBeVisible(); - - // Verify Pre-Beta Banner is present - await expect(page.getByText("Pre-Beta Notice")).toBeVisible(); }); test("authenticated navigation - show quick links and user menu", async ({ diff --git a/e2e/smoke/public-reporting.spec.ts b/e2e/smoke/public-reporting.spec.ts index 6dac8470..7bc1b2d0 100644 --- a/e2e/smoke/public-reporting.spec.ts +++ b/e2e/smoke/public-reporting.spec.ts @@ -26,7 +26,7 @@ test.describe("Public Issue Reporting", () => { }) ).toBeVisible(); - const select = page.getByLabel("Machine *"); + const select = page.getByTestId("machine-select"); await expect(select).toBeVisible(); await select.selectOption({ index: 1 }); // Wait for URL refresh (router.push) to prevent race conditions on Mobile Safari @@ -37,7 +37,7 @@ test.describe("Public Issue Reporting", () => { await page .getByLabel("Description") .fill("Playfield gets stuck during multiball."); - await page.getByLabel("Severity *").selectOption("playable"); + await page.getByTestId("severity-select").selectOption("minor"); await page.getByRole("button", { name: "Submit Issue Report" }).click(); await expect(page).toHaveURL("/report/success"); @@ -58,12 +58,12 @@ test.describe("Public Issue Reporting", () => { const email = `newuser-${timestamp}@example.com`; await page.goto("/report"); - await page.getByLabel("Machine *").selectOption({ index: 1 }); + await page.getByTestId("machine-select").selectOption({ index: 1 }); // Wait for URL refresh (router.push) to prevent race conditions on Mobile Safari await expect(page).toHaveURL(/machine=/); await page.getByLabel("Issue Title *").fill(`${PUBLIC_PREFIX} with Email`); - await page.getByLabel("Severity *").selectOption("minor"); + await page.getByTestId("severity-select").selectOption("minor"); await page.getByLabel("First Name").fill("Test"); await page.getByLabel("Last Name").fill("User"); diff --git a/e2e/smoke/status-overhaul.spec.ts b/e2e/smoke/status-overhaul.spec.ts new file mode 100644 index 00000000..66b2e395 --- /dev/null +++ b/e2e/smoke/status-overhaul.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from "@playwright/test"; +import { loginAs } from "../support/actions"; +import { cleanupTestEntities } from "../support/cleanup"; +import { seededMachines } from "../support/constants"; + +test.describe("Status Overhaul E2E", () => { + test.beforeEach(async ({ page }, testInfo) => { + test.setTimeout(60000); + await loginAs(page, testInfo); + }); + + test.afterEach(async ({ request }) => { + await cleanupTestEntities(request, { + issueTitlePrefix: "E2E Status Overhaul Test", + }); + }); + + test("should create issue and verify all 4 badges", async ({ page }) => { + const machine = seededMachines.addamsFamily; + + // 1. Create Issue + await page.goto(`/report?machine=${machine.initials}`); + await page.getByTestId("machine-select").selectOption(machine.id); + await page.getByLabel("Issue Title *").fill("E2E Status Overhaul Test"); + await page.getByTestId("severity-select").selectOption("major"); + await page.getByTestId("priority-select").selectOption("high"); + await page.getByTestId("consistency-select").selectOption("frequent"); + await page.getByRole("button", { name: "Submit Issue Report" }).click(); + + // 2. Verify redirect and badges + await expect(page).toHaveURL(/\/m\/TAF\/i\/[0-9]+/); + + await expect(page.getByTestId("issue-status-badge")).toHaveText(/New/i); + await expect(page.getByTestId("issue-severity-badge")).toHaveText(/Major/i); + await expect(page.getByTestId("issue-priority-badge")).toHaveText(/High/i); + await expect(page.getByTestId("issue-consistency-badge")).toHaveText( + /Frequent/i + ); + + // 3. Update Status + await page.getByLabel("Update Issue Status").selectOption("in_progress"); + + // 4. Verify status change in badge and timeline + await expect(page.getByTestId("issue-status-badge")).toHaveText( + /In Progress/i + ); + await expect( + page.getByText("Status changed from New to In Progress") + ).toBeVisible(); + }); +}); diff --git a/next.config.ts b/next.config.ts index e49e4b56..59230716 100644 --- a/next.config.ts +++ b/next.config.ts @@ -62,8 +62,8 @@ export default withSentryConfig(nextConfig, { tunnelRoute: "/monitoring", // Automatically tree-shake Sentry logger statements to reduce bundle size - disableLogger: true, - - // Enables automatic instrumentation of Vercel Cron Monitors. - automaticVercelMonitors: true, + // @ts-ignore - treeshake is a valid but potentially untyped property in some versions + treeshake: { + removeDebugLogging: true, + }, }); diff --git a/package.json b/package.json index cc5211cb..75c7b0c0 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,19 @@ "build": "next build", "start": "next start", "typecheck": "tsc --noEmit -p tsconfig.json", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", - "format": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\"", - "format:fix": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"", - "test": "vitest run --project unit --silent --no-color", - "test:integration": "vitest run --project integration --silent --no-color", - "test:integration:supabase": "vitest run --project integration-supabase --silent --no-color", + "lint": "eslint src/ --quiet", + "lint:fix": "eslint src/ --fix --quiet", + "format": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\" --log-level error", + "format:fix": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\" --log-level error", + "test": "vitest run --project unit --silent --no-color --reporter=dot", + "test:integration": "vitest run --project integration --silent --no-color --reporter=dot", + "test:integration:supabase": "vitest run --project integration-supabase --silent --no-color --reporter=dot", "test:watch": "vitest watch --project unit", - "test:coverage": "vitest run --project unit --coverage --silent --no-color", + "test:coverage": "vitest run --project unit --coverage --silent --no-color --reporter=dot", "test:_generate-schema": "drizzle-kit export --dialect=postgresql --schema=./src/server/db/schema.ts > src/test/setup/schema.sql", - "smoke": "playwright test --config=playwright.config.smoke.ts --project=chromium --project='Mobile Chrome'", + "smoke": "playwright test --config=playwright.config.smoke.ts --project=chromium --project='Mobile Chrome' --quiet", "smoke:headed": "playwright test --config=playwright.config.smoke.ts --headed --project=chromium --project='Mobile Chrome'", - "e2e:full": "playwright test --config=playwright.config.full.ts", + "e2e:full": "playwright test --config=playwright.config.full.ts --quiet", "e2e:full:headed": "playwright test --config=playwright.config.full.ts --headed", "e2e:mobile": "playwright test --project='Mobile Chrome'", "db:reset": "npm-run-all db:_restart db:_drop_tables db:migrate test:_generate-schema db:_seed db:_seed-users", @@ -33,8 +33,8 @@ "db:generate": "drizzle-kit generate", "db:_seed": "node --env-file=.env.local -e 'require(\"child_process\").execSync(\"psql \" + process.env.DATABASE_URL + \" -f supabase/seed.sql\", {stdio: \"inherit\"})'", "db:_seed-users": "node --env-file=.env.local supabase/seed-users.mjs", - "db:fast-reset": "node --env-file=.env.local scripts/db-fast-reset.mjs", - "check:config": "python3 scripts/check-config-drift.py", + "db:fast-reset": "node --env-file=.env.local scripts/db-fast-reset.mjs > /dev/null", + "check:config": "python3 scripts/check-config-drift.py > /dev/null", "check": "npm-run-all --silent --parallel typecheck lint test format:fix", "preflight": "npm-run-all --silent --parallel typecheck lint:fix format:fix test check:config --sequential db:fast-reset --parallel build test:integration test:integration:supabase --sequential smoke", "supa:ci": "bash scripts/supa-ci.sh", diff --git a/playwright.config.ts b/playwright.config.ts index b6d2185c..4ef098ad 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -130,7 +130,7 @@ export default defineConfig({ // Reporters: print progress locally; keep CI quiet; never block on HTML reporter: process.env["CI"] ? [["dot"], ["html", { open: "never" }]] - : [["list"], ["html", { open: "never" }]], + : [["line"], ["html", { open: "never" }]], // Shared settings for all the projects below use: { diff --git a/scripts/db-fast-reset.mjs b/scripts/db-fast-reset.mjs index b2bcd1b1..763e9504 100644 --- a/scripts/db-fast-reset.mjs +++ b/scripts/db-fast-reset.mjs @@ -2,10 +2,10 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { execSync } from "child_process"; -const databaseUrl = process.env.DATABASE_URL; +const databaseUrl = process.env.DIRECT_URL || process.env.DATABASE_URL; if (!databaseUrl) { - console.error("❌ DATABASE_URL is not defined in .env.local"); + console.error("❌ DATABASE_URL or DIRECT_URL is not defined"); process.exit(1); } @@ -20,13 +20,13 @@ async function fastReset() { // Truncate all tables except migrations/schema-related if any // We cascade to handle foreign keys await client` - TRUNCATE TABLE - "issue_comments", - "issue_watchers", - "issues", - "machines", - "notifications", - "notification_preferences", + TRUNCATE TABLE + "issue_comments", + "issue_watchers", + "issues", + "machines", + "notifications", + "notification_preferences", "user_profiles", "auth"."users" CASCADE; diff --git a/scripts/force-db-reset.mjs b/scripts/force-db-reset.mjs index c9f97bd1..7dc909b9 100644 --- a/scripts/force-db-reset.mjs +++ b/scripts/force-db-reset.mjs @@ -8,10 +8,10 @@ import postgres from "postgres"; -const databaseUrl = process.env.DATABASE_URL; +const databaseUrl = process.env.DIRECT_URL || process.env.DATABASE_URL; if (!databaseUrl) { - console.error("❌ DATABASE_URL is not defined in .env.local"); + console.error("❌ DATABASE_URL or DIRECT_URL is not defined"); process.exit(1); } diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index d7b75a96..2accbe67 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -8,22 +8,14 @@ import { Wrench, XCircle, } from "lucide-react"; -import { cn } from "~/lib/utils"; import { createClient } from "~/lib/supabase/server"; import { db } from "~/server/db"; import { issues, machines, userProfiles } from "~/server/db/schema"; -import { desc, eq, sql, and, ne } from "drizzle-orm"; +import { desc, eq, sql, and, notInArray } from "drizzle-orm"; +import { CLOSED_STATUSES } from "~/lib/issues/status"; +import type { Issue } from "~/lib/types"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { Badge } from "~/components/ui/badge"; -import { - getIssueStatusStyles, - getIssueSeverityStyles, - getIssueStatusLabel, - getIssueSeverityLabel, - isIssueStatus, - isIssueSeverity, -} from "~/lib/issues/status"; -import { formatIssueId } from "~/lib/issues/utils"; +import { IssueCard } from "~/components/issues/IssueCard"; /** * Cached dashboard data fetcher (CORE-PERF-001) @@ -48,7 +40,7 @@ const getDashboardData = cache(async (userId?: string) => { ? db.query.issues.findMany({ where: and( eq(issues.assignedTo, userId), - ne(issues.status, "resolved") + notInArray(issues.status, [...CLOSED_STATUSES]) ), orderBy: desc(issues.createdAt), limit: 10, @@ -66,6 +58,8 @@ const getDashboardData = cache(async (userId?: string) => { title: true, status: true, severity: true, + priority: true, + consistency: true, machineInitials: true, issueNumber: true, createdAt: true, @@ -79,7 +73,10 @@ const getDashboardData = cache(async (userId?: string) => { .select({ count: sql`count(*)::int` }) .from(issues) .where( - and(eq(issues.assignedTo, userId), ne(issues.status, "resolved")) + and( + eq(issues.assignedTo, userId), + notInArray(issues.status, [...CLOSED_STATUSES]) + ) ) : Promise.resolve([{ count: 0 }]); @@ -107,6 +104,8 @@ const getDashboardData = cache(async (userId?: string) => { title: true, status: true, severity: true, + priority: true, + consistency: true, machineInitials: true, issueNumber: true, createdAt: true, @@ -125,7 +124,10 @@ const getDashboardData = cache(async (userId?: string) => { .from(machines) .innerJoin(issues, eq(issues.machineInitials, machines.initials)) .where( - and(eq(issues.severity, "unplayable"), ne(issues.status, "resolved")) + and( + eq(issues.severity, "unplayable"), + notInArray(issues.status, [...CLOSED_STATUSES]) + ) ) .groupBy(machines.id, machines.name, machines.initials); @@ -133,7 +135,7 @@ const getDashboardData = cache(async (userId?: string) => { const totalOpenIssuesPromise = db .select({ count: sql`count(*)::int` }) .from(issues) - .where(ne(issues.status, "resolved")); + .where(notInArray(issues.status, [...CLOSED_STATUSES])); // Query 6: Machines needing service (machines with at least one open issue) // Optimized to use count(distinct) instead of fetching all IDs @@ -141,7 +143,7 @@ const getDashboardData = cache(async (userId?: string) => { .select({ count: sql`count(distinct ${machines.id})::int` }) .from(machines) .innerJoin(issues, eq(issues.machineInitials, machines.initials)) - .where(ne(issues.status, "resolved")); + .where(notInArray(issues.status, [...CLOSED_STATUSES])); // Execute all queries in parallel const [ @@ -296,63 +298,13 @@ export default async function DashboardPage(): Promise { ) : (
{assignedIssues.map((issue) => ( - - - -
-
- - - {formatIssueId( - issue.machineInitials, - issue.issueNumber - )} - {" "} - {issue.title} - -

- {issue.machine.name} -

-
-
- {/* Status Badge */} - {isIssueStatus(issue.status) && ( - - {getIssueStatusLabel(issue.status)} - - )} - {/* Severity Badge */} - {isIssueSeverity(issue.severity) && ( - - {getIssueSeverityLabel(issue.severity)} - - )} -
-
-
-
- + issue={issue as unknown as Issue} + machine={{ name: issue.machine.name }} + variant="compact" + dataTestId="assigned-issue-card" + /> ))}
)} @@ -424,69 +376,16 @@ export default async function DashboardPage(): Promise { data-testid="recent-issues-list" > {recentIssues.map((issue) => ( - - - -
-
- - - {formatIssueId( - issue.machineInitials, - issue.issueNumber - )} - {" "} - {issue.title} - -
- {issue.machine.name} - - Reported by{" "} - {issue.reportedByUser?.name ?? - "Anonymous Reporter"}{" "} - • {new Date(issue.createdAt).toLocaleDateString()} - -
-
-
- {/* Status Badge */} - {isIssueStatus(issue.status) && ( - - {getIssueStatusLabel(issue.status)} - - )} - {/* Severity Badge */} - {isIssueSeverity(issue.severity) && ( - - {getIssueSeverityLabel(issue.severity)} - - )} -
-
-
-
- + issue={issue as unknown as Issue} + machine={{ name: issue.machine.name }} + showReporter={true} + reporterName={ + issue.reportedByUser?.name ?? "Anonymous Reporter" + } + dataTestId="recent-issue-card" + /> ))} )} diff --git a/src/app/(app)/debug/badges/page.tsx b/src/app/(app)/debug/badges/page.tsx new file mode 100644 index 00000000..33b78b18 --- /dev/null +++ b/src/app/(app)/debug/badges/page.tsx @@ -0,0 +1,126 @@ +import type React from "react"; +import { IssueBadge } from "~/components/issues/IssueBadge"; +import { ISSUE_STATUS_VALUES } from "~/lib/issues/status"; +import type { + IssueSeverity, + IssuePriority, + IssueConsistency, +} from "~/lib/types"; + +export default function BadgeDebugPage(): React.JSX.Element { + const severities: IssueSeverity[] = [ + "cosmetic", + "minor", + "major", + "unplayable", + ]; + const priorities: IssuePriority[] = ["low", "medium", "high"]; + const consistencies: IssueConsistency[] = [ + "intermittent", + "frequent", + "constant", + ]; + + return ( +
+
+

Issue Badge System

+

+ Standardized badges with fixed width (120px) and vibrant colors. +

+
+ +
+ {/* Statuses Section */} +
+

+ Status Variations +

+
+ {ISSUE_STATUS_VALUES.map((status) => { + return ( +
+ + + {status} + +
+ ); + })} +
+
+ +
+ {/* Severities Section */} +
+

+ Severity Variations +

+
+ {severities.map((severity) => { + return ( +
+ + + {severity} + +
+ ); + })} +
+
+ + {/* Priorities Section */} +
+

+ Priority Variations +

+
+ {priorities.map((priority) => { + return ( +
+ + + {priority} + +
+ ); + })} +
+
+ + {/* Consistencies Section */} +
+

+ Consistency Variations +

+
+ {consistencies.map((consistency) => { + return ( +
+ + + {consistency} + +
+ ); + })} +
+
+
+
+
+ ); +} diff --git a/src/app/(app)/help/page.tsx b/src/app/(app)/help/page.tsx index 9967af65..3fe136bd 100644 --- a/src/app/(app)/help/page.tsx +++ b/src/app/(app)/help/page.tsx @@ -46,12 +46,17 @@ export default function HelpPage(): React.JSX.Element {

  • - minor – cosmetic or small - issues that do not change how the game plays. + cosmetic – very minor issues + that do not affect gameplay at all (dirty playfield, minor bulb + out).
  • - playable – the game plays, - but something is clearly wrong (shots not registering, features + minor – small issues that do + not change how the game plays, but might be noticeable. +
  • +
  • + major – the game plays, but + something significant is wrong (shots not registering, features disabled, audio glitches).
  • @@ -76,8 +81,8 @@ export default function HelpPage(): React.JSX.Element { Comments can be added to track troubleshooting steps and fixes.
  • - When the issue is resolved, the machine's status updates based - on its remaining open issues. + When the issue is fixed, the machine's status + updates based on its remaining open issues.
diff --git a/src/app/(app)/issues/actions.ts b/src/app/(app)/issues/actions.ts index 279738ec..7db6596a 100644 --- a/src/app/(app)/issues/actions.ts +++ b/src/app/(app)/issues/actions.ts @@ -19,6 +19,7 @@ import { updateIssueStatusSchema, updateIssueSeveritySchema, updateIssuePrioritySchema, + updateIssueConsistencySchema, assignIssueSchema, addCommentSchema, } from "./schemas"; @@ -30,6 +31,7 @@ import { assignIssue, updateIssueSeverity, updateIssuePriority, + updateIssueConsistency, } from "~/services/issues"; import { canUpdateIssue } from "~/lib/permissions"; import { userProfiles } from "~/server/db/schema"; @@ -70,6 +72,11 @@ export type UpdateIssuePriorityResult = Result< "VALIDATION" | "UNAUTHORIZED" | "NOT_FOUND" | "SERVER" >; +export type UpdateIssueConsistencyResult = Result< + { issueId: string }, + "VALIDATION" | "UNAUTHORIZED" | "NOT_FOUND" | "SERVER" +>; + export type AssignIssueResult = Result< { issueId: string }, "VALIDATION" | "UNAUTHORIZED" | "NOT_FOUND" | "SERVER" @@ -111,6 +118,7 @@ export async function createIssueAction( machineInitials: toOptionalString(formData.get("machineInitials")), severity: toOptionalString(formData.get("severity")), priority: toOptionalString(formData.get("priority")), + consistency: toOptionalString(formData.get("consistency")), }; const validation = createIssueSchema.safeParse(rawData); @@ -127,8 +135,14 @@ export async function createIssueAction( return err("VALIDATION", firstError?.message ?? "Invalid input"); } - const { title, description, machineInitials, severity, priority } = - validation.data; + const { + title, + description, + machineInitials, + severity, + priority, + consistency, + } = validation.data; // Create issue via service try { @@ -138,6 +152,7 @@ export async function createIssueAction( machineInitials, severity, priority, + consistency, reportedBy: user.id, }); @@ -148,7 +163,7 @@ export async function createIssueAction( // Redirect to new issue page redirect(`/m/${machineInitials}/i/${issue.issueNumber}`); - } catch (error) { + } catch (error: unknown) { if (isNextRedirectError(error)) { throw error; } @@ -369,6 +384,93 @@ export async function updateIssueSeverityAction( } } +/** + * Update Issue Consistency Action + */ +export async function updateIssueConsistencyAction( + _prevState: UpdateIssueConsistencyResult | undefined, + formData: FormData +): Promise { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + if (!user) return err("UNAUTHORIZED", "Unauthorized"); + + const rawData = { + issueId: toOptionalString(formData.get("issueId")), + consistency: toOptionalString(formData.get("consistency")), + }; + + const validation = updateIssueConsistencySchema.safeParse(rawData); + if (!validation.success) { + return err( + "VALIDATION", + validation.error.issues[0]?.message ?? "Invalid input" + ); + } + + const { issueId, consistency } = validation.data; + + try { + const currentIssue = await db.query.issues.findFirst({ + where: eq(issues.id, issueId), + columns: { + machineInitials: true, + issueNumber: true, + reportedBy: true, + assignedTo: true, + }, + with: { + machine: { + columns: { ownerId: true }, + }, + }, + }); + + if (!currentIssue) return err("NOT_FOUND", "Issue not found"); + + // Permission check + const userProfile = await db.query.userProfiles.findFirst({ + where: eq(userProfiles.id, user.id), + columns: { role: true }, + }); + + if ( + !canUpdateIssue( + { id: user.id, role: userProfile?.role ?? "guest" }, + currentIssue, + currentIssue.machine + ) + ) { + return err( + "UNAUTHORIZED", + "You do not have permission to update this issue" + ); + } + + // Update consistency + await updateIssueConsistency({ + issueId, + consistency, + }); + + revalidatePath( + `/m/${currentIssue.machineInitials}/i/${currentIssue.issueNumber}` + ); + return ok({ issueId }); + } catch (error: unknown) { + log.error( + { + error: error instanceof Error ? error.message : "Unknown", + action: "updateIssueConsistency", + }, + "Update issue consistency error" + ); + return err("SERVER", "Failed to update consistency"); + } +} + /** * Update Issue Priority Action * diff --git a/src/app/(app)/issues/page.tsx b/src/app/(app)/issues/page.tsx index c546dbbf..bd893506 100644 --- a/src/app/(app)/issues/page.tsx +++ b/src/app/(app)/issues/page.tsx @@ -8,6 +8,13 @@ import { IssueRow } from "~/components/issues/IssueRow"; import { AlertTriangle } from "lucide-react"; import { createClient } from "~/lib/supabase/server"; import { redirect } from "next/navigation"; +import type { + Issue, + IssueStatus, + IssueSeverity, + IssuePriority, +} from "~/lib/types"; +import { OPEN_STATUSES, CLOSED_STATUSES } from "~/lib/issues/status"; export const metadata: Metadata = { title: "Issues | PinPoint", @@ -44,38 +51,40 @@ export default async function IssuesPage({ columns: { initials: true, name: true }, }); - // Safe type casting for filters - // Default to Open issues (new + in_progress) if no status is specified - // If status is 'resolved', show resolved issues - // If specific status (new/in_progress) is requested, respect it - let statusFilter: string[] | undefined; + // Safe type casting for filters using imported constants from single source of truth + // Based on _issue-status-redesign/README.md - Final design with 11 statuses + let statusFilter: IssueStatus[]; - if (status === "resolved") { - statusFilter = ["resolved"]; - } else if (status === "new" || status === "in_progress") { - statusFilter = [status]; + if (status === "closed") { + statusFilter = [...CLOSED_STATUSES]; + } else if ( + (OPEN_STATUSES as readonly IssueStatus[]).includes(status as IssueStatus) + ) { + statusFilter = [status as IssueStatus]; + } else if ( + (CLOSED_STATUSES as readonly IssueStatus[]).includes(status as IssueStatus) + ) { + statusFilter = [status as IssueStatus]; } else { // Default case: Show all Open issues - statusFilter = ["new", "in_progress"]; + statusFilter = [...OPEN_STATUSES]; } - const severityFilter = - severity && ["minor", "playable", "unplayable"].includes(severity) - ? (severity as "minor" | "playable" | "unplayable") + const severityFilter: IssueSeverity | undefined = + severity && ["cosmetic", "minor", "major", "unplayable"].includes(severity) + ? (severity as IssueSeverity) : undefined; - const priorityFilter = - priority && ["low", "medium", "high", "critical"].includes(priority) - ? (priority as "low" | "medium" | "high" | "critical") + const priorityFilter: IssuePriority | undefined = + priority && ["low", "medium", "high"].includes(priority) + ? (priority as IssuePriority) : undefined; // Fetch Issues based on filters - const issuesList = await db.query.issues.findMany({ + // Type assertion needed because Drizzle infers status as string, not IssueStatus + const issuesList = (await db.query.issues.findMany({ where: and( - inArray( - issues.status, - statusFilter as ("new" | "in_progress" | "resolved")[] - ), + inArray(issues.status, statusFilter), severityFilter ? eq(issues.severity, severityFilter) : undefined, priorityFilter ? eq(issues.priority, priorityFilter) : undefined, machine ? eq(issues.machineInitials, machine) : undefined @@ -90,7 +99,21 @@ export default async function IssuesPage({ }, }, limit: 100, // Reasonable limit for now - }); + })) as (Pick< + Issue, + | "id" + | "createdAt" + | "machineInitials" + | "issueNumber" + | "title" + | "status" + | "severity" + | "priority" + | "consistency" + > & { + machine: { name: string } | null; + reportedByUser: { name: string } | null; + })[]; return (
diff --git a/src/app/(app)/issues/schemas.ts b/src/app/(app)/issues/schemas.ts index 54a8b2ec..82c98a19 100644 --- a/src/app/(app)/issues/schemas.ts +++ b/src/app/(app)/issues/schemas.ts @@ -6,6 +6,7 @@ */ import { z } from "zod"; +import { ISSUE_STATUS_VALUES } from "~/lib/issues/status"; const uuidish = z .string() @@ -39,20 +40,25 @@ export const createIssueSchema = z.object({ .min(2, "Machine initials invalid") .max(6, "Machine initials invalid") .regex(/^[A-Z0-9]+$/, "Machine initials invalid"), - severity: z.enum(["minor", "playable", "unplayable"], { + severity: z.enum(["cosmetic", "minor", "major", "unplayable"], { message: "Invalid severity level", }), priority: z.enum(["low", "medium", "high"], { message: "Invalid priority level", }), + consistency: z.enum(["intermittent", "frequent", "constant"], { + message: "Invalid consistency level", + }), }); /** * Schema for updating issue status + * Based on _issue-status-redesign/README.md - Final design with 11 statuses + * Status values imported from single source of truth */ export const updateIssueStatusSchema = z.object({ issueId: uuidish, - status: z.enum(["new", "in_progress", "resolved"], { + status: z.enum(ISSUE_STATUS_VALUES, { message: "Invalid status", }), }); @@ -62,7 +68,7 @@ export const updateIssueStatusSchema = z.object({ */ export const updateIssueSeveritySchema = z.object({ issueId: uuidish, - severity: z.enum(["minor", "playable", "unplayable"], { + severity: z.enum(["cosmetic", "minor", "major", "unplayable"], { message: "Invalid severity level", }), }); @@ -77,6 +83,16 @@ export const updateIssuePrioritySchema = z.object({ }), }); +/** + * Schema for updating issue consistency + */ +export const updateIssueConsistencySchema = z.object({ + issueId: uuidish, + consistency: z.enum(["intermittent", "frequent", "constant"], { + message: "Invalid consistency level", + }), +}); + /** * Schema for assigning issue to user */ diff --git a/src/app/(app)/m/[initials]/i/[issueNumber]/page.tsx b/src/app/(app)/m/[initials]/i/[issueNumber]/page.tsx index c292ebda..83b3ee94 100644 --- a/src/app/(app)/m/[initials]/i/[issueNumber]/page.tsx +++ b/src/app/(app)/m/[initials]/i/[issueNumber]/page.tsx @@ -1,36 +1,17 @@ import type React from "react"; import { redirect } from "next/navigation"; -import { cn } from "~/lib/utils"; import Link from "next/link"; import { ArrowLeft } from "lucide-react"; import { createClient } from "~/lib/supabase/server"; import { db } from "~/server/db"; import { issues, userProfiles, authUsers } from "~/server/db/schema"; import { eq, asc, and } from "drizzle-orm"; -import { Badge } from "~/components/ui/badge"; import { PageShell } from "~/components/layout/PageShell"; import { IssueTimeline } from "~/components/issues/IssueTimeline"; import { IssueSidebar } from "~/components/issues/IssueSidebar"; -import { - getIssueStatusStyles, - getIssueSeverityStyles, - getIssuePriorityStyles, -} from "~/lib/issues/status"; -import { type IssueSeverity, type IssuePriority } from "~/lib/types"; +import { IssueBadgeGrid } from "~/components/issues/IssueBadgeGrid"; import { formatIssueId } from "~/lib/issues/utils"; - -const severityCopy: Record = { - minor: "Minor", - playable: "Playable", - unplayable: "Unplayable", -}; - -const priorityCopy: Record = { - low: "Low", - medium: "Medium", - high: "High", - critical: "Critical", -}; +import type { Issue, IssueWithAllRelations } from "~/lib/types"; /** * Issue Detail Page (Protected Route) @@ -150,37 +131,16 @@ export default async function IssueDetailPage({ {issue.title}
- - {issue.status === "in_progress" - ? "In Progress" - : issue.status.charAt(0).toUpperCase() + issue.status.slice(1)} - - - - {severityCopy[issue.severity]} - - - - {priorityCopy[issue.priority]} - + + } + variant="strip" + size="lg" + />
@@ -190,12 +150,12 @@ export default async function IssueDetailPage({

Activity

- + {/* Sticky Sidebar */} diff --git a/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-consistency-form.tsx b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-consistency-form.tsx new file mode 100644 index 00000000..b5787381 --- /dev/null +++ b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-consistency-form.tsx @@ -0,0 +1,66 @@ +"use client"; + +import type React from "react"; +import { useActionState } from "react"; +import { Loader2 } from "lucide-react"; +import { + updateIssueConsistencyAction, + type UpdateIssueConsistencyResult, +} from "~/app/(app)/issues/actions"; +import { type IssueConsistency } from "~/lib/types"; + +interface UpdateIssueConsistencyFormProps { + issueId: string; + currentConsistency: IssueConsistency; +} + +const consistencyOptions: { value: IssueConsistency; label: string }[] = [ + { value: "intermittent", label: "Intermittent" }, + { value: "frequent", label: "Frequent" }, + { value: "constant", label: "Constant" }, +]; + +export function UpdateIssueConsistencyForm({ + issueId, + currentConsistency, +}: UpdateIssueConsistencyFormProps): React.JSX.Element { + const [state, formAction, isPending] = useActionState< + UpdateIssueConsistencyResult | undefined, + FormData + >(updateIssueConsistencyAction, undefined); + + return ( +
+ +
+ + {isPending && ( +
+ +
+ )} +
+ {state && !state.ok && ( +

{state.message}

+ )} +
+ ); +} diff --git a/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-priority-form.tsx b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-priority-form.tsx index dd0bab79..3ab67708 100644 --- a/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-priority-form.tsx +++ b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-priority-form.tsx @@ -2,7 +2,7 @@ import type React from "react"; import { useActionState } from "react"; -import { Button } from "~/components/ui/button"; +import { Loader2 } from "lucide-react"; import { updateIssuePriorityAction, type UpdateIssuePriorityResult, @@ -32,25 +32,32 @@ export function UpdateIssuePriorityForm({ return (
- - +
+ + {isPending && ( +
+ +
+ )} +
{state && !state.ok && (

{state.message}

)} diff --git a/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-severity-form.tsx b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-severity-form.tsx index 6956ddc5..3c48215d 100644 --- a/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-severity-form.tsx +++ b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-severity-form.tsx @@ -2,7 +2,7 @@ import type React from "react"; import { useActionState } from "react"; -import { Button } from "~/components/ui/button"; +import { Loader2 } from "lucide-react"; import { updateIssueSeverityAction, type UpdateIssueSeverityResult, @@ -15,8 +15,9 @@ interface UpdateIssueSeverityFormProps { } const severityOptions: { value: IssueSeverity; label: string }[] = [ + { value: "cosmetic", label: "Cosmetic" }, { value: "minor", label: "Minor" }, - { value: "playable", label: "Playable" }, + { value: "major", label: "Major" }, { value: "unplayable", label: "Unplayable" }, ]; @@ -32,25 +33,32 @@ export function UpdateIssueSeverityForm({ return ( - - +
+ + {isPending && ( +
+ +
+ )} +
{state && !state.ok && (

{state.message}

)} diff --git a/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-status-form.tsx b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-status-form.tsx index 2374169d..2447c04a 100644 --- a/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-status-form.tsx +++ b/src/app/(app)/m/[initials]/i/[issueNumber]/update-issue-status-form.tsx @@ -2,20 +2,22 @@ import type React from "react"; import { useActionState } from "react"; -import { Button } from "~/components/ui/button"; +import { Loader2 } from "lucide-react"; import { updateIssueStatusAction, type UpdateIssueStatusResult, } from "~/app/(app)/issues/actions"; -import { type IssueStatus } from "~/lib/types"; +import { + getIssueStatusLabel, + STATUS_OPTIONS as statusOptions, +} from "~/lib/issues/status"; +import type { IssueStatus } from "~/lib/types"; interface UpdateIssueStatusFormProps { issueId: string; currentStatus: IssueStatus; } -const statusOptions: IssueStatus[] = ["new", "in_progress", "resolved"]; - export function UpdateIssueStatusForm({ issueId, currentStatus, @@ -28,27 +30,32 @@ export function UpdateIssueStatusForm({ return ( - - +
+ + {isPending && ( +
+ +
+ )} +
{state?.ok && (

Status updated successfully diff --git a/src/app/(app)/m/[initials]/page.tsx b/src/app/(app)/m/[initials]/page.tsx index f56ffb70..65fe04b3 100644 --- a/src/app/(app)/m/[initials]/page.tsx +++ b/src/app/(app)/m/[initials]/page.tsx @@ -1,7 +1,7 @@ import type React from "react"; import { notFound, redirect } from "next/navigation"; import { getUnifiedUsers } from "~/lib/users/queries"; -import type { UnifiedUser } from "~/lib/types"; +import type { UnifiedUser, IssueStatus } from "~/lib/types"; import { cn } from "~/lib/utils"; import Link from "next/link"; import { createClient } from "~/lib/supabase/server"; @@ -14,10 +14,8 @@ import { getMachineStatusStyles, type IssueForStatus, } from "~/lib/machines/status"; -import { - getIssuePriorityLabel, - getIssuePriorityStyles, -} from "~/lib/issues/status"; +import { CLOSED_STATUSES } from "~/lib/issues/status"; +import type { Issue } from "~/lib/types"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; @@ -25,10 +23,10 @@ import { ArrowLeft, Calendar, Plus } from "lucide-react"; import { headers } from "next/headers"; import { resolveRequestUrl } from "~/lib/url"; import { UpdateMachineForm } from "./update-machine-form"; -import { formatIssueId } from "~/lib/issues/utils"; import { QrCodeDialog } from "./qr-code-dialog"; import { buildMachineReportUrl } from "~/lib/machines/report-url"; import { generateQrPngDataUrl } from "~/lib/machines/qr"; +import { IssueCard } from "~/components/issues/IssueCard"; /** * Machine Detail Page (Protected Route) @@ -67,6 +65,8 @@ export default async function MachineDetailPage({ status: true, severity: true, priority: true, + consistency: true, + machineInitials: true, createdAt: true, }, orderBy: (issues, { desc }) => [desc(issues.createdAt)], @@ -108,7 +108,10 @@ export default async function MachineDetailPage({ const qrDataUrl = await generateQrPngDataUrl(reportUrl); const openIssues = machine.issues.filter( - (issue) => issue.status !== "resolved" + (issue) => + !(CLOSED_STATUSES as readonly string[]).includes( + issue.status as IssueStatus + ) ); return ( @@ -168,12 +171,12 @@ export default async function MachineDetailPage({ {/* Content */}

-
-
- {/* Machine Info Card */} - +
+ {/* Sidebar - Machine Info (4 cols) */} +
+ - + Machine Information - + -
- {/* Status */} -
-

- Status -

- - {getMachineStatusLabel(machineStatus)} - -
- {/* Open Issues Count */} -
-

- Open Issues -

-

- {openIssues.length} -

-
- - {/* Created Date */} -
-

- Added Date -

-
- -

- {new Date(machine.createdAt).toLocaleDateString( - undefined, - { - year: "numeric", - month: "long", - day: "numeric", - } +

+ {/* Status & Issues Count Row */} +
+
+

+ Status +

+ + {getMachineStatusLabel(machineStatus)} + +
+ +
+

+ Open Issues +

+

+ {openIssues.length}

- {/* Total Issues */} -
-

- Total Issues -

-

- {machine.issues.length} -

+ {/* Date & Total Row */} +
+
+

+ Added Date +

+
+ +

+ {new Date(machine.createdAt).toLocaleDateString( + undefined, + { + month: "short", + year: "numeric", + } + )} +

+
+
+ +
+

+ Total Issues +

+

+ {machine.issues.length} +

+
- {/* Issues Section */} - - -
- + {/* Main Content - Issues (8 cols) */} +
+ + + Issues -
- {machine.issues.length > 0 ? ( +
+ {machine.issues.length > 5 && ( - ) : null} -
-
-
- - {openIssues.length === 0 ? ( - // Empty state -
-

- No issues reported yet -

-

- Report an issue to get started -

+ )}
- ) : ( - // Issues list -
- {openIssues.map((issue) => ( - -
-
-
- - {formatIssueId( - machine.initials, - issue.issueNumber - )} - -

- {issue.title} -

-
-
- - {new Date(issue.createdAt).toLocaleDateString()} - - - {issue.severity} - - - {getIssuePriorityLabel(issue.priority)} - - - {issue.status} - -
-
+ + + {openIssues.length === 0 ? ( +
+
+ +
+

+ No open issues +

+

+ The game is operational. Great job! +

+
+ ) : ( +
+ {openIssues.slice(0, 50).map((issue) => ( + + ))} + + {openIssues.length > 5 && ( +
+

+ Showing top 5 issues. Use "View All" to see the full + list. +

- - ))} -
- )} -
- + )} +
+ )} + + +
diff --git a/src/app/(app)/m/page.tsx b/src/app/(app)/m/page.tsx index 7d1612bd..be39a10c 100644 --- a/src/app/(app)/m/page.tsx +++ b/src/app/(app)/m/page.tsx @@ -11,6 +11,7 @@ import { getMachineStatusStyles, type IssueForStatus, } from "~/lib/machines/status"; +import { CLOSED_STATUSES } from "~/lib/issues/status"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; @@ -57,7 +58,7 @@ export default async function MachinesPage(): Promise { const machinesWithStatus = allMachines.map((machine) => { const status = deriveMachineStatus(machine.issues as IssueForStatus[]); const openIssuesCount = machine.issues.filter( - (issue) => issue.status !== "resolved" + (issue) => !(CLOSED_STATUSES as readonly string[]).includes(issue.status) ).length; return { @@ -155,7 +156,9 @@ export default async function MachinesPage(): Promise { machine.issues.some( (i) => i.severity === "unplayable" && - i.status !== "resolved" + !( + CLOSED_STATUSES as readonly string[] + ).includes(i.status) ) ? "text-destructive" : "text-muted-foreground" diff --git a/src/app/globals.css b/src/app/globals.css index a4135fe9..2509a7e8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -31,7 +31,6 @@ /* Status Specific */ --color-status-new: #4ade80; --color-status-in-progress: #d946ef; - --color-status-resolved: #22c55e; --color-status-unplayable: #ef4444; /* Semantic Status Colors */ @@ -124,7 +123,7 @@ @apply text-xl font-semibold tracking-tight; } p { - @apply leading-7 [&:not(:first-child)]:mt-4; + @apply leading-7 not-first:mt-4; } a { @apply transition-colors; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e1360ea3..82d32bf7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,8 +6,6 @@ import { ClientLogger } from "~/components/dev/client-logger"; import { SentryInitializer } from "~/components/SentryInitializer"; -import { PreBetaBanner } from "~/components/layout/PreBetaBanner"; - export const metadata: Metadata = { title: "PinPoint - Pinball Machine Issue Tracking", description: @@ -26,7 +24,6 @@ export default function RootLayout({ {isDevelopment && } -
{children}
diff --git a/src/app/report/actions.ts b/src/app/report/actions.ts index 9c33f270..4d9c7c60 100644 --- a/src/app/report/actions.ts +++ b/src/app/report/actions.ts @@ -72,6 +72,7 @@ export async function submitPublicIssueAction( firstName, lastName, priority, + consistency, } = parsedValue.data; // 3. Resolve reporter @@ -194,6 +195,7 @@ export async function submitPublicIssueAction( machineInitials: machine.initials, severity, priority: finalPriority, + consistency, reportedBy, unconfirmedReportedBy, }); diff --git a/src/app/report/schemas.ts b/src/app/report/schemas.ts index 447b6723..0be8f966 100644 --- a/src/app/report/schemas.ts +++ b/src/app/report/schemas.ts @@ -12,14 +12,17 @@ export const publicIssueSchema = z.object({ .trim() .max(5000, "Description is too long") .optional(), - severity: z.enum(["minor", "playable", "unplayable"], { + severity: z.enum(["cosmetic", "minor", "major", "unplayable"], { message: "Select a severity", }), priority: z - .enum(["low", "medium", "high", "critical"], { + .enum(["low", "medium", "high"], { message: "Select a priority", }) .optional(), + consistency: z.enum(["intermittent", "frequent", "constant"], { + message: "Select consistency", + }), firstName: z.string().trim().max(100, "First name too long").optional(), lastName: z.string().trim().max(100, "Last name too long").optional(), email: z diff --git a/src/app/report/unified-report-form.tsx b/src/app/report/unified-report-form.tsx index 4612213c..068f0d70 100644 --- a/src/app/report/unified-report-form.tsx +++ b/src/app/report/unified-report-form.tsx @@ -62,8 +62,9 @@ export function UnifiedReportForm({ ); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); - const [severity, setSeverity] = useState(""); + const [severity, setSeverity] = useState("minor"); const [priority, setPriority] = useState("medium"); + const [consistency, setConsistency] = useState("intermittent"); const [state, formAction, isPending] = useActionState( submitPublicIssueAction, @@ -88,6 +89,7 @@ export function UnifiedReportForm({ description: string; severity: string; priority: string; + consistency: string; }>; // Only restore machineId if not provided via prop or URL already @@ -107,6 +109,7 @@ export function UnifiedReportForm({ if (parsed.description) setDescription(parsed.description); if (parsed.severity) setSeverity(parsed.severity); if (parsed.priority) setPriority(parsed.priority); + if (parsed.consistency) setConsistency(parsed.consistency); } catch { // Clear corrupted localStorage window.localStorage.removeItem("report_form_state"); @@ -123,12 +126,13 @@ export function UnifiedReportForm({ description, severity, priority, + consistency, }; window.localStorage.setItem( "report_form_state", JSON.stringify(stateToSave) ); - }, [selectedMachineId, title, description, severity, priority]); + }, [selectedMachineId, title, description, severity, priority, consistency]); // Cleanup: Clear storage on success (handled by redirect usually, but good for robust logic if no redirect) // Actually, review says: "cleanup effect will never execute because action redirects". @@ -186,6 +190,7 @@ export function UnifiedReportForm({ id="machineId" name="machineId" data-testid="machine-select" + aria-label="Select Machine" required value={selectedMachineId} onChange={(e) => { @@ -256,17 +261,21 @@ export function UnifiedReportForm({
+
+ + +
+ {isAdminOrMember && (
)} diff --git a/src/app/report/validation.ts b/src/app/report/validation.ts index 6883b8d9..00f019f1 100644 --- a/src/app/report/validation.ts +++ b/src/app/report/validation.ts @@ -36,6 +36,7 @@ export function parsePublicIssueForm( lastName: toOptionalString(formData.get("lastName")), email: toOptionalString(formData.get("email")), priority: toOptionalString(formData.get("priority")), + consistency: toOptionalString(formData.get("consistency")), }; const validation = publicIssueSchema.safeParse(rawData); diff --git a/src/components/IssueFilters.tsx b/src/components/IssueFilters.tsx deleted file mode 100644 index 48ed51df..00000000 --- a/src/components/IssueFilters.tsx +++ /dev/null @@ -1,169 +0,0 @@ -"use client"; - -import type React from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import Link from "next/link"; -import { Button } from "~/components/ui/button"; - -interface IssueFiltersProps { - machines: { id: string; name: string }[]; - users: { id: string; name: string }[]; -} - -/** - * Issue Filters Component (Client Component) - * - * Provides filtering dropdowns for issues list page. - * Updates URL search params to filter issues server-side. - */ -export function IssueFilters({ - machines, - users, -}: IssueFiltersProps): React.JSX.Element { - const router = useRouter(); - const searchParams = useSearchParams(); - - const machineId = searchParams.get("machineId") ?? ""; - const status = searchParams.get("status") ?? ""; - const severity = searchParams.get("severity") ?? ""; - const priority = searchParams.get("priority") ?? ""; - const assignedTo = searchParams.get("assignedTo") ?? ""; - - const hasFilters = machineId || status || severity || priority || assignedTo; - - const updateFilter = (key: string, value: string): void => { - const params = new URLSearchParams(searchParams.toString()); - if (value) { - params.set(key, value); - } else { - params.delete(key); - } - router.push(`/issues?${params.toString()}`); - }; - - return ( -
- {/* Machine Filter */} -
- - -
- - {/* Status Filter */} -
- - -
- - {/* Severity Filter */} -
- - -
- - {/* Priority Filter */} -
- - -
- - {/* Assignee Filter */} -
- - -
- - {/* Clear Filters */} - {hasFilters && ( - - )} -
- ); -} diff --git a/src/components/issues/IssueBadge.tsx b/src/components/issues/IssueBadge.tsx new file mode 100644 index 00000000..b22ae08b --- /dev/null +++ b/src/components/issues/IssueBadge.tsx @@ -0,0 +1,124 @@ +"use client"; + +import type React from "react"; +import { Badge } from "~/components/ui/badge"; +import { + getIssueStatusIcon, + getIssueStatusLabel, + getIssueSeverityLabel, + getIssuePriorityLabel, + getIssueConsistencyLabel, + STATUS_STYLES, + SEVERITY_STYLES, + PRIORITY_STYLES, + CONSISTENCY_STYLES, + ISSUE_FIELD_ICONS, + ISSUE_BADGE_WIDTH, + ISSUE_BADGE_MIN_WIDTH_STRIP, +} from "~/lib/issues/status"; +import type { + IssueStatus, + IssueSeverity, + IssuePriority, + IssueConsistency, +} from "~/lib/types"; +import { cn } from "~/lib/utils"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/ui/tooltip"; + +type IssueBadgeProps = { + variant?: "normal" | "strip"; + size?: "normal" | "lg"; + className?: string; + showTooltip?: boolean; +} & ( + | { type: "status"; value: IssueStatus } + | { type: "severity"; value: IssueSeverity } + | { type: "priority"; value: IssuePriority } + | { type: "consistency"; value: IssueConsistency } +); + +export function IssueBadge({ + type, + value, + variant = "normal", + size = "normal", + className, + showTooltip = true, +}: IssueBadgeProps): React.JSX.Element { + let label = ""; + let styles = ""; + let Icon: React.ElementType | null = null; + + switch (type) { + case "status": + label = getIssueStatusLabel(value); + styles = STATUS_STYLES[value]; + Icon = getIssueStatusIcon(value); + break; + case "severity": + label = getIssueSeverityLabel(value); + styles = SEVERITY_STYLES[value]; + Icon = ISSUE_FIELD_ICONS.severity; + break; + case "priority": + label = getIssuePriorityLabel(value); + styles = PRIORITY_STYLES[value]; + Icon = ISSUE_FIELD_ICONS.priority; + break; + case "consistency": + label = getIssueConsistencyLabel(value); + styles = CONSISTENCY_STYLES[value]; + Icon = ISSUE_FIELD_ICONS.consistency; + break; + } + + const badgeElement = ( + +
+ +
+ {label} +
+ ); + + if (!showTooltip) return badgeElement; + + return ( + + + {badgeElement} + + {type}:{" "} + {label} + + + + ); +} diff --git a/src/components/issues/IssueBadgeGrid.tsx b/src/components/issues/IssueBadgeGrid.tsx new file mode 100644 index 00000000..248c2c93 --- /dev/null +++ b/src/components/issues/IssueBadgeGrid.tsx @@ -0,0 +1,60 @@ +"use client"; + +import type React from "react"; +import { cn } from "~/lib/utils"; +import { IssueBadge } from "~/components/issues/IssueBadge"; +import type { Issue } from "~/lib/types"; + +interface IssueBadgeGridProps { + issue: Pick; + variant?: "normal" | "strip"; + size?: "normal" | "lg"; + className?: string; + showPriority?: boolean; // Default true, but maybe hide for guests externally +} + +export function IssueBadgeGrid({ + issue, + variant = "normal", + size = "normal", + className, + showPriority = true, +}: IssueBadgeGridProps): React.JSX.Element { + const gridClass = cn( + variant === "strip" + ? "flex flex-wrap gap-2" + : "grid grid-cols-1 sm:grid-cols-2 gap-2", + className + ); + + return ( +
+ + {showPriority && ( + + )} + + +
+ ); +} diff --git a/src/components/issues/IssueCard.tsx b/src/components/issues/IssueCard.tsx new file mode 100644 index 00000000..fcb16540 --- /dev/null +++ b/src/components/issues/IssueCard.tsx @@ -0,0 +1,103 @@ +"use client"; + +import type React from "react"; +import Link from "next/link"; +import { cn } from "~/lib/utils"; +import { Card, CardHeader, CardTitle } from "~/components/ui/card"; +import { IssueBadgeGrid } from "~/components/issues/IssueBadgeGrid"; +import { formatIssueId } from "~/lib/issues/utils"; +import { CLOSED_STATUSES } from "~/lib/issues/status"; +import type { Issue } from "~/lib/types"; + +interface IssueCardProps { + issue: Pick< + Issue, + | "id" + | "title" + | "status" + | "severity" + | "priority" + | "consistency" + | "machineInitials" + | "issueNumber" + | "createdAt" + >; + machine: { name: string }; + variant?: "normal" | "compact"; + className?: string; + showReporter?: boolean; + reporterName?: string; + dataTestId?: string; +} + +export function IssueCard({ + issue, + machine, + variant = "normal", + className, + showReporter = false, + reporterName, + dataTestId, +}: IssueCardProps): React.JSX.Element { + const isClosed = (CLOSED_STATUSES as readonly string[]).includes( + issue.status + ); + + const initials = issue.machineInitials; + + return ( + + + +
+
+ + + {formatIssueId(initials, issue.issueNumber)} + {" "} + {issue.title} + +
+ + {machine.name} + + {showReporter && ( + + Reported by {reporterName ?? "Anonymous"} •{" "} + {new Date(issue.createdAt).toLocaleDateString()} + + )} + {!showReporter && ( + {new Date(issue.createdAt).toLocaleDateString()} + )} +
+
+
+ +
+
+
+
+ + ); +} diff --git a/src/components/issues/IssueFilters.tsx b/src/components/issues/IssueFilters.tsx index c3a11d9a..84e5e0fb 100644 --- a/src/components/issues/IssueFilters.tsx +++ b/src/components/issues/IssueFilters.tsx @@ -11,7 +11,6 @@ import { SelectTrigger, SelectValue, } from "~/components/ui/select"; -// import { cn } from "~/lib/utils"; // Removed unused import interface MachineOption { initials: string; @@ -54,20 +53,28 @@ export function IssueFilters({ return (
{/* Status Toggle (Open/Closed) */} -
+
@@ -78,13 +85,17 @@ export function IssueFilters({ value={currentFilters.severity ?? "all"} onValueChange={(val) => updateFilter("severity", val)} > - + All Severities + Cosmetic Minor - Playable + Major Unplayable @@ -94,7 +105,10 @@ export function IssueFilters({ value={currentFilters.priority ?? "all"} onValueChange={(val) => updateFilter("priority", val)} > - + @@ -102,7 +116,6 @@ export function IssueFilters({ Low Medium High - Critical @@ -111,7 +124,10 @@ export function IssueFilters({ value={currentFilters.machine ?? "all"} onValueChange={(val) => updateFilter("machine", val)} > - + diff --git a/src/components/issues/IssueRow.tsx b/src/components/issues/IssueRow.tsx index e03b1cf6..30fd5d3f 100644 --- a/src/components/issues/IssueRow.tsx +++ b/src/components/issues/IssueRow.tsx @@ -1,19 +1,24 @@ import type React from "react"; import Link from "next/link"; import { cn } from "~/lib/utils"; -import { Badge } from "~/components/ui/badge"; -import { StatusIndicator } from "~/components/issues/StatusIndicator"; +import { IssueBadgeGrid } from "~/components/issues/IssueBadgeGrid"; +import { CLOSED_STATUSES } from "~/lib/issues/status"; +import { formatIssueId } from "~/lib/issues/utils"; +import type { Issue } from "~/lib/types"; interface IssueRowProps { - issue: { - id: string; - issueNumber: number; - title: string; - status: "new" | "in_progress" | "resolved"; - severity: "minor" | "playable" | "unplayable"; - priority: "low" | "medium" | "high" | "critical"; - createdAt: Date; - machineInitials: string; + issue: Pick< + Issue, + | "id" + | "issueNumber" + | "title" + | "status" + | "severity" + | "priority" + | "consistency" + | "createdAt" + | "machineInitials" + > & { machine: { name: string; } | null; @@ -24,23 +29,19 @@ interface IssueRowProps { } export function IssueRow({ issue }: IssueRowProps): React.JSX.Element { - const severityColor = { - minor: "bg-blue-900/30 text-blue-300", - playable: "bg-yellow-900/30 text-yellow-300", - unplayable: "bg-red-900/30 text-red-300", - }; - return (
- +
@@ -51,28 +52,11 @@ export function IssueRow({ issue }: IssueRowProps): React.JSX.Element { > {issue.title} - - {issue.severity} - - {issue.priority === "critical" && ( - - critical - - )}
- {issue.machineInitials}-{issue.issueNumber} + {formatIssueId(issue.machineInitials, issue.issueNumber)} diff --git a/src/components/issues/IssueSidebar.tsx b/src/components/issues/IssueSidebar.tsx index 239a0ff2..f2a13c29 100644 --- a/src/components/issues/IssueSidebar.tsx +++ b/src/components/issues/IssueSidebar.tsx @@ -48,8 +48,9 @@ export function IssueSidebar({ Reporter
- {issue.reportedByUser?.name.slice(0, 1).toUpperCase() ?? - "U"} + {(issue.reportedByUser?.name ?? "U") + .slice(0, 1) + .toUpperCase()}
{issue.reportedByUser?.name ?? "Unknown user"} diff --git a/src/components/issues/RecentIssuesPanel.tsx b/src/components/issues/RecentIssuesPanel.tsx index 1a55d304..8cece986 100644 --- a/src/components/issues/RecentIssuesPanel.tsx +++ b/src/components/issues/RecentIssuesPanel.tsx @@ -1,17 +1,19 @@ import React from "react"; import Link from "next/link"; -import { - getIssueStatusLabel, - getIssueStatusStyles, - isIssueStatus, -} from "~/lib/issues/status"; -import { Badge } from "~/components/ui/badge"; +import { IssueBadge } from "~/components/issues/IssueBadge"; import { cn } from "~/lib/utils"; import { AlertCircle, CheckCircle2 } from "lucide-react"; import { db } from "~/server/db"; import { issues as issuesTable } from "~/server/db/schema"; import { eq, desc } from "drizzle-orm"; import { log } from "~/lib/logger"; +import { getIssueStatusLabel } from "~/lib/issues/status"; +import type { + IssueStatus, + IssueSeverity, + IssuePriority, + IssueConsistency, +} from "~/lib/types"; interface RecentIssuesPanelProps { machineInitials: string; @@ -45,11 +47,15 @@ export async function RecentIssuesPanel({ id: string; issueNumber: number; title: string; - status: "new" | "in_progress" | "resolved"; + status: IssueStatus; + severity: IssueSeverity; + priority: IssuePriority; + consistency: IssueConsistency; createdAt: Date; }[] = []; try { - issues = await db.query.issues.findMany({ + // Type assertion needed because Drizzle infers status as string, not IssueStatus + issues = (await db.query.issues.findMany({ where: eq(issuesTable.machineInitials, machineInitials), orderBy: [desc(issuesTable.createdAt)], limit: limit, @@ -58,9 +64,12 @@ export async function RecentIssuesPanel({ issueNumber: true, title: true, status: true, + severity: true, + priority: true, + consistency: true, createdAt: true, }, - }); + })) as typeof issues; } catch (err) { log.error( { err, machineInitials }, @@ -119,23 +128,15 @@ export async function RecentIssuesPanel({ key={issue.id} href={`/m/${machineInitials}/i/${issue.issueNumber}`} className="block group" + aria-label={`View issue: ${issue.title} - ${getIssueStatusLabel( + issue.status + )}`} >

{issue.title}

- - {getIssueStatusLabel( - isIssueStatus(issue.status) ? issue.status : "new" - )} - +
))} diff --git a/src/components/issues/SidebarActions.tsx b/src/components/issues/SidebarActions.tsx index deafa8fe..f7d4362f 100644 --- a/src/components/issues/SidebarActions.tsx +++ b/src/components/issues/SidebarActions.tsx @@ -7,6 +7,7 @@ import { AssignIssueForm } from "~/app/(app)/m/[initials]/i/[issueNumber]/assign import { UpdateIssueStatusForm } from "~/app/(app)/m/[initials]/i/[issueNumber]/update-issue-status-form"; import { UpdateIssueSeverityForm } from "~/app/(app)/m/[initials]/i/[issueNumber]/update-issue-severity-form"; import { UpdateIssuePriorityForm } from "~/app/(app)/m/[initials]/i/[issueNumber]/update-issue-priority-form"; +import { UpdateIssueConsistencyForm } from "~/app/(app)/m/[initials]/i/[issueNumber]/update-issue-consistency-form"; interface SidebarActionsProps { issue: IssueWithAllRelations; @@ -63,6 +64,17 @@ export function SidebarActions({ />
+ + {/* Update Consistency */} +
+ +
+ +
+
); } diff --git a/src/components/issues/StatusIndicator.tsx b/src/components/issues/StatusIndicator.tsx deleted file mode 100644 index ecae9231..00000000 --- a/src/components/issues/StatusIndicator.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import type React from "react"; -import { AlertCircle, CheckCircle2, CircleDot } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "~/components/ui/tooltip"; - -interface StatusIndicatorProps { - status: "new" | "in_progress" | "resolved"; -} - -export function StatusIndicator({ - status, -}: StatusIndicatorProps): React.JSX.Element { - const statusConfig = { - new: { - icon: , - label: "New", - description: "This issue has been reported but not yet addressed.", - }, - in_progress: { - icon: , - label: "In Progress", - description: "Work is currently being done to resolve this issue.", - }, - resolved: { - icon: , - label: "Resolved", - description: "This issue has been fixed.", - }, - }; - - const config = statusConfig[status]; - - return ( - - - -
- {config.icon} -
-
- -
-

{config.label}

-

- {config.description} -

-
-
-
-
- ); -} diff --git a/src/components/layout/PreBetaBanner.tsx b/src/components/layout/PreBetaBanner.tsx deleted file mode 100644 index 54cb5fab..00000000 --- a/src/components/layout/PreBetaBanner.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type React from "react"; - -export function PreBetaBanner(): React.JSX.Element { - return ( -
-

- 🚧 PinPoint Pre-Beta Notice: Development in progress. Database resets - may occur. -

-
- ); -} diff --git a/src/lib/issues/queries.ts b/src/lib/issues/queries.ts index 63215509..234785d3 100644 --- a/src/lib/issues/queries.ts +++ b/src/lib/issues/queries.ts @@ -1,5 +1,11 @@ import { cache } from "react"; -import { type IssueListItem } from "~/lib/types"; +import type { + IssueListItem, + IssueStatus, + IssueSeverity, + IssuePriority, +} from "~/lib/types"; +import { ALL_ISSUE_STATUSES } from "~/lib/issues/status"; import { db } from "~/server/db"; import { issues } from "~/server/db/schema"; import { eq, and, desc, isNull, inArray, type SQL } from "drizzle-orm"; @@ -25,35 +31,25 @@ export const getIssues = cache( if (status) { const statuses = Array.isArray(status) ? status : [status]; - // Filter out invalid statuses to be safe, though TS handles most - const validStatuses = statuses.filter((s) => - ["new", "in_progress", "resolved"].includes(s) + // Type-safe filtering using imported constants from single source of truth + const validStatuses = statuses.filter((s): s is IssueStatus => + ALL_ISSUE_STATUSES.includes(s as IssueStatus) ); if (validStatuses.length > 0) { - conditions.push( - inArray( - issues.status, - validStatuses as ("new" | "in_progress" | "resolved")[] - ) - ); + conditions.push(inArray(issues.status, validStatuses)); } } if ( severity && - (severity === "minor" || - severity === "playable" || - severity === "unplayable") + ["cosmetic", "minor", "major", "unplayable"].includes(severity) ) { - conditions.push(eq(issues.severity, severity)); + conditions.push(eq(issues.severity, severity as IssueSeverity)); } - if ( - priority && - (priority === "low" || priority === "medium" || priority === "high") - ) { - conditions.push(eq(issues.priority, priority)); + if (priority && ["low", "medium", "high"].includes(priority)) { + conditions.push(eq(issues.priority, priority as IssuePriority)); } if (assignedTo === "unassigned") { @@ -63,7 +59,8 @@ export const getIssues = cache( } // Query issues with filters - return await db.query.issues.findMany({ + // Type assertion needed because Drizzle infers status as string, not IssueStatus + return (await db.query.issues.findMany({ where: conditions.length > 0 ? and(...conditions) : undefined, orderBy: desc(issues.createdAt), with: { @@ -87,6 +84,6 @@ export const getIssues = cache( }, }, }, - }); + })) as IssueListItem[]; } ); diff --git a/src/lib/issues/status.test.ts b/src/lib/issues/status.test.ts index f63b1032..cab37cf3 100644 --- a/src/lib/issues/status.test.ts +++ b/src/lib/issues/status.test.ts @@ -1,79 +1,67 @@ import { describe, it, expect } from "vitest"; import { - isIssueStatus, - isIssueSeverity, getIssueStatusLabel, - getIssueSeverityLabel, - getIssueStatusStyles, - getIssueSeverityStyles, + getIssueStatusIcon, + STATUS_STYLES, + SEVERITY_STYLES, + ALL_STATUS_OPTIONS, + ISSUE_STATUSES, } from "~/lib/issues/status"; +import { Circle, CircleDot, Disc } from "lucide-react"; describe("Issue Status Utilities", () => { - describe("isIssueStatus", () => { - it("should return true for valid statuses", () => { - expect(isIssueStatus("new")).toBe(true); - expect(isIssueStatus("in_progress")).toBe(true); - expect(isIssueStatus("resolved")).toBe(true); + describe("getIssueStatusIcon", () => { + it("should return Circle for new statuses", () => { + expect(getIssueStatusIcon("new")).toBe(Circle); + expect(getIssueStatusIcon("confirmed")).toBe(Circle); }); - it("should return false for invalid statuses", () => { - expect(isIssueStatus("archived")).toBe(false); - expect(isIssueStatus("")).toBe(false); - expect(isIssueStatus(null)).toBe(false); - expect(isIssueStatus(undefined)).toBe(false); - expect(isIssueStatus(123)).toBe(false); - }); - }); - - describe("isIssueSeverity", () => { - it("should return true for valid severities", () => { - expect(isIssueSeverity("minor")).toBe(true); - expect(isIssueSeverity("playable")).toBe(true); - expect(isIssueSeverity("unplayable")).toBe(true); + it("should return CircleDot for in-progress statuses", () => { + expect(getIssueStatusIcon(ISSUE_STATUSES.IN_PROGRESS)).toBe(CircleDot); + expect(getIssueStatusIcon(ISSUE_STATUSES.WAIT_OWNER)).toBe(CircleDot); }); - it("should return false for invalid severities", () => { - expect(isIssueSeverity("critical")).toBe(false); - expect(isIssueSeverity("")).toBe(false); - expect(isIssueSeverity(null)).toBe(false); + it("should return Disc for closed statuses", () => { + expect(getIssueStatusIcon("fixed")).toBe(Disc); + expect(getIssueStatusIcon("duplicate")).toBe(Disc); }); }); describe("getIssueStatusLabel", () => { it("should return correct labels", () => { expect(getIssueStatusLabel("new")).toBe("New"); + expect(getIssueStatusLabel("confirmed")).toBe("Confirmed"); expect(getIssueStatusLabel("in_progress")).toBe("In Progress"); - expect(getIssueStatusLabel("resolved")).toBe("Resolved"); + expect(getIssueStatusLabel("need_parts")).toBe("Need Parts"); + expect(getIssueStatusLabel("need_help")).toBe("Need Help"); + expect(getIssueStatusLabel("wait_owner")).toBe("Pending Owner"); + expect(getIssueStatusLabel("fixed")).toBe("Fixed"); + expect(getIssueStatusLabel("wai")).toBe("As Intended"); + expect(getIssueStatusLabel("wont_fix")).toBe("Won't Fix"); + expect(getIssueStatusLabel("no_repro")).toBe("No Repro"); + expect(getIssueStatusLabel("duplicate")).toBe("Duplicate"); }); }); - describe("getIssueSeverityLabel", () => { - it("should return correct labels", () => { - expect(getIssueSeverityLabel("minor")).toBe("Minor"); - expect(getIssueSeverityLabel("playable")).toBe("Playable"); - expect(getIssueSeverityLabel("unplayable")).toBe("Unplayable"); + describe("Constants", () => { + it("ALL_STATUS_OPTIONS should contain all statuses", () => { + expect(ALL_STATUS_OPTIONS).toContain("new"); + expect(ALL_STATUS_OPTIONS).toContain("fixed"); + expect(ALL_STATUS_OPTIONS.length).toBe(11); }); - }); - describe("getIssueStatusStyles", () => { - it("should return styles for all statuses", () => { - expect(getIssueStatusStyles("new")).toContain("bg-status-new/20"); - expect(getIssueStatusStyles("in_progress")).toContain( - "bg-status-in-progress/20" - ); - expect(getIssueStatusStyles("resolved")).toContain( - "bg-status-resolved/20" - ); + it("STATUS_STYLES should have styles for all statuses", () => { + ALL_STATUS_OPTIONS.forEach((status) => { + expect(STATUS_STYLES[status]).toBeDefined(); + expect(typeof STATUS_STYLES[status]).toBe("string"); + }); }); - }); - describe("getIssueSeverityStyles", () => { - it("should return styles for all severities", () => { - expect(getIssueSeverityStyles("minor")).toContain("bg-muted/50"); - expect(getIssueSeverityStyles("playable")).toContain("bg-warning/20"); - expect(getIssueSeverityStyles("unplayable")).toContain( - "bg-status-unplayable/20" - ); + it("SEVERITY_STYLES should have styles for all severities", () => { + const severities = ["cosmetic", "minor", "major", "unplayable"] as const; + severities.forEach((sev) => { + expect(SEVERITY_STYLES[sev]).toBeDefined(); + }); }); }); }); diff --git a/src/lib/issues/status.ts b/src/lib/issues/status.ts index 9f2155a6..8151ce75 100644 --- a/src/lib/issues/status.ts +++ b/src/lib/issues/status.ts @@ -1,121 +1,335 @@ -import { type IssuePriority } from "~/lib/types"; - -export type IssueStatus = "new" | "in_progress" | "resolved"; -export type IssueSeverity = "minor" | "playable" | "unplayable"; +import { + Circle, + CircleDot, + Disc, + AlertTriangle, + TrendingUp, + Repeat, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import type { + IssueSeverity, + IssuePriority, + IssueConsistency, +} from "~/lib/types"; /** - * Type guard for IssueStatus - * Validates that a value is a valid issue status + * Single Source of Truth for Issue Status Values + * Based on _issue-status-redesign/README.md - Final design with 11 statuses + * + * All status-related code imports from this file to ensure consistency. + * Database schema, Zod validation, queries, and UI filters all derive from these constants. */ -export function isIssueStatus(value: unknown): value is IssueStatus { - return ( - typeof value === "string" && - ["new", "in_progress", "resolved"].includes(value) - ); -} + +// Named constants for type-safe access +export const ISSUE_STATUSES = { + // New group (2) + NEW: "new", + CONFIRMED: "confirmed", + + // In Progress group (4) + WAIT_OWNER: "wait_owner", + IN_PROGRESS: "in_progress", + NEED_PARTS: "need_parts", + NEED_HELP: "need_help", + + // Closed group (5) + FIXED: "fixed", + WONT_FIX: "wont_fix", + WAI: "wai", + NO_REPRO: "no_repro", + DUPLICATE: "duplicate", +} as const; + +// Array of all valid statuses (for runtime validation) +export const ALL_ISSUE_STATUSES = Object.values(ISSUE_STATUSES); + +// Type-safe array with specific literal types (for TypeScript and Drizzle) +export const ISSUE_STATUS_VALUES = [ + "new", + "confirmed", + "in_progress", + "need_parts", + "need_help", + "wait_owner", + "fixed", + "wont_fix", + "wai", + "no_repro", + "duplicate", +] as const; + +// Derive the type from the array (this is the canonical IssueStatus type) +export type IssueStatus = (typeof ISSUE_STATUS_VALUES)[number]; + +// Shared styling constants +export const ISSUE_BADGE_WIDTH = "w-[120px]"; +export const ISSUE_BADGE_MIN_WIDTH_STRIP = "min-w-[100px]"; + +// Status groups (using constants) +export const STATUS_GROUPS = { + new: [ISSUE_STATUSES.NEW, ISSUE_STATUSES.CONFIRMED], + in_progress: [ + ISSUE_STATUSES.IN_PROGRESS, + ISSUE_STATUSES.NEED_PARTS, + ISSUE_STATUSES.NEED_HELP, + ISSUE_STATUSES.WAIT_OWNER, + ], + closed: [ + ISSUE_STATUSES.FIXED, + ISSUE_STATUSES.WONT_FIX, + ISSUE_STATUSES.WAI, + ISSUE_STATUSES.NO_REPRO, + ISSUE_STATUSES.DUPLICATE, + ], +} as const; + +// Single-level exports for better re-use +export const NEW_STATUSES = STATUS_GROUPS.new; +export const IN_PROGRESS_STATUSES = STATUS_GROUPS.in_progress; +export const CLOSED_STATUSES = STATUS_GROUPS.closed; +export const OPEN_STATUS_GROUPS = ["new", "in_progress"] as const; + +// Convenience exports for common groupings +export const OPEN_STATUSES = [ + ...STATUS_GROUPS.new, + ...STATUS_GROUPS.in_progress, +] as const; + +export const ALL_STATUS_OPTIONS: IssueStatus[] = [ + ...STATUS_GROUPS.new, + ...STATUS_GROUPS.in_progress, + ...STATUS_GROUPS.closed, +]; + +export const STATUS_OPTIONS = ALL_STATUS_OPTIONS; // Alias for form use /** - * Type guard for IssueSeverity - * Validates that a value is a valid issue severity + * Field Configuration: One place to rule them all. + * Contains labels, descriptions, icons, and styles for all issue metadata. */ -export function isIssueSeverity(value: unknown): value is IssueSeverity { - return ( - typeof value === "string" && - ["minor", "playable", "unplayable"].includes(value) - ); + +export const STATUS_CONFIG: Record< + IssueStatus, + { label: string; description: string; styles: string; icon: LucideIcon } +> = { + new: { + label: "New", + description: "Just reported, needs triage", + styles: "bg-cyan-500/20 text-cyan-400 border-cyan-500", + icon: Circle, + }, + confirmed: { + label: "Confirmed", + description: "Verified as a actual issue", + styles: "bg-teal-500/20 text-teal-400 border-teal-500", + icon: Circle, + }, + in_progress: { + label: "In Progress", + description: "Active repair underway", + styles: "bg-fuchsia-500/20 text-fuchsia-400 border-fuchsia-500", + icon: CircleDot, + }, + need_parts: { + label: "Need Parts", + description: "Waiting on new parts", + styles: "bg-purple-600/20 text-purple-300 border-purple-600", + icon: CircleDot, + }, + need_help: { + label: "Need Help", + description: "Escalated to expert help", + styles: "bg-pink-500/20 text-pink-400 border-pink-500", + icon: CircleDot, + }, + wait_owner: { + label: "Pending Owner", + description: "Pending owner decision/action", + styles: "bg-purple-500/20 text-purple-400 border-purple-500", + icon: CircleDot, + }, + fixed: { + label: "Fixed", + description: "Issue is resolved", + styles: "bg-green-500/20 text-green-400 border-green-500", + icon: Disc, + }, + wai: { + label: "As Intended", + description: "Working as intended, no action required", + styles: "bg-zinc-500/20 text-zinc-400 border-zinc-500", + icon: Disc, + }, + wont_fix: { + label: "Won't Fix", + description: "Issue can't or won't be fixed", + styles: "bg-zinc-500/20 text-zinc-400 border-zinc-500", + icon: Disc, + }, + no_repro: { + label: "No Repro", + description: "Couldn't reproduce", + styles: "bg-slate-500/20 text-slate-400 border-slate-500", + icon: Disc, + }, + duplicate: { + label: "Duplicate", + description: "Already reported elsewhere", + styles: "bg-neutral-600/20 text-neutral-400 border-neutral-600", + icon: Disc, + }, +}; + +export const SEVERITY_CONFIG: Record< + IssueSeverity, + { label: string; styles: string; icon: LucideIcon } +> = { + cosmetic: { + label: "Cosmetic", + styles: "bg-amber-200/20 text-amber-300 border-amber-500", + icon: AlertTriangle, + }, + minor: { + label: "Minor", + styles: "bg-amber-400/20 text-amber-400 border-amber-500", + icon: AlertTriangle, + }, + major: { + label: "Major", + styles: "bg-amber-500/20 text-amber-500 border-amber-500", + icon: AlertTriangle, + }, + unplayable: { + label: "Unplayable", + styles: "bg-amber-600/20 text-amber-600 border-amber-500", + icon: AlertTriangle, + }, +}; + +export const PRIORITY_CONFIG: Record< + IssuePriority, + { label: string; styles: string; icon: LucideIcon } +> = { + low: { + label: "Low", + styles: "bg-purple-950/50 text-purple-600 border-purple-500", + icon: TrendingUp, + }, + medium: { + label: "Medium", + styles: "bg-purple-900/50 text-purple-400 border-purple-500", + icon: TrendingUp, + }, + high: { + label: "High", + styles: "bg-purple-500/20 text-purple-200 border-purple-500", + icon: TrendingUp, + }, +}; + +export const CONSISTENCY_CONFIG: Record< + IssueConsistency, + { label: string; styles: string; icon: LucideIcon } +> = { + intermittent: { + label: "Intermittent", + styles: "bg-cyan-950/50 text-cyan-600 border-cyan-500", + icon: Repeat, + }, + frequent: { + label: "Frequent", + styles: "bg-cyan-900/50 text-cyan-400 border-cyan-500", + icon: Repeat, + }, + constant: { + label: "Constant", + styles: "bg-cyan-500/20 text-cyan-200 border-cyan-500", + icon: Repeat, + }, +}; + +// Getter functions - direct config access (type-safe, no assertions needed) +export function getIssueStatusIcon(status: IssueStatus): LucideIcon { + return STATUS_CONFIG[status].icon; } -/** - * Get display label for issue status - */ export function getIssueStatusLabel(status: IssueStatus): string { - const labels: Record = { - new: "New", - in_progress: "In Progress", - resolved: "Resolved", - }; - return labels[status]; + return STATUS_CONFIG[status].label; } -/** - * Get display label for issue severity - */ -export function getIssueSeverityLabel(severity: IssueSeverity): string { - const labels: Record = { - minor: "Minor", - playable: "Playable", - unplayable: "Unplayable", - }; - return labels[severity]; +export function getIssueStatusDescription(status: IssueStatus): string { + return STATUS_CONFIG[status].description; } -/** - * Get CSS classes for issue status badge - * Uses Material Design 3 color system from globals.css - */ export function getIssueStatusStyles(status: IssueStatus): string { - const styles: Record = { - new: "bg-status-new/20 text-status-new border-status-new glow-primary", - in_progress: - "bg-status-in-progress/20 text-status-in-progress border-status-in-progress glow-secondary", - resolved: - "bg-status-resolved/20 text-status-resolved border-status-resolved", - }; - return styles[status]; + return STATUS_CONFIG[status].styles; } -/** - * Get CSS classes for issue severity badge - * Uses Material Design 3 color system from globals.css - */ -export function getIssueSeverityStyles(severity: IssueSeverity): string { - const styles: Record = { - minor: "bg-muted/50 text-muted-foreground border-border", - playable: "bg-warning/20 text-warning border-warning glow-warning", - unplayable: - "bg-status-unplayable/20 text-status-unplayable border-status-unplayable glow-destructive", - }; - return styles[severity]; +export function getIssueSeverityLabel(severity: IssueSeverity): string { + return SEVERITY_CONFIG[severity].label; } -/** - * Type guard for IssuePriority - */ -export function isIssuePriority(value: unknown): value is IssuePriority { - return ( - typeof value === "string" && - ["low", "medium", "high", "critical"].includes(value) - ); +export function getIssuePriorityLabel(priority: IssuePriority): string { + return PRIORITY_CONFIG[priority].label; } -/** - * Get display label for issue priority - */ -export function getIssuePriorityLabel(priority: IssuePriority): string { - const labels: Record = { - low: "Low", - medium: "Medium", - high: "High", - critical: "Critical", - }; - return labels[priority]; +export function getIssueConsistencyLabel( + consistency: IssueConsistency +): string { + return CONSISTENCY_CONFIG[consistency].label; +} + +export function getIssueSeverityStyles(severity: IssueSeverity): string { + return SEVERITY_CONFIG[severity].styles; } -/** - * Get CSS classes for issue priority badge - * Reuses severity color scheme: - * Low -> Minor (Green/Muted) - * Medium -> Playable (Yellow/Warning) - * High -> Unplayable (Red/Destructive) - * Critical -> Destructive (Solid Red) - */ export function getIssuePriorityStyles(priority: IssuePriority): string { - const styles: Record = { - low: "bg-muted/50 text-muted-foreground border-border", - medium: "bg-warning/20 text-warning border-warning glow-warning", - high: "bg-status-unplayable/20 text-status-unplayable border-status-unplayable glow-destructive", - critical: - "bg-destructive text-destructive-foreground border-destructive font-bold", - }; - return styles[priority]; + return PRIORITY_CONFIG[priority].styles; +} + +export function getIssueConsistencyStyles( + consistency: IssueConsistency +): string { + return CONSISTENCY_CONFIG[consistency].styles; } + +// Manual export for component use (easier for Tim to read/change) +export const STATUS_STYLES: Record = { + new: STATUS_CONFIG.new.styles, + confirmed: STATUS_CONFIG.confirmed.styles, + wait_owner: STATUS_CONFIG.wait_owner.styles, + in_progress: STATUS_CONFIG.in_progress.styles, + need_parts: STATUS_CONFIG.need_parts.styles, + need_help: STATUS_CONFIG.need_help.styles, + fixed: STATUS_CONFIG.fixed.styles, + wai: STATUS_CONFIG.wai.styles, + wont_fix: STATUS_CONFIG.wont_fix.styles, + no_repro: STATUS_CONFIG.no_repro.styles, + duplicate: STATUS_CONFIG.duplicate.styles, +}; + +export const SEVERITY_STYLES: Record = { + cosmetic: SEVERITY_CONFIG.cosmetic.styles, + minor: SEVERITY_CONFIG.minor.styles, + major: SEVERITY_CONFIG.major.styles, + unplayable: SEVERITY_CONFIG.unplayable.styles, +}; + +export const PRIORITY_STYLES: Record = { + low: PRIORITY_CONFIG.low.styles, + medium: PRIORITY_CONFIG.medium.styles, + high: PRIORITY_CONFIG.high.styles, +}; + +export const CONSISTENCY_STYLES: Record = { + intermittent: CONSISTENCY_CONFIG.intermittent.styles, + frequent: CONSISTENCY_CONFIG.frequent.styles, + constant: CONSISTENCY_CONFIG.constant.styles, +}; + +export const ISSUE_FIELD_ICONS = { + severity: AlertTriangle, + priority: TrendingUp, + consistency: Repeat, +}; diff --git a/src/lib/machines/status.test.ts b/src/lib/machines/status.test.ts index 94917813..ff5c9a87 100644 --- a/src/lib/machines/status.test.ts +++ b/src/lib/machines/status.test.ts @@ -12,11 +12,11 @@ describe("deriveMachineStatus", () => { expect(deriveMachineStatus(issues)).toBe("operational"); }); - it("should return 'operational' when all issues are resolved", () => { + it("should return 'operational' when all issues are fixed", () => { const issues: IssueForStatus[] = [ - { status: "resolved", severity: "unplayable" }, - { status: "resolved", severity: "playable" }, - { status: "resolved", severity: "minor" }, + { status: "fixed", severity: "unplayable" }, + { status: "fixed", severity: "major" }, + { status: "fixed", severity: "minor" }, ]; expect(deriveMachineStatus(issues)).toBe("operational"); }); @@ -36,10 +36,10 @@ describe("deriveMachineStatus", () => { expect(deriveMachineStatus(issues)).toBe("unplayable"); }); - it("should return 'needs_service' when there are only playable issues", () => { + it("should return 'needs_service' when there are only major issues", () => { const issues: IssueForStatus[] = [ - { status: "new", severity: "playable" }, - { status: "in_progress", severity: "playable" }, + { status: "new", severity: "major" }, + { status: "in_progress", severity: "major" }, ]; expect(deriveMachineStatus(issues)).toBe("needs_service"); }); @@ -52,17 +52,22 @@ describe("deriveMachineStatus", () => { expect(deriveMachineStatus(issues)).toBe("needs_service"); }); - it("should return 'needs_service' when there are mixed playable and minor issues", () => { + it("should return 'needs_service' when there are only cosmetic issues", () => { + const issues: IssueForStatus[] = [{ status: "new", severity: "cosmetic" }]; + expect(deriveMachineStatus(issues)).toBe("needs_service"); + }); + + it("should return 'needs_service' when there are mixed major and minor issues", () => { const issues: IssueForStatus[] = [ - { status: "new", severity: "playable" }, + { status: "new", severity: "major" }, { status: "new", severity: "minor" }, ]; expect(deriveMachineStatus(issues)).toBe("needs_service"); }); - it("should ignore resolved issues when determining status", () => { + it("should ignore fixed issues when determining status", () => { const issues: IssueForStatus[] = [ - { status: "resolved", severity: "unplayable" }, + { status: "fixed", severity: "unplayable" }, { status: "new", severity: "minor" }, ]; expect(deriveMachineStatus(issues)).toBe("needs_service"); @@ -71,9 +76,9 @@ describe("deriveMachineStatus", () => { it("should prioritize unplayable over other severities", () => { const issues: IssueForStatus[] = [ { status: "new", severity: "minor" }, - { status: "new", severity: "playable" }, + { status: "new", severity: "major" }, { status: "new", severity: "unplayable" }, - { status: "resolved", severity: "unplayable" }, + { status: "fixed", severity: "unplayable" }, ]; expect(deriveMachineStatus(issues)).toBe("unplayable"); }); diff --git a/src/lib/machines/status.ts b/src/lib/machines/status.ts index 4bcae67f..b8e35a50 100644 --- a/src/lib/machines/status.ts +++ b/src/lib/machines/status.ts @@ -1,31 +1,26 @@ -/** - * Machine Status Derivation - * - * Derives machine operational status from open issues. - * Status hierarchy: unplayable > needs_service > operational - */ +import type { IssueStatus, IssueSeverity } from "~/lib/types"; +import { CLOSED_STATUSES } from "~/lib/issues/status"; export type MachineStatus = "unplayable" | "needs_service" | "operational"; export interface IssueForStatus { - status: "new" | "in_progress" | "resolved"; - severity: "minor" | "playable" | "unplayable"; + status: IssueStatus; + severity: IssueSeverity; } /** * Derive machine status from its issues * * Logic: - * - `unplayable`: At least one unplayable issue that's not resolved - * - `needs_service`: At least one playable/minor issue that's not resolved, no unplayable + * - `unplayable`: At least one unplayable issue that's not closed + * - `needs_service`: At least one non-closed issue (major/minor/cosmetic), or a major issue * - `operational`: No open issues - * - * @param issues - Array of issues for the machine - * @returns Derived machine status */ export function deriveMachineStatus(issues: IssueForStatus[]): MachineStatus { - // Filter to only open issues (not resolved) - const openIssues = issues.filter((issue) => issue.status !== "resolved"); + // Filter to only open issues (not in CLOSED_STATUSES) + const openIssues = issues.filter( + (issue) => !(CLOSED_STATUSES as readonly string[]).includes(issue.status) + ); // No open issues = operational if (openIssues.length === 0) { diff --git a/src/lib/timeline/events.ts b/src/lib/timeline/events.ts index d5014cb4..3811e6c5 100644 --- a/src/lib/timeline/events.ts +++ b/src/lib/timeline/events.ts @@ -21,7 +21,7 @@ import { issueComments } from "~/server/db/schema"; * ```ts * await createTimelineEvent(issueId, "Status changed from new to in_progress"); * await createTimelineEvent(issueId, "Assigned to John Doe"); - * await createTimelineEvent(issueId, "Marked as resolved"); + * await createTimelineEvent(issueId, "Marked as closed"); * ``` */ export async function createTimelineEvent( diff --git a/src/lib/types/database.ts b/src/lib/types/database.ts index 70285358..052bd1c3 100644 --- a/src/lib/types/database.ts +++ b/src/lib/types/database.ts @@ -17,10 +17,35 @@ import type { issueWatchers, } from "~/server/db/schema"; +// Enum types for type safety (import/define before using in Issue type) +// Based on _issue-status-redesign/README.md - Final design with 11 statuses +import type { UserRole } from "./user"; +import type { IssueStatus } from "~/lib/issues/status"; + +// Re-export types (IssueStatus comes from single source of truth) +export type { UserRole, IssueStatus }; + +export type IssueSeverity = "cosmetic" | "minor" | "major" | "unplayable"; +export type IssuePriority = "low" | "medium" | "high"; +export type IssueConsistency = "intermittent" | "frequent" | "constant"; + // Select types (full row from database) export type UserProfile = InferSelectModel; export type Machine = InferSelectModel; -export type Issue = InferSelectModel; + +// Issue type with proper enum types (Drizzle infers text columns as string) +type DrizzleIssue = InferSelectModel; +export type Issue = Omit< + DrizzleIssue, + "status" | "severity" | "priority" | "consistency" | "closedAt" +> & { + status: IssueStatus; + severity: IssueSeverity; + priority: IssuePriority; + consistency: IssueConsistency; + closedAt: Date | null; +}; + export type IssueComment = InferSelectModel; // Insert types (for creating new rows) @@ -29,13 +54,6 @@ export type NewMachine = InferInsertModel; export type NewIssue = InferInsertModel; export type NewIssueComment = InferInsertModel; -// Enum types for type safety -import type { UserRole } from "./user"; -export type { UserRole }; -export type IssueStatus = "new" | "in_progress" | "resolved"; -export type IssueSeverity = "minor" | "playable" | "unplayable"; -export type IssuePriority = "low" | "medium" | "high" | "critical"; - export type Notification = InferSelectModel; export type NotificationPreference = InferSelectModel< typeof notificationPreferences diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index e4aa07c1..b2be7a54 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -13,6 +13,7 @@ export type { IssueStatus, IssueSeverity, IssuePriority, + IssueConsistency, } from "./database"; export type { UserRole } from "./user"; diff --git a/src/server/db/index.ts b/src/server/db/index.ts index ab3cdd7d..3f53a395 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -11,11 +11,18 @@ if (!process.env["DATABASE_URL"]) { // Strip surrounding quotes if present (handles both single and double quotes) const databaseUrl = process.env["DATABASE_URL"].replace(/^["']|["']$/g, ""); -// Create postgres connection -const queryClient = postgres(databaseUrl); +/** + * Persist the database connection across hot reloads in development. + * This prevents reaching the connection limit (CORE-TEST-001). + */ +const globalForDb = globalThis as unknown as { + conn: postgres.Sql | undefined; +}; -// Create Drizzle instance with schema -export const db = drizzle(queryClient, { schema }); +const conn = globalForDb.conn ?? postgres(databaseUrl); +if (process.env.NODE_ENV !== "production") globalForDb.conn = conn; + +export const db = drizzle(conn, { schema }); export type Db = typeof db; export type Tx = Parameters[0]>[0]; diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 245dfbec..6b263da3 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -12,6 +12,7 @@ import { check, unique, } from "drizzle-orm/pg-core"; +import { ISSUE_STATUS_VALUES } from "~/lib/issues/status"; /** * ⚠️ IMPORTANT: When adding new tables to this schema file, @@ -134,21 +135,32 @@ export const issues = pgTable( issueNumber: integer("issue_number").notNull(), title: text("title").notNull(), description: text("description"), - status: text("status", { enum: ["new", "in_progress", "resolved"] }) + // Status values imported from single source of truth + // Based on _issue-status-redesign/README.md - Final design with 11 statuses + status: text("status", { + enum: ISSUE_STATUS_VALUES as unknown as [string, ...string[]], + }) .notNull() .default("new"), - severity: text("severity", { enum: ["minor", "playable", "unplayable"] }) + severity: text("severity", { + enum: ["cosmetic", "minor", "major", "unplayable"], + }) .notNull() - .default("playable"), - priority: text("priority", { enum: ["low", "medium", "high", "critical"] }) + .default("minor"), + priority: text("priority", { enum: ["low", "medium", "high"] }) .notNull() - .default("low"), + .default("medium"), + consistency: text("consistency", { + enum: ["intermittent", "frequent", "constant"], + }) + .notNull() + .default("intermittent"), reportedBy: uuid("reported_by").references(() => userProfiles.id), unconfirmedReportedBy: uuid("unconfirmed_reported_by").references( () => unconfirmedUsers.id ), assignedTo: uuid("assigned_to").references(() => userProfiles.id), - resolvedAt: timestamp("resolved_at", { withTimezone: true }), + closedAt: timestamp("closed_at", { withTimezone: true }), createdAt: timestamp("created_at", { withTimezone: true }) .notNull() .defaultNow(), diff --git a/src/services/issues.test.ts b/src/services/issues.test.ts index aecfb4cd..f3874617 100644 --- a/src/services/issues.test.ts +++ b/src/services/issues.test.ts @@ -204,10 +204,10 @@ describe("Issue Service", () => { }); describe("updateIssueStatus", () => { - it("notifies watchers and participants", async () => { + it("notifies watchers and participants and sets closedAt when appropriate", async () => { const params = { issueId: "issue-1", - status: "resolved" as const, + status: "fixed" as const, userId: "user-1", }; @@ -220,30 +220,20 @@ describe("Issue Service", () => { machine: { name: "Machine Name" }, assignedTo: "assignee-1", reportedBy: "reporter-1", - } as unknown as Awaited< - ReturnType - >); + } as any); await updateIssueStatus(params); expect(createNotification).toHaveBeenCalledWith( - { + expect.objectContaining({ type: "issue_status_changed", - resourceId: "issue-1", - resourceType: "issue", - actorId: "user-1", - includeActor: true, - issueTitle: "Issue Title", - machineName: "Machine Name", - formattedIssueId: "MM-01", - newStatus: "resolved", - issueContext: { - assignedToId: "assignee-1", - reportedById: "reporter-1", - }, - }, + newStatus: "fixed", + }), expect.anything() ); + + // Verify closedAt was set - using mockDeep patterns + expect(mockDb.update).toHaveBeenCalledWith(expect.anything()); }); }); }); diff --git a/src/services/issues.ts b/src/services/issues.ts index dc96fad5..6b94335f 100644 --- a/src/services/issues.ts +++ b/src/services/issues.ts @@ -12,6 +12,19 @@ import { createTimelineEvent } from "~/lib/timeline/events"; import { createNotification } from "~/lib/notifications"; import { log } from "~/lib/logger"; import { formatIssueId } from "~/lib/issues/utils"; +import { + type IssueSeverity, + type IssuePriority, + type IssueConsistency, + type IssueStatus, +} from "~/lib/types"; +import { + CLOSED_STATUSES, + getIssueConsistencyLabel, + getIssuePriorityLabel, + getIssueSeverityLabel, + getIssueStatusLabel, +} from "~/lib/issues/status"; // --- Types --- @@ -19,15 +32,16 @@ export interface CreateIssueParams { title: string; description?: string | null; machineInitials: string; - severity: string; - priority?: string | undefined; + severity: IssueSeverity; + priority?: IssuePriority | undefined; + consistency?: IssueConsistency | undefined; reportedBy?: string | null; unconfirmedReportedBy?: string | null; } export interface UpdateIssueStatusParams { issueId: string; - status: "new" | "in_progress" | "resolved"; + status: IssueStatus; userId: string; } @@ -45,12 +59,17 @@ export interface AssignIssueParams { export interface UpdateIssueSeverityParams { issueId: string; - severity: string; + severity: IssueSeverity; } export interface UpdateIssuePriorityParams { issueId: string; - priority: string; + priority: IssuePriority; +} + +export interface UpdateIssueConsistencyParams { + issueId: string; + consistency: IssueConsistency; } export type Issue = InferSelectModel; @@ -68,6 +87,7 @@ export async function createIssue({ machineInitials, severity, priority, + consistency, reportedBy, unconfirmedReportedBy, }: CreateIssueParams): Promise { @@ -98,8 +118,9 @@ export async function createIssue({ issueNumber, title, description: description ?? null, - severity: severity as "minor" | "playable" | "unplayable", - priority: (priority ?? "low") as "low" | "medium" | "high", + severity, + priority: priority ?? "medium", + consistency: consistency ?? "intermittent", reportedBy: reportedBy ?? null, unconfirmedReportedBy: unconfirmedReportedBy ?? null, status: "new", @@ -219,18 +240,22 @@ export async function updateIssueStatus({ const oldStatus = currentIssue.status; // 1. Update Status + const isClosed = (CLOSED_STATUSES as readonly string[]).includes(status); await tx .update(issues) .set({ status, updatedAt: new Date(), + closedAt: isClosed ? new Date() : null, }) .where(eq(issues.id, issueId)); // 2. Create Timeline Event + const oldLabel = getIssueStatusLabel(oldStatus as IssueStatus); + const newLabel = getIssueStatusLabel(status); await createTimelineEvent( issueId, - `Status changed from ${oldStatus} to ${status}`, + `Status changed from ${oldLabel} to ${newLabel}`, tx ); @@ -509,15 +534,17 @@ export async function updateIssueSeverity({ await db .update(issues) .set({ - severity: severity as "minor" | "playable" | "unplayable", + severity, updatedAt: new Date(), }) .where(eq(issues.id, issueId)); // Create timeline event + const oldLabel = getIssueSeverityLabel(oldSeverity as IssueSeverity); + const newLabel = getIssueSeverityLabel(severity); await createTimelineEvent( issueId, - `Severity changed from ${oldSeverity} to ${severity}` + `Severity changed from ${oldLabel} to ${newLabel}` ); log.info( @@ -560,15 +587,17 @@ export async function updateIssuePriority({ await db .update(issues) .set({ - priority: priority as "low" | "medium" | "high", + priority, updatedAt: new Date(), }) .where(eq(issues.id, issueId)); // Create timeline event + const oldLabel = getIssuePriorityLabel(oldPriority as IssuePriority); + const newLabel = getIssuePriorityLabel(priority); await createTimelineEvent( issueId, - `Priority changed from ${oldPriority} to ${priority}` + `Priority changed from ${oldLabel} to ${newLabel}` ); log.info( @@ -583,3 +612,56 @@ export async function updateIssuePriority({ return { issueId, oldPriority, newPriority: priority }; } + +/** + * Update issue consistency + */ +export async function updateIssueConsistency({ + issueId, + consistency, +}: UpdateIssueConsistencyParams): Promise<{ + issueId: string; + oldConsistency: string; + newConsistency: string; +}> { + // Get current issue to check old consistency + const currentIssue = await db.query.issues.findFirst({ + where: eq(issues.id, issueId), + columns: { consistency: true, machineInitials: true }, + }); + + if (!currentIssue) { + throw new Error("Issue not found"); + } + + const oldConsistency = currentIssue.consistency; + + // Update consistency + await db + .update(issues) + .set({ + consistency, + updatedAt: new Date(), + }) + .where(eq(issues.id, issueId)); + + // Create timeline event + const oldLabel = getIssueConsistencyLabel(oldConsistency as IssueConsistency); + const newLabel = getIssueConsistencyLabel(consistency); + await createTimelineEvent( + issueId, + `Consistency changed from ${oldLabel} to ${newLabel}` + ); + + log.info( + { + issueId, + oldConsistency, + newConsistency: consistency, + action: "updateIssueConsistency", + }, + "Issue consistency updated" + ); + + return { issueId, oldConsistency, newConsistency: consistency }; +} diff --git a/src/test/helpers/factories.ts b/src/test/helpers/factories.ts index c1c403da..cae14563 100644 --- a/src/test/helpers/factories.ts +++ b/src/test/helpers/factories.ts @@ -59,11 +59,11 @@ export function createTestIssue( title: "Test Issue", description: "Test description", status: "new", - severity: "playable", + severity: "minor", priority: "low", reportedBy: null, assignedTo: null, - resolvedAt: null, + closedAt: null, createdAt: new Date(), updatedAt: new Date(), ...overrides, diff --git a/src/test/integration/dashboard.test.ts b/src/test/integration/dashboard.test.ts index 74d58f47..c34a765c 100644 --- a/src/test/integration/dashboard.test.ts +++ b/src/test/integration/dashboard.test.ts @@ -50,7 +50,7 @@ describe("Dashboard Queries (PGlite)", () => { .values(testMachine) .returning(); - // Create issues: 2 for user1 (1 open, 1 resolved), 1 for user2 + // Create issues: 2 for user1 (1 open, 1 fixed), 1 for user2 await db.insert(issues).values([ createTestIssue(machine.initials, { title: "Issue 1 for User 1", @@ -59,10 +59,10 @@ describe("Dashboard Queries (PGlite)", () => { status: "new", }), createTestIssue(machine.initials, { - title: "Issue 2 for User 1 (resolved)", + title: "Issue 2 for User 1 (fixed)", issueNumber: 2, assignedTo: testUser1.id, - status: "resolved", + status: "fixed", }), createTestIssue(machine.initials, { title: "Issue for User 2", @@ -76,7 +76,7 @@ describe("Dashboard Queries (PGlite)", () => { const assignedIssues = await db.query.issues.findMany({ where: and( eq(issues.assignedTo, testUser1.id), - ne(issues.status, "resolved") + ne(issues.status, "fixed") ), orderBy: desc(issues.createdAt), with: { @@ -101,10 +101,7 @@ describe("Dashboard Queries (PGlite)", () => { // Query with no data const assignedIssues = await db.query.issues.findMany({ - where: and( - eq(issues.assignedTo, userId), - ne(issues.status, "resolved") - ), + where: and(eq(issues.assignedTo, userId), ne(issues.status, "fixed")), }); expect(assignedIssues).toHaveLength(0); @@ -198,12 +195,12 @@ describe("Dashboard Queries (PGlite)", () => { }) ); - // Machine 3: unplayable issue (resolved - should NOT appear) + // Machine 3: unplayable issue (fixed - should NOT appear) await db.insert(issues).values( createTestIssue(machine3.initials, { issueNumber: 1, severity: "unplayable", - status: "resolved", + status: "fixed", }) ); @@ -227,7 +224,7 @@ describe("Dashboard Queries (PGlite)", () => { ); const unplayableIssuesCount = machine.issues.filter( (issue: { severity: string; status: string }) => - issue.severity === "unplayable" && issue.status !== "resolved" + issue.severity === "unplayable" && issue.status !== "fixed" ).length; return { @@ -257,7 +254,7 @@ describe("Dashboard Queries (PGlite)", () => { .values(testMachine) .returning(); - // Create 3 open issues, 2 resolved + // Create 3 open issues, 2 fixed await db.insert(issues).values([ createTestIssue(machine.initials, { issueNumber: 1, status: "new" }), createTestIssue(machine.initials, { @@ -267,11 +264,11 @@ describe("Dashboard Queries (PGlite)", () => { createTestIssue(machine.initials, { issueNumber: 3, status: "new" }), createTestIssue(machine.initials, { issueNumber: 4, - status: "resolved", + status: "fixed", }), createTestIssue(machine.initials, { issueNumber: 5, - status: "resolved", + status: "fixed", }), ]); @@ -279,7 +276,7 @@ describe("Dashboard Queries (PGlite)", () => { const totalOpenIssuesResult = await db .select({ count: sql`count(*)::int` }) .from(issues) - .where(ne(issues.status, "resolved")); + .where(ne(issues.status, "fixed")); const totalOpenIssues = totalOpenIssuesResult[0]?.count ?? 0; @@ -316,11 +313,11 @@ describe("Dashboard Queries (PGlite)", () => { }) ); - // Machine 3: only resolved issues (operational) + // Machine 3: only fixed issues (operational) await db.insert(issues).values( createTestIssue(machine3.initials, { issueNumber: 1, - status: "resolved", + status: "fixed", }) ); @@ -361,7 +358,7 @@ describe("Dashboard Queries (PGlite)", () => { .values(testMachine) .returning(); - // Create 2 open assigned issues, 1 resolved assigned issue + // Create 2 open assigned issues, 1 fixed assigned issue await db.insert(issues).values([ createTestIssue(machine.initials, { issueNumber: 1, @@ -376,16 +373,13 @@ describe("Dashboard Queries (PGlite)", () => { createTestIssue(machine.initials, { issueNumber: 3, assignedTo: user.id, - status: "resolved", + status: "fixed", }), ]); // Query assigned issues count (dashboard pattern) const assignedIssues = await db.query.issues.findMany({ - where: and( - eq(issues.assignedTo, user.id), - ne(issues.status, "resolved") - ), + where: and(eq(issues.assignedTo, user.id), ne(issues.status, "fixed")), }); const myIssuesCount = assignedIssues.length; diff --git a/src/test/integration/machines.test.ts b/src/test/integration/machines.test.ts index 58f14eeb..49e5d119 100644 --- a/src/test/integration/machines.test.ts +++ b/src/test/integration/machines.test.ts @@ -149,14 +149,14 @@ describe("Machine CRUD Operations (PGlite)", () => { .values(testMachine) .returning(); - // Create only resolved issues + // Create only fixed issues await db.insert(issues).values([ createTestIssue(machine.initials, { title: "Fixed issue", issueNumber: 1, severity: "unplayable", - status: "resolved", - resolvedAt: new Date(), + status: "fixed", + closedAt: new Date(), }), ]); @@ -191,12 +191,12 @@ describe("Machine CRUD Operations (PGlite)", () => { .values(testMachine) .returning(); - // Create playable and minor issues + // Create major and minor issues await db.insert(issues).values([ createTestIssue(machine.initials, { - title: "Playable issue", + title: "Major issue", issueNumber: 1, - severity: "playable", + severity: "major", status: "new", }), createTestIssue(machine.initials, { @@ -272,12 +272,12 @@ describe("Machine CRUD Operations (PGlite)", () => { expect(status).toBe("unplayable"); }); - it("should ignore resolved issues when deriving status", async () => { + it("should ignore fixed issues when deriving status", async () => { const db = await getTestDb(); // Create machine const testMachine = createTestMachine({ - name: "Machine with resolved unplayable", + name: "Machine with fixed unplayable", initials: "MR", }); const [machine] = await db @@ -285,14 +285,14 @@ describe("Machine CRUD Operations (PGlite)", () => { .values(testMachine) .returning(); - // Create resolved unplayable issue and open minor issue + // Create fixed unplayable issue and open minor issue await db.insert(issues).values([ createTestIssue(machine.initials, { title: "Fixed unplayable issue", issueNumber: 1, severity: "unplayable", - status: "resolved", - resolvedAt: new Date(), + status: "fixed", + closedAt: new Date(), }), createTestIssue(machine.initials, { title: "Current minor issue", diff --git a/src/test/integration/supabase/issue-services.test.ts b/src/test/integration/supabase/issue-services.test.ts new file mode 100644 index 00000000..fb724a43 --- /dev/null +++ b/src/test/integration/supabase/issue-services.test.ts @@ -0,0 +1,214 @@ +/** + * Integration Tests for Issue Service Functions + * + * Verifies that service layer functions correctly handle the new + * status overhaul fields, transactions, and timeline events. + */ + +import { describe, it, expect, beforeEach, vi, beforeAll } from "vitest"; +import { eq, desc } from "drizzle-orm"; +import { getTestDb, setupTestDb } from "~/test/setup/pglite"; +import { createTestUser } from "~/test/helpers/factories"; +import { + issues, + machines, + userProfiles, + issueComments, +} from "~/server/db/schema"; +import { + updateIssueStatus, + updateIssueSeverity, + updateIssuePriority, + updateIssueConsistency, +} from "~/services/issues"; + +// Mock the database to use the PGlite instance +vi.mock("~/server/db", () => ({ + db: { + insert: vi.fn((...args: any[]) => + (globalThis as any).testDb.insert(...args) + ), + update: vi.fn((...args: any[]) => + (globalThis as any).testDb.update(...args) + ), + delete: vi.fn((...args: any[]) => + (globalThis as any).testDb.delete(...args) + ), + select: vi.fn((...args: any[]) => + (globalThis as any).testDb.select(...args) + ), + query: { + issues: { + findFirst: vi.fn((...args: any[]) => + (globalThis as any).testDb.query.issues.findFirst(...args) + ), + findMany: vi.fn((...args: any[]) => + (globalThis as any).testDb.query.issues.findMany(...args) + ), + }, + userProfiles: { + findFirst: vi.fn((...args: any[]) => + (globalThis as any).testDb.query.userProfiles.findFirst(...args) + ), + }, + }, + transaction: vi.fn((cb: any) => cb((globalThis as any).testDb)), + }, +})); + +describe("Issue Service Functions (Integration)", () => { + setupTestDb(); + + let testMachine: any; + let testUser: any; + let testIssue: any; + + beforeAll(async () => { + (globalThis as any).testDb = await getTestDb(); + }); + + beforeEach(async () => { + const db = await getTestDb(); + + // Create test user (admin for permissions if needed, though services don't check permissions) + const [user] = await db + .insert(userProfiles) + .values( + createTestUser({ + id: "00000000-0000-0000-0000-000000000001", + role: "admin", + }) + ) + .returning(); + testUser = user; + + // Create test machine + const [machine] = await db + .insert(machines) + .values({ + name: "Service Test Machine", + initials: "STM", + ownerId: testUser.id, + }) + .returning(); + testMachine = machine; + + // Create a base issue + const [issue] = await db + .insert(issues) + .values({ + title: "Service Test Issue", + machineInitials: testMachine.initials, + issueNumber: 1, + severity: "minor", + priority: "low", + consistency: "intermittent", + status: "new", + reportedBy: testUser.id, + }) + .returning(); + testIssue = issue; + }); + + it("should update status and create timeline event", async () => { + const db = await getTestDb(); + const newStatus = "in_progress"; + + await updateIssueStatus({ + issueId: testIssue.id, + status: newStatus, + userId: testUser.id, + }); + + const updated = await db.query.issues.findFirst({ + where: eq(issues.id, testIssue.id), + }); + + expect(updated?.status).toBe(newStatus); + + // Verify timeline event + const events = await db.query.issueComments.findMany({ + where: eq(issueComments.issueId, testIssue.id), + orderBy: desc(issueComments.createdAt), + }); + + const statusEvent = events.find((e) => + e.content.includes("Status changed") + ); + expect(statusEvent).toBeDefined(); + expect(statusEvent?.content).toContain("from New to In Progress"); + expect(statusEvent?.isSystem).toBe(true); + }); + + it("should update severity and create timeline event", async () => { + const db = await getTestDb(); + const newSeverity = "unplayable"; + + await updateIssueSeverity({ + issueId: testIssue.id, + severity: newSeverity, + }); + + const updated = await db.query.issues.findFirst({ + where: eq(issues.id, testIssue.id), + }); + + expect(updated?.severity).toBe(newSeverity); + + const event = await db.query.issueComments.findFirst({ + where: eq(issueComments.issueId, testIssue.id), + orderBy: desc(issueComments.createdAt), + }); + + expect(event?.content).toContain("Severity changed"); + expect(event?.content).toContain("from Minor to Unplayable"); + }); + + it("should update priority and create timeline event", async () => { + const db = await getTestDb(); + const newPriority = "high"; + + await updateIssuePriority({ + issueId: testIssue.id, + priority: newPriority, + }); + + const updated = await db.query.issues.findFirst({ + where: eq(issues.id, testIssue.id), + }); + + expect(updated?.priority).toBe(newPriority); + + const event = await db.query.issueComments.findFirst({ + where: eq(issueComments.issueId, testIssue.id), + orderBy: desc(issueComments.createdAt), + }); + + expect(event?.content).toContain("Priority changed"); + expect(event?.content).toContain("from Low to High"); + }); + + it("should update consistency and create timeline event", async () => { + const db = await getTestDb(); + const newConsistency = "constant"; + + await updateIssueConsistency({ + issueId: testIssue.id, + consistency: newConsistency, + }); + + const updated = await db.query.issues.findFirst({ + where: eq(issues.id, testIssue.id), + }); + + expect(updated?.consistency).toBe(newConsistency); + + const event = await db.query.issueComments.findFirst({ + where: eq(issueComments.issueId, testIssue.id), + orderBy: desc(issueComments.createdAt), + }); + + expect(event?.content).toContain("Consistency changed"); + expect(event?.content).toContain("from Intermittent to Constant"); + }); +}); diff --git a/src/test/integration/supabase/issues.test.ts b/src/test/integration/supabase/issues.test.ts index c48f893d..f9ffe51f 100644 --- a/src/test/integration/supabase/issues.test.ts +++ b/src/test/integration/supabase/issues.test.ts @@ -105,7 +105,7 @@ describe("Issues CRUD Operations (PGlite)", () => { expect(issue.status).toBe("new"); }); - it("should default severity to 'playable' if not provided", async () => { + it("should default severity to 'minor' if not provided", async () => { const db = await getTestDb(); const [issue] = await db @@ -118,7 +118,7 @@ describe("Issues CRUD Operations (PGlite)", () => { }) .returning(); - expect(issue.severity).toBe("playable"); + expect(issue.severity).toBe("minor"); }); }); @@ -149,7 +149,7 @@ describe("Issues CRUD Operations (PGlite)", () => { machineInitials: testMachine.initials, issueNumber: 3, severity: "minor", - status: "resolved", + status: "fixed", reportedBy: testUser.id, }, ]); @@ -260,21 +260,21 @@ describe("Issues CRUD Operations (PGlite)", () => { expect(updated?.severity).toBe("unplayable"); }); - it("should set resolvedAt when status is resolved", async () => { + it("should set closedAt when status is fixed", async () => { const db = await getTestDb(); - const resolvedDate = new Date(); + const closedDate = new Date(); await db .update(issues) - .set({ status: "resolved", resolvedAt: resolvedDate }) + .set({ status: "fixed", closedAt: closedDate }) .where(eq(issues.id, testIssue.id)); const updated = await db.query.issues.findFirst({ where: eq(issues.id, testIssue.id), }); - expect(updated?.status).toBe("resolved"); - expect(updated?.resolvedAt).toBeDefined(); + expect(updated?.status).toBe("fixed"); + expect(updated?.closedAt).toBeDefined(); }); it("should assign issue to user", async () => { diff --git a/src/test/setup/schema.sql b/src/test/setup/schema.sql index e0b794e8..e37a2dbc 100644 --- a/src/test/setup/schema.sql +++ b/src/test/setup/schema.sql @@ -26,12 +26,13 @@ CREATE TABLE "issues" ( "title" text NOT NULL, "description" text, "status" text DEFAULT 'new' NOT NULL, - "severity" text DEFAULT 'playable' NOT NULL, - "priority" text DEFAULT 'low' NOT NULL, + "severity" text DEFAULT 'minor' NOT NULL, + "priority" text DEFAULT 'medium' NOT NULL, + "consistency" text DEFAULT 'intermittent' NOT NULL, "reported_by" uuid, "unconfirmed_reported_by" uuid, "assigned_to" uuid, - "resolved_at" timestamp with time zone, + "closed_at" timestamp with time zone, "created_at" timestamp with time zone DEFAULT now() NOT NULL, "updated_at" timestamp with time zone DEFAULT now() NOT NULL, CONSTRAINT "unique_issue_number" UNIQUE("machine_initials","issue_number"), diff --git a/src/test/unit/issue-actions.test.ts b/src/test/unit/issue-actions.test.ts index be5b248a..d6a46749 100644 --- a/src/test/unit/issue-actions.test.ts +++ b/src/test/unit/issue-actions.test.ts @@ -2,13 +2,16 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { addCommentAction, updateIssueStatusAction, + updateIssueConsistencyAction, } from "~/app/(app)/issues/actions"; import { canUpdateIssue } from "~/lib/permissions"; // Mock Next.js modules vi.mock("next/navigation", () => ({ - redirect: vi.fn(() => { - throw new Error("NEXT_REDIRECT"); + redirect: vi.fn((url: string) => { + const error = new Error("NEXT_REDIRECT"); + (error as any).digest = `NEXT_REDIRECT;replace;${url};`; + throw error; }), })); @@ -52,9 +55,11 @@ vi.mock("~/lib/notifications", () => ({ // Mock services vi.mock("~/services/issues", () => ({ - addIssueComment: vi.fn(), - createIssue: vi.fn(), updateIssueStatus: vi.fn(), + updateIssueSeverity: vi.fn(), + updateIssuePriority: vi.fn(), + updateIssueConsistency: vi.fn(), + addIssueComment: vi.fn(), })); // Mock permissions @@ -64,7 +69,11 @@ vi.mock("~/lib/permissions", () => ({ import { revalidatePath } from "next/cache"; import { createClient } from "~/lib/supabase/server"; -import { addIssueComment, updateIssueStatus } from "~/services/issues"; +import { + addIssueComment, + updateIssueStatus, + updateIssueConsistency, +} from "~/services/issues"; import { db } from "~/server/db"; type SupabaseClient = Awaited>; @@ -182,11 +191,9 @@ describe("updateIssueStatusAction", () => { status: "new", machineInitials: "MM", issueNumber: 1, - machineId: "machine-123", // Still needed? Schema changed but maybe tests mock it loosely? - // Actually, query selects machineInitials. reportedBy: "user-123", assignedTo: null, - machine: { ownerId: "owner-123" }, + machine: { ownerId: "owner-123", name: "Test Machine" }, } as any); // Mock user profile @@ -250,3 +257,47 @@ describe("updateIssueStatusAction", () => { expect(updateIssueStatus).not.toHaveBeenCalled(); }); }); + +describe("updateIssueConsistencyAction", () => { + const validUuid = "123e4567-e89b-12d3-a456-426614174000"; + const mockUser = { id: "user-123" }; + const initialState = undefined; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(createClient).mockResolvedValue({ + auth: { + getUser: vi.fn().mockResolvedValue({ data: { user: mockUser } }), + }, + } as any); + }); + + it("should successfully update consistency", async () => { + vi.mocked(db.query.issues.findFirst).mockResolvedValue({ + machineInitials: "MM", + issueNumber: 1, + reportedBy: mockUser.id, + assignedTo: null, + machine: { ownerId: "owner-123" }, + } as any); + vi.mocked(db.query.userProfiles.findFirst).mockResolvedValue({ + role: "member", + } as any); + vi.mocked(canUpdateIssue).mockReturnValue(true); + vi.mocked(updateIssueConsistency).mockResolvedValue({ + issueId: validUuid, + oldConsistency: "intermittent", + newConsistency: "constant", + }); + + const formData = new FormData(); + formData.append("issueId", validUuid); + formData.append("consistency", "constant"); + + const result = await updateIssueConsistencyAction(initialState, formData); + + expect(result.ok).toBe(true); + expect(canUpdateIssue).toHaveBeenCalled(); + expect(updateIssueConsistency).toHaveBeenCalled(); + }); +}); diff --git a/src/test/unit/issue-schemas.test.ts b/src/test/unit/issue-schemas.test.ts index 58bf9bed..83ffc8f5 100644 --- a/src/test/unit/issue-schemas.test.ts +++ b/src/test/unit/issue-schemas.test.ts @@ -19,6 +19,7 @@ describe("Issue Validation Schemas", () => { machineInitials: validInitials, severity: "minor", priority: "low", + consistency: "intermittent", }); expect(result.success).toBe(true); }); @@ -27,8 +28,9 @@ describe("Issue Validation Schemas", () => { const result = createIssueSchema.safeParse({ title: "Test Issue", machineInitials: validInitials, - severity: "playable", + severity: "cosmetic", priority: "medium", + consistency: "constant", }); expect(result.success).toBe(true); }); diff --git a/src/test/unit/machine-actions.test.ts b/src/test/unit/machine-actions.test.ts index d35ca832..77a04528 100644 --- a/src/test/unit/machine-actions.test.ts +++ b/src/test/unit/machine-actions.test.ts @@ -8,8 +8,10 @@ import { db } from "~/server/db"; // Mock Next.js modules vi.mock("next/navigation", () => ({ - redirect: vi.fn(() => { - throw new Error("NEXT_REDIRECT"); + redirect: vi.fn((url: string) => { + const error = new Error("NEXT_REDIRECT"); + (error as any).digest = `NEXT_REDIRECT;replace;${url};`; + throw error; }), })); diff --git a/src/test/unit/public-issue-schema.test.ts b/src/test/unit/public-issue-schema.test.ts index a289f63e..a87bde28 100644 --- a/src/test/unit/public-issue-schema.test.ts +++ b/src/test/unit/public-issue-schema.test.ts @@ -10,6 +10,7 @@ describe("publicIssueSchema", () => { title: "Public Report", description: "Something is wrong", severity: "minor", + consistency: "intermittent", }); expect(result.success).toBe(true); }); @@ -18,7 +19,8 @@ describe("publicIssueSchema", () => { const result = publicIssueSchema.safeParse({ machineId: validUuid, title: "Public Report", - severity: "playable", + severity: "minor", + consistency: "constant", }); expect(result.success).toBe(true); }); diff --git a/src/test/unit/public-issue-validation.test.ts b/src/test/unit/public-issue-validation.test.ts index 281b24b5..5c38b0e3 100644 --- a/src/test/unit/public-issue-validation.test.ts +++ b/src/test/unit/public-issue-validation.test.ts @@ -19,7 +19,8 @@ describe("parsePublicIssueForm", () => { machineId: "123e4567-e89b-12d3-a456-426614174000", title: "Flipper stuck", description: "Left flipper sometimes sticks halfway.", - severity: "playable", + severity: "minor", + consistency: "intermittent", }); const result = parsePublicIssueForm(formData); @@ -27,7 +28,8 @@ describe("parsePublicIssueForm", () => { expect(result).toHaveProperty("success", true); if (result.success) { expect(result.data.title).toBe("Flipper stuck"); - expect(result.data.severity).toBe("playable"); + expect(result.data.severity).toBe("minor"); + expect(result.data.consistency).toBe("intermittent"); } }); diff --git a/supabase/seed-users.mjs b/supabase/seed-users.mjs index 8f291f89..d64ab621 100644 --- a/supabase/seed-users.mjs +++ b/supabase/seed-users.mjs @@ -143,17 +143,19 @@ async function seedUsersAndData() { // 3. Seed Issues console.log("\n🔧 Seeding issues..."); - // Attack from Mars: 1 playable issue + // Attack from Mars: 1 issue await sql` - INSERT INTO issues (id, machine_initials, issue_number, title, description, status, severity, created_at, updated_at) + INSERT INTO issues (id, machine_initials, issue_number, title, description, status, severity, priority, consistency, created_at, updated_at) VALUES ( '10000000-0000-4000-8000-000000000001', 'AFM', 1, 'Right flipper feels weak', 'The right flipper doesn\''t have full strength. Can still play but makes ramp shots difficult.', - 'new', - 'playable', + 'confirmed', + 'minor', + 'medium', + 'constant', NOW() - INTERVAL '2 days', NOW() - INTERVAL '2 days' ) @@ -165,7 +167,7 @@ async function seedUsersAndData() { // The Addams Family: Multiple issues await sql` - INSERT INTO issues (id, machine_initials, issue_number, title, description, status, severity, created_at, updated_at) + INSERT INTO issues (id, machine_initials, issue_number, title, description, status, severity, priority, consistency, created_at, updated_at) VALUES ( '10000000-0000-4000-8000-000000000002', @@ -173,8 +175,10 @@ async function seedUsersAndData() { 1, 'Ball stuck in Thing\''s box', 'Extended sample issue with many timeline updates.', - 'new', + 'in_progress', 'unplayable', + 'high', + 'constant', NOW() - INTERVAL '1 day', NOW() - INTERVAL '1 day' ), @@ -184,8 +188,10 @@ async function seedUsersAndData() { 2, 'Bookcase not registering hits', 'The bookcase target doesn\''t registering when hit.', - 'in_progress', - 'playable', + 'need_parts', + 'major', + 'high', + 'frequent', NOW() - INTERVAL '3 days', NOW() - INTERVAL '1 day' ), @@ -196,7 +202,9 @@ async function seedUsersAndData() { 'Dim GI lighting on left side', 'General illumination bulbs on left side are dim.', 'new', - 'minor', + 'cosmetic', + 'low', + 'constant', NOW() - INTERVAL '5 days', NOW() - INTERVAL '5 days' ), @@ -206,16 +214,31 @@ async function seedUsersAndData() { 4, 'Bear Kick opto not working', 'Bear Kick feature not detecting ball.', - 'new', - 'playable', + 'wait_owner', + 'major', + 'medium', + 'intermittent', NOW() - INTERVAL '1 week', NOW() - INTERVAL '1 week' + ), + ( + '10000000-0000-4000-8000-000000000006', + 'TAF', + 5, + 'Magnet throwing ball to Drain', + 'The Power magnet seems too strong or mistimed.', + 'wont_fix', + 'minor', + 'low', + 'frequent', + NOW() - INTERVAL '2 weeks', + NOW() - INTERVAL '2 weeks' ) ON CONFLICT (id) DO NOTHING `; // Update TAF next issue number - await sql`UPDATE machines SET next_issue_number = 5 WHERE initials = 'TAF'`; + await sql`UPDATE machines SET next_issue_number = 6 WHERE initials = 'TAF'`; console.log("✅ Issues seeded."); @@ -235,7 +258,7 @@ async function seedUsersAndData() { }, { author: userIds.admin, - content: "Severity changed from playable to unplayable", + content: "Severity changed from minor to unplayable", isSystem: true, daysAgo: 8, }, @@ -247,7 +270,7 @@ async function seedUsersAndData() { }, { author: userIds.admin, - content: "Status changed from new to in_progress", + content: "Status changed from New to In Progress", isSystem: true, daysAgo: 6, }, @@ -271,13 +294,13 @@ async function seedUsersAndData() { }, { author: userIds.member, - content: "Status changed from in_progress to resolved", + content: "Status changed from In Progress to Fixed", isSystem: true, daysAgo: 2, }, { author: userIds.member, - content: "Severity changed from unplayable to playable", + content: "Severity changed from Unplayable to Minor", isSystem: true, daysAgo: 1.5, },