Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copy to .env.local and fill in.

# --- Supabase ---
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
# Service-role key — bypasses RLS. Used server-only (src/lib/server/serviceRole.ts).
SUPABASE_SECRET_KEY=

# --- Auth ---
NEXT_PUBLIC_AUTH_CALLBACK_URL=

# --- Feature flags ---
NEXT_PUBLIC_ENABLE_MATCH_REQUESTS=

# --- Matching algorithm ---
# Server-only — never exposed to the browser. The admin "Run Matching" page
# (src/app/admin/matching) calls this through this app's own /api/admin/matching/*
# routes, which proxy to it; the browser never talks to it directly.
# The algorithm itself (ml_microservice) is deployed as a second container
# alongside aspc-website-v2's backend, reachable only through that backend's
# own public URL + /api/ml — not a separate host of its own.
# See aspc-website-v2/backend/.env.example for the other side of this.
ALGORITHM_SERVICE_URL=https://api.pomonastudents.org/api/ml
# Must match ML_PROXY_API_KEY, set as a GitHub secret on aspc-website-v2
# (gh secret set ML_PROXY_API_KEY there — it's a GitHub Actions secret, not
# something that lives in this repo or Vercel automatically).
ALGORITHM_SERVICE_API_KEY=
5 changes: 5 additions & 0 deletions frontend/src/app/admin/matching/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import MatchingAlgorithm from '@/components/admin/MatchingAlgorithm'

export default async function MatchingPage() {
return <MatchingAlgorithm />
}
67 changes: 67 additions & 0 deletions frontend/src/app/api/admin/matching/run/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { badRequestJson, withAdminRoute } from '@/lib/server/auth'
import { NextResponse } from 'next/server'

export const POST = withAdminRoute(async (request) => {
const serviceUrl = process.env.ALGORITHM_SERVICE_URL

if (!serviceUrl) {
return NextResponse.json(
{ error: 'ALGORITHM_SERVICE_URL is not configured on the server.' },
{ status: 503 },
)
}

let body: any
try {
body = await request.json()
} catch {
return badRequestJson('Invalid JSON body.')
}

const { daysAhead, daysAheadStart } = body || {}
if (daysAhead !== undefined && typeof daysAhead !== 'number') {
return badRequestJson('daysAhead must be a number.')
}
if (daysAheadStart !== undefined && typeof daysAheadStart !== 'number') {
return badRequestJson('daysAheadStart must be a number.')
}

const apiKey = process.env.ALGORITHM_SERVICE_API_KEY

try {
// /run matches riders from the database and commits the result to
// Supabase for real (Rides/Matches, voucher consumption) — unlike the
// old /match-based preview, this writes production data.
const response = await fetch(`${serviceUrl}/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(apiKey ? { 'X-API-Key': apiKey } : {}),
},
body: JSON.stringify({
days_ahead: daysAhead,
days_ahead_start: daysAheadStart,
}),
signal: AbortSignal.timeout(60000),
})

const data = await response.json().catch(() => null)

if (!response.ok) {
return NextResponse.json(
{
error: data?.detail || 'The algorithm service rejected the request.',
},
{ status: response.status },
)
}

return NextResponse.json(data)
} catch (error) {
console.error('Algorithm service run failed:', error)
return NextResponse.json(
{ error: 'Failed to reach the algorithm service.' },
{ status: 502 },
)
}
})
46 changes: 46 additions & 0 deletions frontend/src/app/api/admin/matching/status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { withAdminRoute } from '@/lib/server/auth'
import { NextResponse } from 'next/server'

export const GET = withAdminRoute(async () => {
const serviceUrl = process.env.ALGORITHM_SERVICE_URL

if (!serviceUrl) {
return NextResponse.json({
configured: false,
reachable: false,
message: 'ALGORITHM_SERVICE_URL is not set on the server.',
})
}

const apiKey = process.env.ALGORITHM_SERVICE_API_KEY

try {
const response = await fetch(serviceUrl, {
headers: apiKey ? { 'X-API-Key': apiKey } : undefined,
signal: AbortSignal.timeout(5000),
})

if (!response.ok) {
return NextResponse.json({
configured: true,
reachable: false,
message: `Algorithm service responded with ${response.status}.`,
})
}

const data = await response.json()

return NextResponse.json({
configured: true,
reachable: true,
supabaseConfigured: Boolean(data?.supabase_configured),
})
} catch (error) {
console.error('Algorithm service unreachable:', error)
return NextResponse.json({
configured: true,
reachable: false,
message: 'Could not reach the algorithm service.',
})
}
})
5 changes: 5 additions & 0 deletions frontend/src/components/admin/AdminDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ export default function AdminDashboard() {
route="/admin/groups"
variant="cta"
/>
<SimpleRedirectButton
label="Run Matching Algorithm"
route="/admin/matching"
variant="cta"
/>
</div>

{/* Match no-shows — warm orange–yellow banner (Cancellations keeps red–orange) */}
Expand Down
Loading
Loading