Skip to content
Merged
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
22 changes: 11 additions & 11 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
NEXT_PUBLIC_SITE_URL=http://localhost:3000

# =============================================================================
# 2. Optional feature: catalogue imports
# 2. Optional feature: catalogue synchronisation
# =============================================================================
# Ordinary catalogue browsing and student planning do not need these credentials.
# Configure them when running the corresponding import operations below.
# Configure them when checking detailed ANU source material.

# Privileged Supabase server key for local preview and browser test user setup.
# dev:local supplies the local server key automatically.
Expand All @@ -50,18 +50,18 @@ NEXT_PUBLIC_SITE_URL=http://localhost:3000
# Used by: lib/catalogue-import/openrouter.ts.
# OPENROUTER_API_KEY=

# Direct Postgres connection for the import worker on Vercel. Development and
# Direct Postgres connection for the sync worker on Vercel. Development and
# the local test server use the loopback database automatically, so leave this
# unset locally. Use the Supabase connection pooler URL for the hosted project.
# Used by: lib/catalogue-import/import-store.ts.
# COURSEMAP_IMPORT_DATABASE_URL=
# Used by: lib/catalogue-sync/sync-store.ts.
# COURSEMAP_SYNC_DATABASE_URL=

# Set to exactly "true" to publish import targets to Vercel Queues
# (topic catalogue-import-v1, consumed by app/api/queues/catalogue-import).
# Anything else processes targets in the web process after the request, which
# is how local development completes an import without a queue.
# Used by: lib/catalogue-import/queue.ts.
# COURSEMAP_QUEUE_IMPORTS_ENABLED=false
# Set to exactly "true" to publish catalogue syncs to Vercel Queues
# (topic catalogue-sync-v1, consumed by app/api/queues/catalogue-sync).
# Anything else processes the record sync in the web process after the request,
# which is how local development completes a sync without a queue.
# Used by: lib/catalogue-sync/sync-queue.ts.
# COURSEMAP_QUEUE_SYNCS_ENABLED=false

# =============================================================================
# 3. Optional overrides: Room Finder services
Expand Down
2 changes: 0 additions & 2 deletions apps/web/app/admin/courses/imports/error.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions apps/web/app/admin/courses/imports/loading.tsx

This file was deleted.

8 changes: 0 additions & 8 deletions apps/web/app/admin/courses/imports/page.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions apps/web/app/admin/majors/imports/error.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions apps/web/app/admin/majors/imports/loading.tsx

This file was deleted.

8 changes: 0 additions & 8 deletions apps/web/app/admin/majors/imports/page.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions apps/web/app/admin/minors/imports/error.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions apps/web/app/admin/minors/imports/loading.tsx

This file was deleted.

8 changes: 0 additions & 8 deletions apps/web/app/admin/minors/imports/page.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions apps/web/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UsersRound } from "lucide-react";
import { ImportModelCard } from "@/ui/admin/imports/import-model-card";
import { loadImportModelSetting } from "@/lib/admin/settings";
import { loadAdminUserSummary } from "@/lib/admin/users";
import { canManageCourseImports } from "@/lib/auth/viewer";
import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { AppShell } from "@/ui/shell";
import { StatTile } from "@/ui/common/stat-tile";

Expand All @@ -12,7 +12,7 @@ export default async function AdminOverviewPage() {
const [users, importModel, canManageImports] = await Promise.all([
loadAdminUserSummary(),
loadImportModelSetting(),
canManageCourseImports(),
canManageCatalogueSources(),
]);

return (
Expand Down
2 changes: 0 additions & 2 deletions apps/web/app/admin/programmes/imports/error.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions apps/web/app/admin/programmes/imports/loading.tsx

This file was deleted.

8 changes: 0 additions & 8 deletions apps/web/app/admin/programmes/imports/page.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions apps/web/app/admin/specialisations/imports/error.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions apps/web/app/admin/specialisations/imports/loading.tsx

This file was deleted.

10 changes: 0 additions & 10 deletions apps/web/app/admin/specialisations/imports/page.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions apps/web/app/api/admin/catalogue-directory/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { canManageCourseImports } from "@/lib/auth/viewer";
import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { refreshCatalogueDirectory } from "@/lib/catalogue-import/directory";
import { isCatalogueKind } from "@/lib/catalogue/content";

Expand All @@ -20,7 +20,7 @@ function eventResponse(data: unknown, status: number) {

/** Streams directory refresh progress as server-sent events. */
export async function POST(request: Request) {
if (!(await canManageCourseImports())) {
if (!(await canManageCatalogueSources())) {
return eventResponse(
{ type: "error", message: "Import permission is required." },
403,
Expand Down

This file was deleted.

106 changes: 0 additions & 106 deletions apps/web/app/api/admin/catalogue-imports/route.ts

This file was deleted.

73 changes: 73 additions & 0 deletions apps/web/app/api/admin/catalogue-syncs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { after } from "next/server";
import { canManageCatalogueSources } from "@/lib/auth/viewer";
import { isCatalogueKind } from "@/lib/catalogue/content";
import { processCatalogueSyncInline } from "@/lib/catalogue-sync/sync-queue";
import { startCatalogueSync } from "@/lib/catalogue-sync/sync-service";
import { createClient } from "@/lib/supabase/server";

export const runtime = "nodejs";
export const maxDuration = 60;

type StartRequest = { recordId?: unknown; kind?: unknown };

function json(data: unknown, status = 200) {
return Response.json(data, { status });
}

/** Creates one record sync. Inline processing continues after the response. */
export async function POST(request: Request) {
if (!(await canManageCatalogueSources())) {
return json({ error: "Catalogue sync permission is required." }, 403);
}
let payload: StartRequest;
try {
payload = (await request.json()) as StartRequest;
} catch {
return json({ error: "Invalid sync request." }, 400);
}
const recordId = Number(payload.recordId);
if (!Number.isInteger(recordId) || !isCatalogueKind(payload.kind)) {
return json({ error: "A catalogue record is required." }, 400);
}
try {
const result = await startCatalogueSync({
recordId,
trigger: "manual",
kind: payload.kind,
});
if (result.mode === "inline") {
after(() => processCatalogueSyncInline({ syncId: result.syncId }));
}
return json(result);
} catch (error) {
return json(
{
error:
error instanceof Error ? error.message : "The sync could not start.",
},
400,
);
}
}

/** Stops an unfinished record sync. */
export async function DELETE(request: Request) {
if (!(await canManageCatalogueSources())) {
return json({ error: "Catalogue sync permission is required." }, 403);
}
let payload: { syncId?: unknown };
try {
payload = (await request.json()) as typeof payload;
} catch {
return json({ error: "Invalid request." }, 400);
}
if (typeof payload.syncId !== "string") {
return json({ error: "A sync identifier is required." }, 400);
}
const supabase = await createClient();
const { data, error } = await supabase.rpc("cancel_catalogue_sync", {
p_sync_id: payload.syncId,
});
if (error) return json({ error: error.message }, 400);
return json({ cancelled: data });
}
Loading
Loading