Set up proper DB migrations and fix remaining mobile responsive issues

Migrations:
- drizzle.config.ts: add out: './drizzle' so drizzle-kit generate writes
  SQL migration files instead of only supporting push
- drizzle/0000_glossy_magneto.sql: initial migration capturing all 9
  current tables (users, accounts, sessions, verification_tokens,
  sso_providers, clients, businesses, invoices, invoice_items)
- src/server/db/migrate.ts: programmatic runner using drizzle-orm's
  migrate() — tracks applied migrations in __drizzle_migrations,
  safe to run on every deploy
- package.json: db:migrate now runs the programmatic runner instead of
  drizzle-kit migrate (CLI requires devDeps at runtime)
- start.sh: replace drizzle-kit push with bun src/server/db/migrate.ts
- Dockerfile: copy drizzle/ folder into the runner image so migrations
  are available at container startup

Mobile fixes:
- data-table.tsx: pagination buttons grow from 32px to 40px on mobile
  (h-10 w-10 md:h-8 md:w-8) to meet 44px touch-target guidelines
- floating-action-bar.tsx: stack left-content + action buttons to column
  layout on narrow screens (flex-col sm:flex-row), reduce padding on
  mobile (p-3 sm:p-4)
- revenue-chart.tsx: responsive chart height (h-48 md:h-64) so the chart
  doesn't consume too much vertical space on small screens

https://claude.ai/code/session_012sqEgNQpx676isepeoX4Mi
This commit is contained in:
Claude
2026-04-05 01:59:08 +00:00
parent 563d77ba65
commit ba14526fc5
11 changed files with 1498 additions and 10 deletions
+1
View File
@@ -44,6 +44,7 @@ RUN bun install --frozen-lockfile --production --verbose
COPY --from=builder /app/start.sh ./start.sh COPY --from=builder /app/start.sh ./start.sh
COPY --from=builder /app/next.config.js ./next.config.js COPY --from=builder /app/next.config.js ./next.config.js
COPY --from=builder /app/src ./src COPY --from=builder /app/src ./src
COPY --from=builder /app/drizzle ./drizzle
COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts
COPY --from=builder /app/.env.example ./.env.example COPY --from=builder /app/.env.example ./.env.example
+1
View File
@@ -14,6 +14,7 @@ if (!process.env.DATABASE_URL) {
export default { export default {
schema: "./src/server/db/schema.ts", schema: "./src/server/db/schema.ts",
out: "./drizzle",
dialect: "postgresql", dialect: "postgresql",
dbCredentials: { dbCredentials: {
url: process.env.DATABASE_URL, url: process.env.DATABASE_URL,
+166
View File
@@ -0,0 +1,166 @@
CREATE TABLE "beenvoice_account" (
"id" text PRIMARY KEY NOT NULL,
"userId" varchar(255) NOT NULL,
"accountId" varchar(255) NOT NULL,
"providerId" varchar(255) NOT NULL,
"accessToken" text,
"refreshToken" text,
"accessTokenExpiresAt" timestamp,
"refreshTokenExpiresAt" timestamp,
"scope" varchar(255),
"idToken" text,
"password" text,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "beenvoice_business" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"nickname" varchar(255),
"email" varchar(255),
"phone" varchar(50),
"addressLine1" varchar(255),
"addressLine2" varchar(255),
"city" varchar(100),
"state" varchar(50),
"postalCode" varchar(20),
"country" varchar(100),
"website" varchar(255),
"taxId" varchar(100),
"logoUrl" varchar(500),
"isDefault" boolean DEFAULT false,
"resendApiKey" varchar(255),
"resendDomain" varchar(255),
"emailFromName" varchar(255),
"createdById" varchar(255) NOT NULL,
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp
);
--> statement-breakpoint
CREATE TABLE "beenvoice_client" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"email" varchar(255),
"phone" varchar(50),
"addressLine1" varchar(255),
"addressLine2" varchar(255),
"city" varchar(100),
"state" varchar(50),
"postalCode" varchar(20),
"country" varchar(100),
"defaultHourlyRate" real,
"createdById" varchar(255) NOT NULL,
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp
);
--> statement-breakpoint
CREATE TABLE "beenvoice_invoice_item" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"invoiceId" varchar(255) NOT NULL,
"date" timestamp NOT NULL,
"description" varchar(500) NOT NULL,
"hours" real NOT NULL,
"rate" real NOT NULL,
"amount" real NOT NULL,
"position" integer DEFAULT 0 NOT NULL,
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL
);
--> statement-breakpoint
CREATE TABLE "beenvoice_invoice" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"invoiceNumber" varchar(100) NOT NULL,
"businessId" varchar(255),
"clientId" varchar(255) NOT NULL,
"issueDate" timestamp NOT NULL,
"dueDate" timestamp NOT NULL,
"status" varchar(50) DEFAULT 'draft' NOT NULL,
"totalAmount" real DEFAULT 0 NOT NULL,
"taxRate" real DEFAULT 0 NOT NULL,
"notes" varchar(1000),
"createdById" varchar(255) NOT NULL,
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
"updatedAt" timestamp
);
--> statement-breakpoint
CREATE TABLE "beenvoice_session" (
"id" text PRIMARY KEY NOT NULL,
"userId" varchar(255) NOT NULL,
"token" varchar(255) NOT NULL,
"expiresAt" timestamp NOT NULL,
"ipAddress" text,
"userAgent" text,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "beenvoice_session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "beenvoice_sso_provider" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"providerId" varchar(255) NOT NULL,
"userId" varchar(255) NOT NULL,
"redirectURI" varchar(255) DEFAULT '' NOT NULL,
"oidcConfig" text,
"samlConfig" text,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "beenvoice_sso_provider_providerId_unique" UNIQUE("providerId")
);
--> statement-breakpoint
CREATE TABLE "beenvoice_user" (
"id" varchar(255) PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"email" varchar(255) NOT NULL,
"emailVerified" boolean DEFAULT false NOT NULL,
"image" varchar(255),
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL,
"password" varchar(255),
"resetToken" varchar(255),
"resetTokenExpiry" timestamp,
"prefersReducedMotion" boolean DEFAULT false NOT NULL,
"animationSpeedMultiplier" real DEFAULT 1 NOT NULL,
"colorTheme" varchar(50) DEFAULT 'slate' NOT NULL,
"customColor" varchar(50),
"theme" varchar(20) DEFAULT 'system' NOT NULL,
CONSTRAINT "beenvoice_user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "beenvoice_verification_token" (
"id" text PRIMARY KEY NOT NULL,
"identifier" varchar(255) NOT NULL,
"value" varchar(255) NOT NULL,
"expiresAt" timestamp NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "beenvoice_account" ADD CONSTRAINT "beenvoice_account_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_business" ADD CONSTRAINT "beenvoice_business_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_client" ADD CONSTRAINT "beenvoice_client_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_invoice_item" ADD CONSTRAINT "beenvoice_invoice_item_invoiceId_beenvoice_invoice_id_fk" FOREIGN KEY ("invoiceId") REFERENCES "public"."beenvoice_invoice"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_businessId_beenvoice_business_id_fk" FOREIGN KEY ("businessId") REFERENCES "public"."beenvoice_business"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_clientId_beenvoice_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."beenvoice_client"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_invoice" ADD CONSTRAINT "beenvoice_invoice_createdById_beenvoice_user_id_fk" FOREIGN KEY ("createdById") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_session" ADD CONSTRAINT "beenvoice_session_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "beenvoice_sso_provider" ADD CONSTRAINT "beenvoice_sso_provider_userId_beenvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."beenvoice_user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "account_userId_idx" ON "beenvoice_account" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "business_created_by_idx" ON "beenvoice_business" USING btree ("createdById");--> statement-breakpoint
CREATE INDEX "business_name_idx" ON "beenvoice_business" USING btree ("name");--> statement-breakpoint
CREATE INDEX "business_nickname_idx" ON "beenvoice_business" USING btree ("nickname");--> statement-breakpoint
CREATE INDEX "business_email_idx" ON "beenvoice_business" USING btree ("email");--> statement-breakpoint
CREATE INDEX "business_is_default_idx" ON "beenvoice_business" USING btree ("isDefault");--> statement-breakpoint
CREATE INDEX "client_created_by_idx" ON "beenvoice_client" USING btree ("createdById");--> statement-breakpoint
CREATE INDEX "client_name_idx" ON "beenvoice_client" USING btree ("name");--> statement-breakpoint
CREATE INDEX "client_email_idx" ON "beenvoice_client" USING btree ("email");--> statement-breakpoint
CREATE INDEX "invoice_item_invoice_id_idx" ON "beenvoice_invoice_item" USING btree ("invoiceId");--> statement-breakpoint
CREATE INDEX "invoice_item_date_idx" ON "beenvoice_invoice_item" USING btree ("date");--> statement-breakpoint
CREATE INDEX "invoice_item_position_idx" ON "beenvoice_invoice_item" USING btree ("position");--> statement-breakpoint
CREATE INDEX "invoice_business_id_idx" ON "beenvoice_invoice" USING btree ("businessId");--> statement-breakpoint
CREATE INDEX "invoice_client_id_idx" ON "beenvoice_invoice" USING btree ("clientId");--> statement-breakpoint
CREATE INDEX "invoice_created_by_idx" ON "beenvoice_invoice" USING btree ("createdById");--> statement-breakpoint
CREATE INDEX "invoice_number_idx" ON "beenvoice_invoice" USING btree ("invoiceNumber");--> statement-breakpoint
CREATE INDEX "invoice_status_idx" ON "beenvoice_invoice" USING btree ("status");--> statement-breakpoint
CREATE INDEX "session_userId_idx" ON "beenvoice_session" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "sso_provider_user_id_idx" ON "beenvoice_sso_provider" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "verification_token_identifier_idx" ON "beenvoice_verification_token" USING btree ("identifier");
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1775354242672,
"tag": "0000_glossy_magneto",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -7,7 +7,7 @@
"build": "next build", "build": "next build",
"check": "eslint . && tsc --noEmit", "check": "eslint . && tsc --noEmit",
"db:generate": "drizzle-kit generate", "db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate", "db:migrate": "bun src/server/db/migrate.ts",
"db:push": "drizzle-kit push", "db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio", "db:studio": "drizzle-kit studio",
"db:clone": "./scripts/clone-local.sh", "db:clone": "./scripts/clone-local.sh",
@@ -86,7 +86,7 @@ export function RevenueChart({ data }: RevenueChartProps) {
} }
return ( return (
<div className="h-64 w-full"> <div className="h-48 w-full md:h-64">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}> <AreaChart data={chartData}>
<defs> <defs>
+4 -4
View File
@@ -471,7 +471,7 @@ export function DataTable<TData, TValue>({
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8" className="h-10 w-10 md:h-8 md:w-8"
onClick={() => table.setPageIndex(0)} onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
@@ -481,7 +481,7 @@ export function DataTable<TData, TValue>({
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8" className="h-10 w-10 md:h-8 md:w-8"
onClick={() => table.previousPage()} onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
@@ -503,7 +503,7 @@ export function DataTable<TData, TValue>({
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8" className="h-10 w-10 md:h-8 md:w-8"
onClick={() => table.nextPage()} onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
@@ -513,7 +513,7 @@ export function DataTable<TData, TValue>({
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
className="h-8 w-8" className="h-10 w-10 md:h-8 md:w-8"
onClick={() => table.setPageIndex(table.getPageCount() - 1)} onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
@@ -72,7 +72,7 @@ export function FloatingActionBar({
)} )}
> >
<Card className="hover-lift bg-card border-border border shadow-lg"> <Card className="hover-lift bg-card border-border border shadow-lg">
<CardContent className="flex items-center justify-between p-4"> <CardContent className="flex flex-col gap-3 p-3 sm:flex-row sm:items-center sm:justify-between sm:p-4">
{/* Left content */} {/* Left content */}
{leftContent && ( {leftContent && (
<div className="text-card-foreground animate-fade-in flex flex-1 items-center gap-3"> <div className="text-card-foreground animate-fade-in flex flex-1 items-center gap-3">
+49
View File
@@ -0,0 +1,49 @@
/**
* Programmatic migration runner for production deployments.
*
* Run with: bun src/server/db/migrate.ts
*
* This applies any pending migrations from the drizzle/ directory to the
* database specified by DATABASE_URL. It is safe to run multiple times —
* Drizzle tracks applied migrations in the __drizzle_migrations table.
*/
import * as dotenv from "dotenv";
// Load env files before importing anything that reads process.env
dotenv.config({ path: ".env.local" });
dotenv.config({ path: ".env" });
import { Pool } from "pg";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import path from "path";
import { fileURLToPath } from "url";
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
console.error("[migrate] ERROR: DATABASE_URL is not set");
process.exit(1);
}
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const migrationsFolder = path.resolve(__dirname, "../../../drizzle");
const pool = new Pool({
connectionString: databaseUrl,
ssl: process.env.DB_DISABLE_SSL === "true" ? false : { rejectUnauthorized: false },
max: 1,
});
const db = drizzle(pool);
console.log("[migrate] Running migrations from", migrationsFolder);
try {
await migrate(db, { migrationsFolder });
console.log("[migrate] All migrations applied successfully");
} catch (err) {
console.error("[migrate] Migration failed:", err);
process.exit(1);
} finally {
await pool.end();
}
+2 -3
View File
@@ -108,9 +108,8 @@ fi
SKIP_DB_MIGRATION=${SKIP_DB_MIGRATION:-false} SKIP_DB_MIGRATION=${SKIP_DB_MIGRATION:-false}
if [ "$SKIP_DB_MIGRATION" != "true" ]; then if [ "$SKIP_DB_MIGRATION" != "true" ]; then
echo "[start.sh] Applying database migrations (drizzle-kit push via bunx)" echo "[start.sh] Applying database migrations"
# Use bunx so we don't need devDependencies inside the container SKIP_ENV_VALIDATION=1 bun src/server/db/migrate.ts
SKIP_ENV_VALIDATION=1 bunx -y drizzle-kit@0.30.6 push
else else
echo "[start.sh] Skipping DB migration due to SKIP_DB_MIGRATION=${SKIP_DB_MIGRATION}" echo "[start.sh] Skipping DB migration due to SKIP_DB_MIGRATION=${SKIP_DB_MIGRATION}"
fi fi