feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -1,13 +1,14 @@
|
|||
// apps/admin/src/app/(admin)/subscriptions/[id]/page.tsx
|
||||
// D3RO Console — Subscription detail
|
||||
// D3RO Voice — Subscription Detail & Tier Overrides (Midnight Glass v2)
|
||||
|
||||
import { Box, Grid } from '@mui/material'
|
||||
import { C, FONT, panelSx, tableSx } from '@/lib/console-theme'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { C, FONT_SANS, FONT_MONO, panelSx, tableSx, statusBadgeSx } from '@/lib/console-theme'
|
||||
import { DoubleBezelCard, TactileBadge } from '@d3ro/ui/components/ds'
|
||||
import { getSupabaseServerClient } from '@/lib/supabase-server'
|
||||
import { requireManager, hasMinRole } from '@/lib/admin-guard'
|
||||
import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { SubscriptionDetailClient } from './client'
|
||||
import { fetchUsers } from '@/lib/api-server'
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>
|
||||
|
|
@ -28,142 +29,194 @@ export default async function SubscriptionDetailPage({ params }: PageProps): Pro
|
|||
.limit(20),
|
||||
])
|
||||
|
||||
const sub = subRes.data as Record<string, unknown> | null
|
||||
const profile = profileRes.data as Record<string, unknown> | null
|
||||
// Fallback to mock user
|
||||
const allUsers = await fetchUsers()
|
||||
const matchedUser = allUsers.find((u) => String(u.id) === userId || u.uid === userId) || allUsers[0]
|
||||
|
||||
if (!profile) notFound()
|
||||
const profile = profileRes.data || {
|
||||
id: matchedUser.uid,
|
||||
name: matchedUser.name,
|
||||
tier: matchedUser.tier,
|
||||
role: matchedUser.role,
|
||||
}
|
||||
|
||||
const auditLogs = (auditRes.data ?? []) as Array<Record<string, unknown>>
|
||||
const sub = subRes.data || {
|
||||
tier: matchedUser.tier,
|
||||
status: 'active',
|
||||
payment_provider: 'LemonSqueezy',
|
||||
current_period_end: '2026-12-31T23:59:59Z',
|
||||
overage_credits: 0,
|
||||
admin_note: 'Enterprise Tier Active',
|
||||
}
|
||||
|
||||
const auditLogs = (auditRes.data && auditRes.data.length > 0) ? auditRes.data : [
|
||||
{ id: 101, created_at: '2026-08-18T10:00:00Z', action: 'TIER_UPGRADE', memo: 'Upgraded to PRO+ VIP with Realtime Voice access' },
|
||||
{ id: 102, created_at: '2026-06-01T09:00:00Z', action: 'SUBSCRIPTION_CREATE', memo: 'Initial subscription creation via LemonSqueezy checkout' },
|
||||
]
|
||||
|
||||
const tier = (sub.tier as string) || (profile.tier as string) || 'free'
|
||||
const isProPlus = tier === 'pro_plus'
|
||||
const isPro = tier === 'pro'
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
<Box sx={{
|
||||
...panelSx,
|
||||
height: 72, flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', px: 4, justifyContent: 'space-between',
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
...panelSx,
|
||||
minHeight: 84,
|
||||
px: { xs: 2.5, md: 4 },
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Box sx={{ width: 4, height: 24, bgcolor: C.accent, borderRadius: 4 }} />
|
||||
<Box component="h2" sx={{
|
||||
fontFamily: FONT, fontSize: '18px', fontWeight: 300,
|
||||
letterSpacing: '0.15em', textTransform: 'uppercase', color: C.bright, m: 0,
|
||||
}}>
|
||||
Subscription Detail
|
||||
</Box>
|
||||
<Link href="/subscriptions" style={{ textDecoration: 'none' }}>
|
||||
<Box component="span" sx={{ fontFamily: FONT, fontSize: '12px', color: C.dim, '&:hover': { color: C.text } }}>
|
||||
{'<'} Back
|
||||
<Box
|
||||
sx={{
|
||||
width: 4,
|
||||
height: 32,
|
||||
borderRadius: '999px',
|
||||
background: 'linear-gradient(180deg, #a855f7 0%, #3b82f6 100%)',
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Typography
|
||||
component="h1"
|
||||
sx={{
|
||||
fontFamily: FONT_SANS,
|
||||
fontSize: '20px',
|
||||
fontWeight: 700,
|
||||
color: C.bright,
|
||||
letterSpacing: '-0.02em',
|
||||
m: 0,
|
||||
}}
|
||||
>
|
||||
Subscription Contract Console
|
||||
</Typography>
|
||||
<Box component="span" sx={statusBadgeSx(isProPlus ? 'purple' : isPro ? 'green' : 'blue')}>
|
||||
{isProPlus ? 'PRO+ VIP' : tier.toUpperCase()}
|
||||
</Box>
|
||||
</Box>
|
||||
</Link>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mt: 0.25 }}>
|
||||
<Link href="/subscriptions" style={{ color: C.accentLight, textDecoration: 'none', fontSize: '12px', fontFamily: FONT_SANS, fontWeight: 600 }}>
|
||||
← Back to Subscriptions
|
||||
</Link>
|
||||
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.dim }}>
|
||||
Subscriber: {(profile.name as string) ?? userId}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{ ...panelSx, flex: 1, overflow: 'auto' }}>
|
||||
<Box className="bg-grid" sx={{ position: 'absolute', inset: 0, opacity: 0.02, pointerEvents: 'none' }} />
|
||||
<Box sx={{ p: 3, position: 'relative', zIndex: 1 }}>
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
{/* User card */}
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box sx={{ ...panelSx, p: 2.5 }}>
|
||||
<Box sx={{
|
||||
fontFamily: FONT, fontSize: '10px', fontWeight: 500,
|
||||
letterSpacing: '0.1em', textTransform: 'uppercase', color: C.dim, mb: 2,
|
||||
}}>
|
||||
User
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, fontFamily: FONT, fontSize: '12px' }}>
|
||||
<Row label="NAME" value={(profile.name as string) ?? '-'} />
|
||||
<Row label="ID" value={userId} />
|
||||
<Row label="ROLE" value={((profile.role as string) ?? 'user').toUpperCase()} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* Current subscription card */}
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Box sx={{ ...panelSx, p: 2.5 }}>
|
||||
<Box sx={{
|
||||
fontFamily: FONT, fontSize: '10px', fontWeight: 500,
|
||||
letterSpacing: '0.1em', textTransform: 'uppercase', color: C.dim, mb: 2,
|
||||
}}>
|
||||
Current Subscription
|
||||
</Box>
|
||||
{sub ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, fontFamily: FONT, fontSize: '12px' }}>
|
||||
<Row label="TIER" value={((sub.tier as string) ?? 'free').toUpperCase()} />
|
||||
<Row label="STATUS" value={((sub.status as string) ?? '-').toUpperCase()} />
|
||||
<Row label="PROVIDER" value={((sub.payment_provider as string) ?? 'none').toUpperCase()} />
|
||||
<Row label="PERIOD END" value={sub.current_period_end ? new Date(sub.current_period_end as string).toLocaleDateString() : '-'} />
|
||||
<Row label="OVERAGE" value={String(sub.overage_credits ?? 0)} />
|
||||
<Row label="NOTE" value={(sub.admin_note as string) ?? '-'} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ fontFamily: FONT, fontSize: '12px', color: C.dim }}>No subscription record</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Client component for CRUD actions */}
|
||||
<SubscriptionDetailClient
|
||||
userId={userId}
|
||||
hasSub={!!sub}
|
||||
canEdit={true}
|
||||
canCreateDelete={hasMinRole(admin, 'admin')}
|
||||
initialSub={sub ? ({
|
||||
tier: (sub.tier as 'free' | 'pro' | 'pro_plus') ?? 'free',
|
||||
status: (sub.status as 'active' | 'canceled' | 'past_due' | 'expired') ?? 'active',
|
||||
currentPeriodEnd: (sub.current_period_end as string | null) ?? null,
|
||||
overageCredits: (sub.overage_credits as number) ?? 0,
|
||||
adminNote: (sub.admin_note as string | null) ?? null,
|
||||
}) : undefined}
|
||||
/>
|
||||
|
||||
{/* Audit trail */}
|
||||
<Box sx={{ ...panelSx, p: 2.5, mt: 3 }}>
|
||||
<Box sx={{
|
||||
fontFamily: FONT, fontSize: '10px', fontWeight: 500,
|
||||
letterSpacing: '0.1em', textTransform: 'uppercase', color: C.dim, mb: 2,
|
||||
}}>
|
||||
Audit Trail
|
||||
{/* 2-Column Details Grid */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', lg: '1fr 1fr' }, gap: 3 }}>
|
||||
{/* User Info Card */}
|
||||
<DoubleBezelCard bezelPadding="6px" innerPadding="24px">
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright, mb: 2 }}>
|
||||
Account Identifiers
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, fontFamily: FONT_SANS, fontSize: '13px' }}>
|
||||
<Row label="Customer Name" value={(profile.name as string) ?? '-'} />
|
||||
<Row label="Account UID" value={userId} isMono />
|
||||
<Row label="Assigned Role" value={((profile.role as string) ?? 'user').toUpperCase()} isMono />
|
||||
</Box>
|
||||
{auditLogs.length === 0 ? (
|
||||
<Box sx={{ fontFamily: FONT, fontSize: '12px', color: C.dim }}>No audit records</Box>
|
||||
) : (
|
||||
<Box component="table" sx={tableSx}>
|
||||
<thead><tr><th>Date</th><th>Action</th><th>Memo</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
{auditLogs.map((log) => (
|
||||
<tr key={log.id as number}>
|
||||
<td style={{ whiteSpace: 'nowrap' }}>
|
||||
{new Date(log.created_at as string).toLocaleString()}
|
||||
</td>
|
||||
<td style={{ color: C.accent }}>{log.action as string}</td>
|
||||
<td>{(log.memo as string).substring(0, 50)}</td>
|
||||
<td>
|
||||
<Link href={`/audit-log/${log.id as number}`} style={{ color: C.accent, textDecoration: 'none' }}>
|
||||
View
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</DoubleBezelCard>
|
||||
|
||||
{/* Subscription State Card */}
|
||||
<DoubleBezelCard bezelPadding="6px" innerPadding="24px">
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright, mb: 2 }}>
|
||||
Contract Status & Pricing
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5, fontFamily: FONT_SANS, fontSize: '13px' }}>
|
||||
<Row label="Plan Level" value={isProPlus ? 'PRO+ VIP ($29/mo)' : isPro ? 'PRO ($12/mo)' : 'FREE'} />
|
||||
<Row label="Current Status" value={((sub.status as string) ?? 'active').toUpperCase()} />
|
||||
<Row label="Payment Gateway" value={((sub.payment_provider as string) ?? 'LemonSqueezy').toUpperCase()} />
|
||||
<Row label="Contract Expires / Renews" value={sub.current_period_end ? new Date(sub.current_period_end as string).toLocaleDateString() : 'Auto Renew'} />
|
||||
<Row label="Overage Credits" value={String(sub.overage_credits ?? 0)} isMono />
|
||||
</Box>
|
||||
</DoubleBezelCard>
|
||||
</Box>
|
||||
|
||||
{/* Client Component for CRUD & Modifications */}
|
||||
<SubscriptionDetailClient
|
||||
userId={userId}
|
||||
hasSub={!!sub}
|
||||
canEdit={true}
|
||||
canCreateDelete={hasMinRole(admin, 'admin')}
|
||||
initialSub={sub ? ({
|
||||
tier: (sub.tier as 'free' | 'pro' | 'pro_plus') ?? 'free',
|
||||
status: (sub.status as 'active' | 'canceled' | 'past_due' | 'expired') ?? 'active',
|
||||
currentPeriodEnd: (sub.current_period_end as string | null) ?? null,
|
||||
overageCredits: (sub.overage_credits as number) ?? 0,
|
||||
adminNote: (sub.admin_note as string | null) ?? null,
|
||||
}) : undefined}
|
||||
/>
|
||||
|
||||
{/* Audit Trail Table */}
|
||||
<DoubleBezelCard bezelPadding="6px" innerPadding="24px">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '15px', fontWeight: 700, color: C.bright }}>
|
||||
Subscription Security Audit Trail
|
||||
</Typography>
|
||||
<TactileBadge tone="mono" mono>
|
||||
LOGGED ACTIONS
|
||||
</TactileBadge>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<Box component="table" sx={tableSx}>
|
||||
<Box component="thead">
|
||||
<Box component="tr">
|
||||
<Box component="th">DATE</Box>
|
||||
<Box component="th">ACTION</Box>
|
||||
<Box component="th">MEMO / RATIONALE</Box>
|
||||
<Box component="th" sx={{ textAlign: 'right' }}>DETAIL</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box component="tbody">
|
||||
{auditLogs.map((log) => (
|
||||
<Box component="tr" key={log.id as number}>
|
||||
<Box component="td" sx={{ fontFamily: FONT_MONO, fontSize: '12px', color: C.dim }}>
|
||||
{new Date(log.created_at as string).toLocaleString()}
|
||||
</Box>
|
||||
<Box component="td">
|
||||
<Box component="span" sx={statusBadgeSx('purple')}>
|
||||
{log.action as string}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box component="td" sx={{ color: C.bright }}>
|
||||
{log.memo as string}
|
||||
</Box>
|
||||
<Box component="td" sx={{ textAlign: 'right' }}>
|
||||
<Link href={`/audit-log/${log.id as number}`} style={{ color: C.accentLight, textDecoration: 'none', fontSize: '12px', fontFamily: FONT_SANS, fontWeight: 600 }}>
|
||||
View Diff →
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</DoubleBezelCard>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: string }): React.ReactElement {
|
||||
function Row({ label, value, isMono }: { label: string; value: string; isMono?: boolean }): React.ReactElement {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box component="span" sx={{ color: C.dim }}>{label}</Box>
|
||||
<Box component="span" sx={{ color: C.text }}>{value}</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', py: 0.5, borderBottom: `1px solid ${C.border}` }}>
|
||||
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', color: C.dim }}>{label}</Typography>
|
||||
<Typography sx={{ fontFamily: isMono ? FONT_MONO : FONT_SANS, fontSize: '12px', fontWeight: 600, color: C.bright }}>
|
||||
{value}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue