feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s

- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
This commit is contained in:
openclaw
2026-06-09 11:32:17 -06:00
parent bb464bda65
commit 916ad39176
349 changed files with 6706 additions and 3343 deletions
@@ -21,9 +21,6 @@ import {
const BRAND_ID = "64294306-5f42-463d-a5e8-2ad6c81a96de";
const BRAND_NAME = "Tuxedo Corn";
const OLATHE_SWEET_LOGO_DARK =
"https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo.png";
// ── Translations ───────────────────────────────────────────────────────────────
type Translations = {
@@ -167,10 +164,13 @@ interface OpenEntry {
export default function TimeTrackingFieldClient({
brandId,
brandName,
brandAccent,
logoUrl,
}: {
brandId: string;
brandName: string;
brandAccent: string;
logoUrl?: string | null;
}) {
const [lang, setLang] = useState<"en" | "es">("en");
const [screen, setScreen] = useState<Screen>("pin");
@@ -385,7 +385,7 @@ export default function TimeTrackingFieldClient({
<div className="bg-zinc-900 border-b border-zinc-800 px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="relative inline-block h-8 w-auto">
<Image src={OLATHE_SWEET_LOGO_DARK} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="opacity-80" />
{logoUrl ? <Image src={logoUrl} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="opacity-80" /> : null}
</span>
<span className="text-sm font-semibold text-zinc-400">{brandName}</span>
</div>
@@ -410,6 +410,7 @@ export default function TimeTrackingFieldClient({
onSubmit={handlePinSubmit}
error={error}
submitting={submitting}
logoUrl={logoUrl}
/>
)}
@@ -421,6 +422,7 @@ export default function TimeTrackingFieldClient({
onSelectTask={handleSelectTask}
error={error}
submitting={submitting}
logoUrl={logoUrl}
/>
)}
@@ -433,6 +435,7 @@ export default function TimeTrackingFieldClient({
showOvertimeWarning={showOvertimeWarning ?? false}
onClockOut={() => setScreen("clock_out_form")}
onLogout={handleLogout}
logoUrl={logoUrl}
/>
)}
@@ -452,6 +455,7 @@ export default function TimeTrackingFieldClient({
onBack={() => setScreen("working")}
error={error}
submitting={submitting}
logoUrl={logoUrl}
/>
)}
@@ -462,6 +466,7 @@ export default function TimeTrackingFieldClient({
payPeriod={payPeriod}
clockOutResult={clockOutResult}
onBackToPin={handleBackToPin}
logoUrl={logoUrl}
/>
)}
@@ -483,6 +488,7 @@ function PayPeriodCard({
lang: "en" | "es";
payPeriod: PayPeriodHours | null;
showWarning: boolean;
logoUrl?: string | null;
}) {
if (!payPeriod) return null;
@@ -577,6 +583,7 @@ function WorkingScreen({
showOvertimeWarning,
onClockOut,
onLogout,
logoUrl,
}: {
t: Translations;
lang: "en" | "es";
@@ -585,10 +592,11 @@ function WorkingScreen({
showOvertimeWarning: boolean;
onClockOut: () => void;
onLogout: () => void;
logoUrl?: string | null;
}) {
return (
<div className="space-y-4">
<PayPeriodCard t={t} lang={lang} payPeriod={payPeriod} showWarning={showOvertimeWarning} />
<PayPeriodCard t={t} lang={lang} payPeriod={payPeriod} showWarning={showOvertimeWarning} logoUrl={logoUrl} />
<div className="bg-zinc-900 border border-zinc-800 rounded-2xl p-5 text-center">
<div className="inline-flex items-center justify-center w-14 h-14 rounded-full bg-emerald-900/40 border border-emerald-700/50 mb-3">
@@ -629,6 +637,7 @@ function TaskSelectScreen({
onSelectTask,
error,
submitting,
logoUrl,
}: {
t: Translations;
tasks: TimeTaskField[];
@@ -636,12 +645,13 @@ function TaskSelectScreen({
onSelectTask: (name: string) => void;
error: string | null;
submitting: boolean;
logoUrl?: string | null;
}) {
return (
<div className="space-y-5">
<div className="text-center">
<span className="relative inline-block h-14 w-auto">
<Image src={OLATHE_SWEET_LOGO_DARK} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" />
{logoUrl ? <Image src={logoUrl} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" /> : null}
</span>
<h1 className="text-2xl font-black text-white">{t.title}</h1>
<p className="text-sm text-zinc-400 mt-1">{t.select_task}</p>
@@ -709,6 +719,7 @@ function ClockOutForm({
onBack: () => void;
error: string | null;
submitting: boolean;
logoUrl?: string | null;
}) {
return (
<div className="space-y-5">
@@ -820,6 +831,7 @@ function ClockedOutScreen({
payPeriod: PayPeriodHours | null;
clockOutResult: { clock_out: string; total_minutes: number };
onBackToPin: () => void;
logoUrl?: string | null;
}) {
return (
<div className="space-y-5">
@@ -875,11 +887,13 @@ function PinEntry({
onSubmit,
error,
submitting,
logoUrl,
}: {
t: Translations;
onSubmit: (pin: string) => void;
error: string | null;
submitting: boolean;
logoUrl?: string | null;
}) {
const [pin, setPin] = useState("");
@@ -893,7 +907,7 @@ function PinEntry({
<div className="space-y-8">
<div className="text-center">
<span className="relative inline-block h-14 w-auto">
<Image src={OLATHE_SWEET_LOGO_DARK} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" />
{logoUrl ? <Image src={logoUrl} alt="Olathe Sweet" fill style={{ objectFit: "contain" }} className="mx-auto opacity-80 mb-3" /> : null}
</span>
<h1 className="text-2xl font-black text-white">{t.title}</h1>
</div>