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
Original file line number Diff line number Diff line change
Expand Up @@ -3029,28 +3029,10 @@ describe.skipIf(process.env.OPENMAPX_RUN_DATABASE_TESTS !== "1")(
expect(JSON.stringify(unchanged!.result)).toBe(failed);
return;
}
let retainedBytes: Buffer | undefined;
const unlinkBoundary = async () => {
const marker = join(host.root, "coordinator-retirement-unlinked"),
until = Date.now() + 150000;
while (!existsSync(marker)) {
const [observed] = await fixture.db
.select()
.from(schema.adminJob)
.where(eq(schema.adminJob.id, id)),
candidate = (
observed?.result as
| { _configurationCandidate?: { sealedOperationKey?: string } }
| undefined
)?._configurationCandidate;
if (candidate?.sealedOperationKey) {
const path = join(
host.queue,
".candidates",
`${candidate.sealedOperationKey}.json`,
);
if (existsSync(path)) retainedBytes = readFileSync(path);
}
if (Date.now() > until) throw Error("Actual retirement unlink boundary missing");
await delay(10);
}
Expand All @@ -3068,7 +3050,11 @@ describe.skipIf(process.env.OPENMAPX_RUN_DATABASE_TESTS !== "1")(
prior = before!.result as Record<string, unknown>,
candidate = prior._configurationCandidate as Record<string, unknown>,
path = join(host.queue, ".candidates", `${candidate.sealedOperationKey}.json`),
bytes = point === "candidate unlink" ? retainedBytes! : readFileSync(path),
// The native unlink hook publishes these exact bytes only after unlink succeeds.
bytes =
point === "candidate unlink"
? readFileSync(join(host.root, "coordinator-retirement-unlinked"))
: readFileSync(path),
decision = JSON.stringify(prior._configurationMaintenanceDecision),
inspected = inspectPreparedConfigurationRecord(bytes, host.token).record,
originalNormal =
Expand All @@ -3085,7 +3071,7 @@ describe.skipIf(process.env.OPENMAPX_RUN_DATABASE_TESTS !== "1")(
point === "candidate unlink" ? "settled" : !pullBoundary ? "saved" : "prepared",
);
if (point === "candidate unlink") {
expect(bytes).toBeDefined();
expect(inspected.operationKey).toBe(candidate.sealedOperationKey);
expect(existsSync(path)).toBe(false);
expect(candidate.retirement).toMatchObject({ version: 1, integrationIds: [] });
expect(posts.get("configurationMaintenance.settle")).toBe(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* reply delivery and kills this native process; no DB/service/authority mocks. */

import { randomUUID } from "node:crypto";
import { constants, writeFileSync } from "node:fs";
import { constants, readFileSync, renameSync, writeFileSync } from "node:fs";
import filesystem from "node:fs/promises";
import { syncBuiltinESMExports } from "node:module";
import { join } from "node:path";
Expand Down Expand Up @@ -36,12 +36,12 @@ if (input.filesystemFault === "after-retirement-unlink") {
const unlink = filesystem.unlink,
directory = join(process.env.OPS_TRUSTED_CONFIG_DIR!, ".candidates") + "/";
filesystem.unlink = async (path) => {
const candidate = String(path).startsWith(directory) ? readFileSync(path) : undefined;
await unlink(path);
if (String(path).startsWith(directory)) {
writeFileSync(
join(process.env.OPENMAPX_ROOT_DIR!, "coordinator-retirement-unlinked"),
"unlinked",
);
if (candidate) {
const marker = join(process.env.OPENMAPX_ROOT_DIR!, "coordinator-retirement-unlinked");
writeFileSync(`${marker}.partial`, candidate, { mode: 0o600, flag: "wx" });
renameSync(`${marker}.partial`, marker);
await new Promise<void>(() => {});
}
};
Expand Down
30 changes: 30 additions & 0 deletions apps/web/src/app/admin/(full-admin)/aliases.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect, it, vi } from "vitest";

const mocks = vi.hoisted(() => ({ redirect: vi.fn() }));
vi.mock("next/navigation", () => ({ redirect: mocks.redirect }));
it.each([
[() => import("./services/page"), "/admin/system/services"],
[() => import("./services/compose/page"), "/admin/system/deployment"],
[() => import("./services/backups/page"), "/admin/system/backups"],
[() => import("./services/data/page"), "/admin/data"],
[() => import("./transit/page"), "/admin/data/transit"],
[() => import("./coverage/page"), "/admin/data/coverage"],
[() => import("./cache/page"), "/admin/system/cache"],
] as const)(
"preserves supported navigation state through the legacy route",
async (load, target) => {
const { default: Page } = await load();
await Page({
searchParams: Promise.resolve({ tab: "credentials", page: "2", token: "private" }),
});
expect(mocks.redirect).toHaveBeenLastCalledWith(`${target}?tab=credentials&page=2`);
},
);
it("encodes service IDs while redirecting details", async () => {
const { default: Page } = await import("./services/[id]/page");
await Page({
params: Promise.resolve({ id: "service id" }),
searchParams: Promise.resolve({ tab: "logs" }),
});
expect(mocks.redirect).toHaveBeenLastCalledWith("/admin/system/services/service%20id?tab=logs");
});
15 changes: 8 additions & 7 deletions apps/web/src/app/admin/(full-admin)/cache/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { CacheManager } from "@/components/admin/cache/CacheManager";

export const metadata: Metadata = { title: "Cache — Admin — OpenMapX" };

export default function AdminCachePage() {
return <CacheManager />;
import { redirect } from "next/navigation";
import { legacyAdminHref } from "@/lib/admin/adminNavigation";
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
redirect(legacyAdminHref("/admin/cache", await searchParams));
}
15 changes: 8 additions & 7 deletions apps/web/src/app/admin/(full-admin)/coverage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { CoveragePage } from "@/components/admin/coverage/CoveragePage";

export const metadata: Metadata = { title: "Coverage & freshness — Admin — OpenMapX" };

export default function AdminCoveragePage() {
return <CoveragePage />;
import { redirect } from "next/navigation";
import { legacyAdminHref } from "@/lib/admin/adminNavigation";
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
redirect(legacyAdminHref("/admin/coverage", await searchParams));
}
10 changes: 10 additions & 0 deletions apps/web/src/app/admin/(full-admin)/data/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { DataWorkflowsPage } from "@/components/admin/services/DataWorkflowsPage";
import { HostedDataTopics } from "@/components/admin/shared/HostedDataTopics";
import { getSelfHosted } from "@/lib/admin/serverDeployment";
export const metadata: Metadata = { title: "Data workflows — Admin — OpenMapX" };
export default async function Page() {
const selfHosted = await getSelfHosted((await cookies()).toString());
return selfHosted === false ? <HostedDataTopics /> : <DataWorkflowsPage />;
}
6 changes: 6 additions & 0 deletions apps/web/src/app/admin/(full-admin)/data/transit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Metadata } from "next";
import { TransitPipelinePage } from "@/components/admin/transit/TransitPipelinePage";
export const metadata: Metadata = { title: "Transit — Admin — OpenMapX" };
export default function Page() {
return <TransitPipelinePage />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,11 @@ it("groups setup fields, preserves environment locks, and links failed readiness
screen.getByRole("heading", { name: /privacySetup.steps.retention.title/ }),
).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: /privacySetup.steps.notifications.title/ }));
const fromAddress = await screen.findByLabelText("privacySetup.settings.smtpFromAddress.label");
expect(fromAddress).toBeDisabled();
expect(screen.queryByLabelText("privacySetup.settings.smtpFromAddress.label")).toBeNull();
expect(screen.getByRole("link", { name: "privacySetup.openEmailSettings" })).toHaveAttribute(
"href",
"/admin/settings/email",
);

expect(
await screen.findByRole("link", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useQuery } from "@tanstack/react-query";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useMemo } from "react";
import { PrivacyApprovalForm } from "@/components/admin/privacy/PrivacyApprovalForm";
Expand All @@ -31,6 +32,7 @@ const STEP_IDS = ["operator", "notifications", "retention", "reviews"] as const;
export function PrivacySetupPage() {
const t = useTranslations("privacySetup");
const env = useEnv();
const field = useSearchParams()?.get("field");
const sections = useMemo<SettingsEditorSection[]>(
() => [
{
Expand All @@ -41,13 +43,6 @@ export function PrivacySetupPage() {
description: t("steps.operator.description"),
defaultExpanded: true,
},
{
id: "notifications",
groupId: "email",
label: t("steps.notifications.title"),
description: t("steps.notifications.description"),
includeTestEmail: true,
},
{
id: "retention",
groupId: "legal",
Expand Down Expand Up @@ -131,15 +126,28 @@ export function PrivacySetupPage() {

<Paper component="nav" aria-label={t("stepNavigation")} variant="outlined" sx={{ p: 1 }}>
<Stack direction="row" useFlexGap sx={{ gap: 0.5, flexWrap: "wrap" }}>
{STEP_IDS.map((id, index) => (
<Button key={id} component={Link} href={`#${id}`} size="small">
{t("stepLink", { number: index + 1, title: t(`steps.${id}.title`) })}
{STEP_IDS.map((id) => (
<Button key={id} component="a" href={`#${id}`} size="small">
{t(`steps.${id}.title`)}
</Button>
))}
</Stack>
</Paper>

<Paper id="notifications" variant="outlined" sx={{ p: 2 }}>
<Typography component="h2" variant="subtitle1">
{t("steps.notifications.title")}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("notificationsOwner")}
</Typography>
<Button component={Link} href="/admin/settings/email">
{t("openEmailSettings")}
</Button>
</Paper>

<SystemSettings
focusKey={field && /^[a-zA-Z0-9_-]{1,128}$/.test(field) ? field : undefined}
sections={sections}
showHeader={false}
showTransfer={false}
Expand Down
22 changes: 9 additions & 13 deletions apps/web/src/app/admin/(full-admin)/services/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import type { Metadata } from "next";
import { ServiceDetail } from "@/components/admin/services/ServiceDetail";

interface Props {
import { redirect } from "next/navigation";
import { legacyAdminHref } from "@/lib/admin/adminNavigation";
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
return { title: `${id} — Services — Admin — OpenMapX` };
}

export default async function AdminServiceDetailPage({ params }: Props) {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const { id } = await params;
return <ServiceDetail id={id} />;
redirect(legacyAdminHref(`/admin/services/${encodeURIComponent(id)}`, await searchParams));
}
15 changes: 8 additions & 7 deletions apps/web/src/app/admin/(full-admin)/services/backups/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { BackupsPage } from "@/components/admin/services/BackupsPage";

export const metadata: Metadata = { title: "Backups — Admin — OpenMapX" };

export default function AdminBackupsPage() {
return <BackupsPage />;
import { redirect } from "next/navigation";
import { legacyAdminHref } from "@/lib/admin/adminNavigation";
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
redirect(legacyAdminHref("/admin/services/backups", await searchParams));
}
15 changes: 8 additions & 7 deletions apps/web/src/app/admin/(full-admin)/services/compose/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { ComposePreview } from "@/components/admin/services/ComposePreview";

export const metadata: Metadata = { title: "Compose Preview — Admin — OpenMapX" };

export default function ComposePreviewPage() {
return <ComposePreview />;
import { redirect } from "next/navigation";
import { legacyAdminHref } from "@/lib/admin/adminNavigation";
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
redirect(legacyAdminHref("/admin/services/compose", await searchParams));
}
15 changes: 8 additions & 7 deletions apps/web/src/app/admin/(full-admin)/services/data/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { DataWorkflowsPage } from "@/components/admin/services/DataWorkflowsPage";

export const metadata: Metadata = { title: "Data Workflows — Admin — OpenMapX" };

export default function AdminDataWorkflowsPage() {
return <DataWorkflowsPage />;
import { redirect } from "next/navigation";
import { legacyAdminHref } from "@/lib/admin/adminNavigation";
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
redirect(legacyAdminHref("/admin/services/data", await searchParams));
}
15 changes: 8 additions & 7 deletions apps/web/src/app/admin/(full-admin)/services/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
import { ServiceCatalog } from "@/components/admin/services/ServiceCatalog";

export const metadata: Metadata = { title: "Services — Admin — OpenMapX" };

export default function AdminServicesPage() {
return <ServiceCatalog />;
import { redirect } from "next/navigation";
import { legacyAdminHref } from "@/lib/admin/adminNavigation";
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
redirect(legacyAdminHref("/admin/services", await searchParams));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { notFound } from "next/navigation";
import { SettingsWorkspace } from "@/components/admin/settings/SettingsWorkspace";
import { SETTINGS_TOPICS } from "@/lib/admin/adminNavigation";
export default async function Page({ params }: { params: Promise<{ topic: string }> }) {
const { topic } = await params;
const valid = SETTINGS_TOPICS.find((value) => value === topic);
if (!valid) notFound();
return <SettingsWorkspace topic={valid} />;
}
17 changes: 3 additions & 14 deletions apps/web/src/app/admin/(full-admin)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import Stack from "@mui/material/Stack";
import type { Metadata } from "next";
import { PlatformConfiguration } from "@/components/admin/settings/PlatformConfiguration";
import { SystemSettings } from "@/components/admin/settings/SystemSettings";

export const metadata: Metadata = { title: "Settings — Admin — OpenMapX" };

export default function AdminSettingsPage() {
return (
<Stack sx={{ gap: 3 }}>
<PlatformConfiguration />
<SystemSettings />
</Stack>
);
import { LegacySettingsRedirect } from "@/components/admin/settings/SettingsWorkspace";
export default function Page() {
return <LegacySettingsRedirect />;
}
6 changes: 6 additions & 0 deletions apps/web/src/app/admin/(full-admin)/system/backups/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Metadata } from "next";
import { BackupsPage } from "@/components/admin/services/BackupsPage";
export const metadata: Metadata = { title: "Backups — Admin — OpenMapX" };
export default function Page() {
return <BackupsPage />;
}
6 changes: 6 additions & 0 deletions apps/web/src/app/admin/(full-admin)/system/cache/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Metadata } from "next";
import { CacheManager } from "@/components/admin/cache/CacheManager";
export const metadata: Metadata = { title: "Cache — Admin — OpenMapX" };
export default function Page() {
return <CacheManager />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SettingsTransfer } from "@/components/admin/settings/SystemSettings";
export default function Page() {
return <SettingsTransfer />;
}
11 changes: 11 additions & 0 deletions apps/web/src/app/admin/(full-admin)/system/deployment/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Stack from "@mui/material/Stack";
import { ComposePreview } from "@/components/admin/services/ComposePreview";
import { PlatformConfiguration } from "@/components/admin/settings/PlatformConfiguration";
export default function Page() {
return (
<Stack sx={{ gap: 2 }}>
<PlatformConfiguration />
<ComposePreview />
</Stack>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SystemMaintenance } from "@/components/admin/system/SystemMaintenance";
export default function Page() {
return <SystemMaintenance section="diagnostics" />;
}
Loading
Loading