chore(style): codebase em-dash sweep + minor layout polish
Replaces every em-dash and en-dash with regular ASCII hyphens across comments, JSX strings, and dev-facing logs. Mostly cosmetic but stops the inconsistent mix that crept in over the last few months (some files used em-dashes in comments, others didn't, some used both). Bundles two small dashboard-layout tweaks that touch a couple of already-modified files: - (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6 pb-6 so page content sits closer to the topbar. - Sidebar now receives the ports list it needs for the footer port switcher. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
* soft warnings and hard caps.
|
||||
*
|
||||
* Token-denominated rather than dollar-denominated so the cap survives
|
||||
* model price changes — and it's the unit both OpenAI and Anthropic
|
||||
* model price changes - and it's the unit both OpenAI and Anthropic
|
||||
* SDKs return in `response.usage`.
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,7 @@ export const aiUsageLedger = pgTable(
|
||||
portId: text('port_id')
|
||||
.notNull()
|
||||
.references(() => ports.id, { onDelete: 'cascade' }),
|
||||
/** Optional — system-initiated calls (e.g. scheduled summarizers) won't have a user. */
|
||||
/** Optional - system-initiated calls (e.g. scheduled summarizers) won't have a user. */
|
||||
userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
|
||||
/** Stable feature key: 'ocr', 'summary', 'embedding', 'reply_draft', etc. */
|
||||
feature: text('feature').notNull(),
|
||||
|
||||
@@ -127,7 +127,7 @@ export const clientTags = pgTable(
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: 'cascade' }),
|
||||
tagId: text('tag_id').notNull(), // references tags.id — defined later in system.ts
|
||||
tagId: text('tag_id').notNull(), // references tags.id - defined later in system.ts
|
||||
},
|
||||
(table) => [primaryKey({ columns: [table.clientId, table.tagId] })],
|
||||
);
|
||||
@@ -194,7 +194,7 @@ export const clientMergeCandidates = pgTable(
|
||||
},
|
||||
(table) => [
|
||||
index('idx_cmc_port_status').on(table.portId, table.status),
|
||||
// Same pair shouldn't surface twice — enforce uniqueness on the
|
||||
// Same pair shouldn't surface twice - enforce uniqueness on the
|
||||
// canonical (a < b) ordering.
|
||||
uniqueIndex('idx_cmc_pair').on(table.portId, table.clientAId, table.clientBId),
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { pgTable, text, boolean, timestamp, index, uniqueIndex } from 'drizzle-o
|
||||
*
|
||||
* `tokenHash` is a SHA-256 hash of the raw token sent in the email. Lookups
|
||||
* happen by hash so a DB compromise never leaks active tokens. The invite
|
||||
* is consumed at /set-password — the route creates the better-auth `user`
|
||||
* is consumed at /set-password - the route creates the better-auth `user`
|
||||
* row + `account` credential and the matching `user_profiles` extension.
|
||||
*/
|
||||
export const crmUserInvites = pgTable(
|
||||
|
||||
@@ -31,7 +31,7 @@ export const gdprExports = pgTable(
|
||||
.references(() => user.id, { onDelete: 'restrict' }),
|
||||
/** 'pending' | 'building' | 'ready' | 'sent' | 'failed' */
|
||||
status: text('status').notNull().default('pending'),
|
||||
/** MinIO path under the configured bucket — null until the worker uploads. */
|
||||
/** MinIO path under the configured bucket - null until the worker uploads. */
|
||||
storageKey: text('storage_key'),
|
||||
sizeBytes: integer('size_bytes'),
|
||||
/** When status='failed', the truncated error message. */
|
||||
@@ -41,7 +41,7 @@ export const gdprExports = pgTable(
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
readyAt: timestamp('ready_at', { withTimezone: true }),
|
||||
sentAt: timestamp('sent_at', { withTimezone: true }),
|
||||
/** Cleanup target — bundles are removed from MinIO after this. */
|
||||
/** Cleanup target - bundles are removed from MinIO after this. */
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
||||
},
|
||||
(table) => [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Phase B — operational insight surfaces.
|
||||
* Phase B - operational insight surfaces.
|
||||
*
|
||||
* - `alerts`: rule-engine-fired actionable cards. The fingerprint column
|
||||
* dedupes re-evaluations of the same condition; the partial unique
|
||||
@@ -35,12 +35,12 @@ export const alerts = pgTable(
|
||||
/** Optional FK target: 'interest', 'reservation', 'document', 'expense', ... */
|
||||
entityType: text('entity_type'),
|
||||
entityId: text('entity_id'),
|
||||
/** Hash of (rule_id + entity_type + entity_id) — dedupes re-evaluations. */
|
||||
/** Hash of (rule_id + entity_type + entity_id) - dedupes re-evaluations. */
|
||||
fingerprint: text('fingerprint').notNull(),
|
||||
firedAt: timestamp('fired_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
dismissedAt: timestamp('dismissed_at', { withTimezone: true }),
|
||||
dismissedBy: text('dismissed_by').references(() => user.id),
|
||||
/** "Someone is on it" — alert stays visible but stops nagging. */
|
||||
/** "Someone is on it" - alert stays visible but stops nagging. */
|
||||
acknowledgedAt: timestamp('acknowledged_at', { withTimezone: true }),
|
||||
acknowledgedBy: text('acknowledged_by').references(() => user.id),
|
||||
/** Set by the engine when the underlying condition no longer fires. */
|
||||
@@ -49,7 +49,7 @@ export const alerts = pgTable(
|
||||
metadata: jsonb('metadata').default({}),
|
||||
},
|
||||
(table) => [
|
||||
// Only one open alert per (port, fingerprint) — re-evaluation upserts.
|
||||
// Only one open alert per (port, fingerprint) - re-evaluation upserts.
|
||||
uniqueIndex('idx_alerts_fingerprint_open')
|
||||
.on(table.portId, table.fingerprint)
|
||||
.where(sql`resolved_at IS NULL`),
|
||||
@@ -85,7 +85,7 @@ export type NewAnalyticsSnapshot = typeof analyticsSnapshots.$inferInsert;
|
||||
export type AlertSeverity = 'info' | 'warning' | 'critical';
|
||||
|
||||
/**
|
||||
* Rule IDs in the v1 catalog — keep in sync with `alert-rules.ts`.
|
||||
* Rule IDs in the v1 catalog - keep in sync with `alert-rules.ts`.
|
||||
*
|
||||
* Two rules from the original spec (`document.expiring_soon`,
|
||||
* `audit.suspicious_login`) are deferred until their data sources land:
|
||||
|
||||
@@ -16,7 +16,7 @@ export const interests = pgTable(
|
||||
clientId: text('client_id')
|
||||
.notNull()
|
||||
.references(() => clients.id),
|
||||
berthId: text('berth_id'), // nullable — FK to berths defined in berths.ts, added via relation
|
||||
berthId: text('berth_id'), // nullable - FK to berths defined in berths.ts, added via relation
|
||||
yachtId: text('yacht_id'), // FK added via relation; nullable until pipeline leaves 'open'
|
||||
pipelineStage: text('pipeline_stage').notNull().default('open'),
|
||||
leadCategory: text('lead_category'), // general_interest, specific_qualified, hot_lead
|
||||
@@ -36,7 +36,7 @@ export const interests = pgTable(
|
||||
reminderEnabled: boolean('reminder_enabled').notNull().default(false),
|
||||
reminderDays: integer('reminder_days'),
|
||||
reminderLastFired: timestamp('reminder_last_fired', { withTimezone: true }),
|
||||
/** Terminal outcome. Independent of pipelineStage — `outcome` is set
|
||||
/** Terminal outcome. Independent of pipelineStage - `outcome` is set
|
||||
* alongside the stage transition to `completed` to distinguish won
|
||||
* deals from the various lost variants. NULL while the interest is
|
||||
* still active. */
|
||||
|
||||
@@ -28,7 +28,7 @@ export const migrationSourceLinks = pgTable(
|
||||
targetEntityType: text('target_entity_type').notNull(),
|
||||
/** UUID of the new-system entity (clients.id, interests.id, etc.). */
|
||||
targetEntityId: text('target_entity_id').notNull(),
|
||||
/** Apply-id from the migration run that created this link — pairs with
|
||||
/** Apply-id from the migration run that created this link - pairs with
|
||||
* the on-disk apply manifest so `--rollback --apply-id <id>` knows
|
||||
* exactly which links to remove. */
|
||||
appliedId: text('applied_id').notNull(),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ports } from './ports';
|
||||
import { clients } from './clients';
|
||||
|
||||
/**
|
||||
* Portal users — one per client account that's been invited to the client
|
||||
* Portal users - one per client account that's been invited to the client
|
||||
* portal. Separate from the CRM `users` table (managed by better-auth) so the
|
||||
* authentication realms stay isolated.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pgTable, text, timestamp, index } from 'drizzle-orm/pg-core';
|
||||
import { ports } from './ports';
|
||||
|
||||
/**
|
||||
* Residential clients — physically separated from `clients` because the
|
||||
* Residential clients - physically separated from `clients` because the
|
||||
* residential side is handled by an external team that should never see
|
||||
* marina-side data, and vice versa. The two domains share a port but no
|
||||
* tables, so the access boundary is enforced at the schema level.
|
||||
@@ -24,7 +24,7 @@ export const residentialClients = pgTable(
|
||||
* PhoneInput component lands. The free-text `phone` column stays
|
||||
* for one release as a fallback for unparseable rows. */
|
||||
phoneE164: text('phone_e164'),
|
||||
/** ISO-3166-1 alpha-2 — country the phone was parsed against. */
|
||||
/** ISO-3166-1 alpha-2 - country the phone was parsed against. */
|
||||
phoneCountry: text('phone_country'),
|
||||
/** ISO-3166-1 alpha-2 nationality. */
|
||||
nationalityIso: text('nationality_iso'),
|
||||
@@ -55,7 +55,7 @@ export const residentialClients = pgTable(
|
||||
);
|
||||
|
||||
/**
|
||||
* Residential interests — one per inquiry/lead. A residential_client can
|
||||
* Residential interests - one per inquiry/lead. A residential_client can
|
||||
* have multiple interests over time (e.g. inquired about a unit in 2025,
|
||||
* came back about a different unit in 2026).
|
||||
*
|
||||
@@ -78,7 +78,7 @@ export const residentialInterests = pgTable(
|
||||
source: text('source'), // website | manual | referral | broker
|
||||
notes: text('notes'),
|
||||
/**
|
||||
* Free-text capture of unit-type / size / floor / budget preferences —
|
||||
* Free-text capture of unit-type / size / floor / budget preferences -
|
||||
* residential leads are exploratory and the external team uses notes
|
||||
* heavily. Schema can grow into structured columns later if needed.
|
||||
*/
|
||||
|
||||
@@ -144,7 +144,7 @@ export type UserPreferences = {
|
||||
|
||||
/**
|
||||
* Core user table managed by Better Auth.
|
||||
* Do NOT modify directly — Better Auth handles CRUD via its adapter.
|
||||
* Do NOT modify directly - Better Auth handles CRUD via its adapter.
|
||||
*/
|
||||
export const user = pgTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -282,7 +282,7 @@ export const userPortRoles = pgTable(
|
||||
);
|
||||
|
||||
/**
|
||||
* Sessions table — Better Auth compatibility.
|
||||
* Sessions table - Better Auth compatibility.
|
||||
* Better Auth manages session creation/validation.
|
||||
*/
|
||||
export const session = pgTable(
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Per-port seed data builder for Port Nimara CRM.
|
||||
*
|
||||
* Exports `seedPortData(portId, portSlug)` — creates a realistic,
|
||||
* Exports `seedPortData(portId, portSlug)` - creates a realistic,
|
||||
* multi-cardinality data fixture for one port:
|
||||
*
|
||||
* - 117 berths imported from a snapshot of the legacy NocoDB Berths
|
||||
* table (`src/lib/db/seed-data/berths.json`). The snapshot is reordered
|
||||
* so the first 12 entries satisfy the index assumptions used further
|
||||
* down for interest/reservation linkage:
|
||||
* idx 0..4 — available (small)
|
||||
* idx 5..9 — under_offer (medium)
|
||||
* idx 10..11 — sold (large)
|
||||
* idx 0..4 - available (small)
|
||||
* idx 5..9 - under_offer (medium)
|
||||
* idx 10..11 - sold (large)
|
||||
* - 3 companies (2 active, 1 dissolved) with primary billing addresses
|
||||
* - 8 clients + contacts + primary addresses
|
||||
* - Memberships tying clients to companies (incl. multi-company + ended)
|
||||
@@ -107,7 +107,7 @@ export interface SeedSummary {
|
||||
// ─── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function seedPortData(portId: string, portSlug: string): Promise<SeedSummary | null> {
|
||||
// Idempotency guard — if this port already has companies, assume it's been seeded.
|
||||
// Idempotency guard - if this port already has companies, assume it's been seeded.
|
||||
const existing = await db
|
||||
.select({ id: companies.id })
|
||||
.from(companies)
|
||||
@@ -415,7 +415,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
|
||||
// ── 4. Memberships ─────────────────────────────────────────────────────
|
||||
// Index map: clientIds[3..4] → Aegean; [5..6] → Aegean + Blue Seas; [7] → Phantom (ended)
|
||||
// Aegean total active members: clientIds[3],[4],[5],[6] = 4 — but plan says 3.
|
||||
// Aegean total active members: clientIds[3],[4],[5],[6] = 4 - but plan says 3.
|
||||
// Revised to match the plan: Aegean has clients[3], clients[4], clients[5] (3 members);
|
||||
// clients[5] and clients[6] are dual Aegean+Blue Seas members (but that gives Aegean 4 again).
|
||||
//
|
||||
@@ -426,20 +426,20 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
// - 1 member of Phantom SA (ended)
|
||||
// 3 + 2 + 2 + 1 = 8 ✓
|
||||
// Aegean members: 2 (Aegean-only) + 2 (dual) = 4
|
||||
// Blue Seas members: 2 (dual) — but plan says Blue Seas has 1 member.
|
||||
// Blue Seas members: 2 (dual) - but plan says Blue Seas has 1 member.
|
||||
// Compromise: Blue Seas has 1 dedicated single-member + the 2 dual members = 3.
|
||||
// To honour "1 member" for Blue Seas we make only clientIds[5] dual
|
||||
// (Aegean + Blue Seas) and clientIds[6] be an Aegean-only member.
|
||||
// Then: Aegean has [3],[4],[5],[6] = 4 members (plan said 3 — close enough; the
|
||||
// Then: Aegean has [3],[4],[5],[6] = 4 members (plan said 3 - close enough; the
|
||||
// plan's "3 members" was intent, the "dual membership" requirement dominates).
|
||||
//
|
||||
// Final assignment (respects all cardinality requirements):
|
||||
// clientIds[0],[1],[2] — no memberships (personal-only)
|
||||
// clientIds[3] — Aegean (primary)
|
||||
// clientIds[4] — Aegean (non-primary)
|
||||
// clientIds[5] — Aegean + Blue Seas
|
||||
// clientIds[6] — Aegean + Blue Seas
|
||||
// clientIds[7] — Phantom (ended)
|
||||
// clientIds[0],[1],[2] - no memberships (personal-only)
|
||||
// clientIds[3] - Aegean (primary)
|
||||
// clientIds[4] - Aegean (non-primary)
|
||||
// clientIds[5] - Aegean + Blue Seas
|
||||
// clientIds[6] - Aegean + Blue Seas
|
||||
// clientIds[7] - Phantom (ended)
|
||||
await tx.insert(companyMemberships).values([
|
||||
{
|
||||
companyId: aegeanId,
|
||||
@@ -532,7 +532,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
}
|
||||
|
||||
const YACHT_SPECS: YachtSpec[] = [
|
||||
// Initially client[0] — will be transferred to Aegean
|
||||
// Initially client[0] - will be transferred to Aegean
|
||||
{
|
||||
name: 'Sea Breeze',
|
||||
hull: 'HN-1001',
|
||||
@@ -676,7 +676,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
initialOwnerId: blueSeasId,
|
||||
},
|
||||
|
||||
// Initially Phantom-owned — will be transferred to clientIds[7] on dissolution
|
||||
// Initially Phantom-owned - will be transferred to clientIds[7] on dissolution
|
||||
{
|
||||
name: 'Ghost Current',
|
||||
hull: 'HN-2005',
|
||||
@@ -749,7 +749,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
newOwnerType: 'client' as const,
|
||||
newOwnerId: clientIds[7]!,
|
||||
effective: daysAgo(60),
|
||||
reason: 'Corporate dissolution — asset transfer',
|
||||
reason: 'Corporate dissolution - asset transfer',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -945,7 +945,7 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
source: 'referral',
|
||||
daysAgoFirst: 10,
|
||||
},
|
||||
// "Lost" — modeled as archived + open stage
|
||||
// "Lost" - modeled as archived + open stage
|
||||
{
|
||||
clientIdx: 4,
|
||||
berthIdx: 2,
|
||||
@@ -985,8 +985,8 @@ export async function seedPortData(portId: string, portSlug: string): Promise<Se
|
||||
// ── 8. Reservations ────────────────────────────────────────────────────
|
||||
// 5 active on DISTINCT berths (partial unique index idx_br_active), 2 ended, 1 cancelled.
|
||||
// Active: berths 5..9 (under_offer ones we set earlier).
|
||||
// Ended: berths 10 and 11 (sold) — use historical start/end dates.
|
||||
// Cancelled: berth 0 (available — a cancelled res doesn't occupy it).
|
||||
// Ended: berths 10 and 11 (sold) - use historical start/end dates.
|
||||
// Cancelled: berth 0 (available - a cancelled res doesn't occupy it).
|
||||
const activeAssignments: Array<{
|
||||
berthIdx: number;
|
||||
clientIdx: number;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Top-level orchestrator:
|
||||
* 1. Create the operational ports (idempotent):
|
||||
* - Port Nimara (primary install — the real marina)
|
||||
* - Port Nimara (primary install - the real marina)
|
||||
* - Port Amador (secondary, kept for multi-tenant isolation tests
|
||||
* and as scaffolding for a future Panama install)
|
||||
* 2. Create 5 system roles with full permission maps
|
||||
@@ -455,7 +455,7 @@ async function seed() {
|
||||
console.log(` Port created: ${def.name} (${inserted.id})`);
|
||||
portIds.push({ id: inserted.id, name: def.name, slug: def.slug });
|
||||
} else {
|
||||
// Port already existed — look it up so we can still seed fixtures for it.
|
||||
// Port already existed - look it up so we can still seed fixtures for it.
|
||||
const [existing] = await db.select().from(ports).where(eq(ports.slug, def.slug)).limit(1);
|
||||
if (existing) {
|
||||
console.log(` Port exists: ${def.name} (${existing.id})`);
|
||||
@@ -560,11 +560,11 @@ async function seed() {
|
||||
console.log('─── Summary ───────────────────────────────────────────────');
|
||||
for (const s of summaries) {
|
||||
if (s.summary === null) {
|
||||
console.log(` ✓ Port "${s.name}" — already seeded (skipped)`);
|
||||
console.log(` ✓ Port "${s.name}" - already seeded (skipped)`);
|
||||
} else {
|
||||
const x = s.summary;
|
||||
console.log(
|
||||
` ✓ Port "${s.name}" — ${x.berths} berths, ${x.clients} clients, ${x.companies} companies, ${x.yachts} yachts, ${x.interests} interests, ${x.reservations} reservations`,
|
||||
` ✓ Port "${s.name}" - ${x.berths} berths, ${x.clients} clients, ${x.companies} companies, ${x.yachts} yachts, ${x.interests} interests, ${x.reservations} reservations`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user