chore(style): codebase em-dash sweep + minor layout polish
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped

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:
Matt Ciaccio
2026-05-04 22:57:01 +02:00
parent d62822c284
commit 8699f81879
225 changed files with 844 additions and 845 deletions

View File

@@ -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(),

View File

@@ -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),
],

View File

@@ -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(

View File

@@ -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) => [

View File

@@ -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:

View File

@@ -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. */

View File

@@ -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(),

View File

@@ -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.
*

View File

@@ -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.
*/

View File

@@ -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(

View File

@@ -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;

View File

@@ -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`,
);
}
}