diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a912316..766fe85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,6 +137,9 @@ jobs: - name: Typecheck run: npx tsc --noEmit + - name: Web tests + run: npm test + - name: Build run: npm run build diff --git a/apps/web/README.md b/apps/web/README.md index 0f0ebc1..94e9d54 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -57,10 +57,15 @@ From `apps/web`: ```bash npm run lint +npm test npx tsc --noEmit npm run build ``` +`npm test` uses Node's built-in test runner and the existing TypeScript compiler +to render the Policies page without API access and check that retired policy +file operations are absent from the Hosted client. + See [CONTRIBUTING.md](../../CONTRIBUTING.md), [Cold start](../../docs/COLD_START.md), and [SECURITY.md](../../SECURITY.md) for the full contributor workflow and Hosted operating requirements. diff --git a/apps/web/package.json b/apps/web/package.json index 840113f..ffeeec9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,7 +7,8 @@ "dev:turbo": "WATCHPACK_POLLING=true next dev --turbopack --hostname 127.0.0.1 --port 3000", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "node --test tests/policies-page.test.mjs" }, "dependencies": { "@xyflow/react": "^12.12.0", diff --git a/apps/web/src/app/fonts/IBM-Plex-Mono-LICENSE.txt b/apps/web/src/app/fonts/IBM-Plex-Mono-LICENSE.txt new file mode 100644 index 0000000..83eac82 --- /dev/null +++ b/apps/web/src/app/fonts/IBM-Plex-Mono-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/web/src/app/fonts/README.md b/apps/web/src/app/fonts/README.md new file mode 100644 index 0000000..28ae9ec --- /dev/null +++ b/apps/web/src/app/fonts/README.md @@ -0,0 +1,14 @@ +# Bundled UI fonts + +The UI loads Latin Rubik (variable, weights 400–800) and IBM Plex Mono +(weights 400, 500, 600) through `next/font/local`. Builds do not fetch Google +Fonts CSS or font files. + +Unmodified WOFF2 files were extracted from these npm packages, with the archive +SHA-512 checked against the registry's `dist.integrity`: + +- `@fontsource-variable/rubik@5.3.0`: `files/rubik-latin-wght-normal.woff2` +- `@fontsource/ibm-plex-mono@5.3.0`: `files/ibm-plex-mono-latin-{400,500,600}-normal.woff2` + +Both fonts use the SIL Open Font License 1.1. Their copyright notices and +licenses are included beside the font files. Preserve these when updating fonts. diff --git a/apps/web/src/app/fonts/Rubik-LICENSE.txt b/apps/web/src/app/fonts/Rubik-LICENSE.txt new file mode 100644 index 0000000..b25d078 --- /dev/null +++ b/apps/web/src/app/fonts/Rubik-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik) Rubik-Italic[wght].ttf: Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/apps/web/src/app/fonts/ibm-plex-mono-latin-400-normal.woff2 b/apps/web/src/app/fonts/ibm-plex-mono-latin-400-normal.woff2 new file mode 100644 index 0000000..0804aaf Binary files /dev/null and b/apps/web/src/app/fonts/ibm-plex-mono-latin-400-normal.woff2 differ diff --git a/apps/web/src/app/fonts/ibm-plex-mono-latin-500-normal.woff2 b/apps/web/src/app/fonts/ibm-plex-mono-latin-500-normal.woff2 new file mode 100644 index 0000000..090f82f Binary files /dev/null and b/apps/web/src/app/fonts/ibm-plex-mono-latin-500-normal.woff2 differ diff --git a/apps/web/src/app/fonts/ibm-plex-mono-latin-600-normal.woff2 b/apps/web/src/app/fonts/ibm-plex-mono-latin-600-normal.woff2 new file mode 100644 index 0000000..67aeeb0 Binary files /dev/null and b/apps/web/src/app/fonts/ibm-plex-mono-latin-600-normal.woff2 differ diff --git a/apps/web/src/app/fonts/rubik-latin-wght-normal.woff2 b/apps/web/src/app/fonts/rubik-latin-wght-normal.woff2 new file mode 100644 index 0000000..42fc52a Binary files /dev/null and b/apps/web/src/app/fonts/rubik-latin-wght-normal.woff2 differ diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index e2376ee..121917a 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,18 +1,23 @@ import type { Metadata } from "next"; -import { Rubik, IBM_Plex_Mono } from "next/font/google"; +import localFont from "next/font/local"; import { AppShell } from "@/components/AppShell"; import "./globals.css"; -const rubik = Rubik({ +const rubik = localFont({ + src: "./fonts/rubik-latin-wght-normal.woff2", variable: "--font-rubik", - subsets: ["latin"], - weight: ["400", "500", "600", "700", "800"], + weight: "400 800", + display: "swap", }); -const plexMono = IBM_Plex_Mono({ +const plexMono = localFont({ + src: [ + { path: "./fonts/ibm-plex-mono-latin-400-normal.woff2", weight: "400" }, + { path: "./fonts/ibm-plex-mono-latin-500-normal.woff2", weight: "500" }, + { path: "./fonts/ibm-plex-mono-latin-600-normal.woff2", weight: "600" }, + ], variable: "--font-plex-mono", - subsets: ["latin"], - weight: ["400", "500", "600"], + display: "swap", }); export const metadata: Metadata = { diff --git a/apps/web/src/app/policies/page.tsx b/apps/web/src/app/policies/page.tsx index 973352c..63709d5 100644 --- a/apps/web/src/app/policies/page.tsx +++ b/apps/web/src/app/policies/page.tsx @@ -1,116 +1,15 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; -import { - DEFAULT_PROJECT_PATH, - mutinyApi, - type PolicyContent, - type PolicyEntry, - type PolicyRule, -} from "@/lib/api"; -import { Button, Chip, Skeleton } from "@/components/ui"; - -function projectPathFromQuery(): string { - if (typeof window === "undefined") return DEFAULT_PROJECT_PATH; - const raw = new URLSearchParams(window.location.search).get("project_path"); - return raw?.trim() || DEFAULT_PROJECT_PATH; -} - -function formatConstraint( - label: string, - value: Record | undefined -): string | null { - if (!value || Object.keys(value).length === 0) return null; - const parts = Object.entries(value).map(([k, v]) => { - if (v && typeof v === "object" && !Array.isArray(v)) { - const op = Object.entries(v as Record) - .map(([ok, ov]) => `${ok} ${JSON.stringify(ov)}`) - .join(", "); - return `${k} ${op}`; - } - return `${k} = ${JSON.stringify(v)}`; - }); - return `${label}: ${parts.join(" · ")}`; -} +import { CodeBlock } from "@/components/ui/CodeBlock"; export default function PoliciesPage() { - const [projectPath, setProjectPath] = useState(DEFAULT_PROJECT_PATH); - const [entry, setEntry] = useState(null); - const [content, setContent] = useState(null); - const [draft, setDraft] = useState(""); - const [editing, setEditing] = useState(false); - const [error, setError] = useState(null); - const [status, setStatus] = useState(null); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - - const load = useCallback(async (path: string) => { - setLoading(true); - setError(null); - setStatus(null); - try { - const [list, raw] = await Promise.all([ - mutinyApi.policies(path), - mutinyApi.policyContent(path), - ]); - setEntry(list.policies[0] ?? null); - setContent(raw); - setDraft(raw.content); - setEditing(false); - } catch (e) { - setEntry(null); - setContent(null); - setError(e instanceof Error ? e.message : String(e)); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - const path = projectPathFromQuery(); - setProjectPath(path); - void load(path); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - async function onSave() { - setSaving(true); - setError(null); - setStatus(null); - try { - const saved = await mutinyApi.savePolicyContent(draft, projectPath); - setContent(saved); - setDraft(saved.content); - setEntry({ - id: projectPath.split("/").filter(Boolean).pop() || "project", - project_path: saved.project_path, - path: saved.path, - version: saved.version, - target: saved.policy_set.target, - policy_set: saved.policy_set, - }); - setEditing(false); - setStatus(`Saved v${saved.version} → ${saved.path}`); - } catch (e) { - setError(e instanceof Error ? e.message : String(e)); - } finally { - setSaving(false); - } - } - - const rules: PolicyRule[] = entry?.policy_set.rules ?? []; - return ( -
+
-

Project → Policies → Campaign

Policies

- Explicit tool invariants from the project{" "} - policy.yaml. Same file - for Local CLI execution and Hosted observation. + Edit policy.yaml in your local project. Hosted observes the results + of local campaigns.

@@ -118,143 +17,31 @@ export default function PoliciesPage() {
-
- - - {!editing ? ( - - ) : ( - <> - - - - )} -
- - {error &&

{error}

} - {status &&

{status}

} - {loading && } - - {!loading && !error && !entry && ( -

- No policy set found for{" "} - {projectPath}. Check the path or - run mutiny init. +

+

+ Policies are managed locally +

+

+ Hosted does not read or edit policy files on your machine. There is + no policy editor here. Initialize your project with mutiny init, + edit policy.yaml, then run a campaign from that project:

- )} - - {!loading && entry && ( -
-
-

Policy set

-

- {entry.id} -

-

- Target{" "} - - {entry.policy_set.target} - - {" · "}v{entry.policy_set.version} - {entry.path && ( - <> - {" · "} - {entry.path} - - )} -

-
- - {editing ? ( -
-
- policy.yaml - YAML · editable -
-