diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61a731c..625d56f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,20 +10,20 @@ env: CARGO_TERM_COLOR: always jobs: - build: - name: Build & test + rust: + name: Rust build & test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable - name: Install Bun (for plugin UIs) - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Cache cargo - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - name: cargo fmt run: cargo fmt --all -- --check @@ -33,3 +33,25 @@ jobs: - name: cargo test run: cargo test --workspace + + typescript: + name: TypeScript build & test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Install Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.3.14 + + - name: Install locked dependencies + working-directory: deployment-pulse-plugin + run: bun install --frozen-lockfile + + - name: Test and build Deployment Pulse + working-directory: deployment-pulse-plugin + run: | + bun run test + bun run build + bunx tsc --noEmit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0f37a09..5215ee9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read jobs: build: @@ -23,27 +23,37 @@ jobs: - target: x86_64-unknown-linux-gnu os: ubuntu-latest suffix: x86_64-linux + bun_target: bun-linux-x64 - target: aarch64-unknown-linux-gnu os: ubuntu-latest suffix: aarch64-linux linker: aarch64-linux-gnu-gcc + bun_target: bun-linux-arm64 - target: x86_64-apple-darwin os: macos-latest suffix: x86_64-darwin + bun_target: bun-darwin-x64 - target: aarch64-apple-darwin os: macos-latest suffix: aarch64-darwin + bun_target: bun-darwin-arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + + - name: Verify release commit is on main + shell: bash + run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: targets: ${{ matrix.target }} - name: Install Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install cross-compilation tools (Linux aarch64) if: matrix.target == 'aarch64-unknown-linux-gnu' @@ -52,7 +62,7 @@ jobs: sudo apt-get install -y gcc-aarch64-linux-gnu - name: Cache cargo - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 with: key: ${{ matrix.target }} @@ -61,6 +71,16 @@ jobs: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc run: cargo build --release --workspace --target ${{ matrix.target }} + - name: Install Deployment Pulse dependencies + working-directory: deployment-pulse-plugin + run: bun install --frozen-lockfile + + - name: Build Deployment Pulse + working-directory: deployment-pulse-plugin + run: | + bun run embed + bun build src/index.ts --compile --target=${{ matrix.bun_target }} --outfile=dist/temps-deployment-pulse-plugin + - name: Stage release artifacts shell: bash run: | @@ -75,10 +95,13 @@ jobs: dst="dist/${binary}-${{ matrix.suffix }}" cp "${src}" "${dst}" done + cp \ + deployment-pulse-plugin/dist/temps-deployment-pulse-plugin \ + "dist/temps-deployment-pulse-plugin-${{ matrix.suffix }}" ls -la dist - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: plugins-${{ matrix.suffix }} path: dist/* @@ -88,8 +111,11 @@ jobs: needs: build runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') + environment: release + permissions: + contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: path: dist merge-multiple: true @@ -98,7 +124,7 @@ jobs: run: ls -la dist - name: Create release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: dist/* generate_release_notes: true diff --git a/README.md b/README.md index 6376d3b..250da00 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Official external plugins for [Temps](https://temps.sh) — drop-in binaries that extend the platform without forking or rebuilding it. -All plugins in this repo are written in Rust using [`temps-plugin-sdk`](https://github.com/gotempsh/temps/tree/main/crates/temps-plugin-sdk) and communicate with the Temps server over stdin/stdout. +Plugins in this repo use either the Rust [`temps-plugin-sdk`](https://github.com/gotempsh/temps/tree/main/crates/temps-plugin-sdk) or the TypeScript [`@temps-sdk/plugin`](https://github.com/gotempsh/temps/tree/main/sdks/node/packages/plugin-sdk). Both communicate with the Temps server over stdin/stdout and compile to standalone binaries. ## Plugins @@ -12,6 +12,7 @@ All plugins in this repo are written in Rust using [`temps-plugin-sdk`](https:// | [`lighthouse-plugin`](./lighthouse-plugin) | Runs Google Lighthouse audits after every deployment and tracks Core Web Vitals over time. | | [`indexnow-plugin`](./indexnow-plugin) | Automatically submits deployed URLs to IndexNow-supporting search engines (Bing, Yandex, Seznam). | | [`google-indexing-plugin`](./google-indexing-plugin) | Notifies the Google Indexing API when pages are published or removed. | +| [`deployment-pulse-plugin`](./deployment-pulse-plugin) | TypeScript reference plugin with a searchable, cross-project deployment health dashboard. | ## Install a prebuilt binary @@ -47,9 +48,20 @@ chmod +x ~/.temps/plugins/temps-lighthouse-plugin Open **Settings → Plugins** in the Temps dashboard and click **Reload Plugins**. +For the TypeScript example, use Bun: + +```bash +cd deployment-pulse-plugin +bun install --frozen-lockfile +bun run test +bun run build +cp dist/temps-deployment-pulse-plugin ~/.temps/plugins/ +chmod +x ~/.temps/plugins/temps-deployment-pulse-plugin +``` + ## Plugin structure -Each plugin is a standalone Cargo crate with the same layout: +Rust plugins are standalone Cargo crates: ``` lighthouse-plugin/ @@ -61,13 +73,31 @@ lighthouse-plugin/ The `build.rs` runs `bun install && bun run build` in `web/` when the `web/` directory exists, then the Rust code embeds the built assets with `include_dir!`. +TypeScript plugins follow the same single-binary model: + +``` +deployment-pulse-plugin/ +├── package.json # SDK dependency and Bun scripts +├── scripts/ # embeds the compiled UI +├── src/ # plugin process and tests +└── web/ # Vite + React UI +``` + +`bun run build` builds the UI, embeds it into TypeScript, and compiles the +plugin into one executable. Temps does not need Bun or Node.js at runtime. + ## Write your own plugin -The easiest starting point is to copy `example-plugin`, rename it, and edit from there. See the [plugin system docs](https://temps.sh/docs/plugins) for the full SDK reference, protocol details, and service registration patterns. +For Rust, copy `example-plugin`. For TypeScript, copy +`deployment-pulse-plugin`. See the [plugin system docs](https://temps.sh/docs/plugins) +for the full SDK reference, protocol details, and service registration patterns. ## Versioning -This repo pins to a specific Temps SDK version via `temps-plugin-sdk = { git = "...", tag = "vX.Y.Z" }` in the workspace `Cargo.toml`. Bump the tag in a single PR when you want to pick up new SDK features — every plugin in the workspace upgrades together. +Rust plugins pin `temps-plugin-sdk = { git = "...", tag = "vX.Y.Z" }`. +The TypeScript SDK and individual plugins use their own semantic versions +because they can release independently from Temps. Compatibility is defined by +the external-plugin protocol version. ## License diff --git a/deployment-pulse-plugin/.gitignore b/deployment-pulse-plugin/.gitignore new file mode 100644 index 0000000..77572e7 --- /dev/null +++ b/deployment-pulse-plugin/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +web/node_modules/ +web/dist/ +src/_embedded_ui.ts +*.tgz diff --git a/deployment-pulse-plugin/README.md b/deployment-pulse-plugin/README.md new file mode 100644 index 0000000..227142a --- /dev/null +++ b/deployment-pulse-plugin/README.md @@ -0,0 +1,47 @@ +# Deployment Pulse TypeScript Plugin + +A useful, read-only Temps plugin built with `@temps-sdk/plugin`. It gives an +operator one compact view of deployment health across every project: + +- latest deployment state, branch, commit, and age; +- projects needing attention sorted first; +- recent deployment success rate per project; +- search and health filters; +- automatic refresh every 30 seconds; and +- partial results when one project's history cannot be read. + +The Bun build embeds the React UI and SDK into a single executable. The Temps +host does not need Bun or Node.js installed. + +## Build and test + +Install the exact SDK version recorded in `bun.lock`, then build and test: + +```bash +bun install --frozen-lockfile +bun run test +bun run build +``` + +The executable is written to `dist/temps-deployment-pulse-plugin`. + +## Install locally + +```bash +mkdir -p ~/.temps/plugins +cp dist/temps-deployment-pulse-plugin ~/.temps/plugins/ +chmod +x ~/.temps/plugins/temps-deployment-pulse-plugin +``` + +Open **Settings → Plugins** and select **Reload Plugins**, or restart Temps. +The plugin contributes one **Deployment Pulse** navigation item and mounts its +read-only API at `/api/x/deployment-pulse/overview`. + +## Permissions and data + +Deployment Pulse declares no raw API capability, database access, or host-data +access. It reads only the caller-scoped `list_projects` and `list_deployments` +methods exposed by the signed protocol-v2 SDK channel. + +Project links use the public `/projects/` route. Internal project +IDs are never exposed in dashboard URLs. diff --git a/deployment-pulse-plugin/bun.lock b/deployment-pulse-plugin/bun.lock new file mode 100644 index 0000000..6ca1965 --- /dev/null +++ b/deployment-pulse-plugin/bun.lock @@ -0,0 +1,30 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "typescript-deployment-pulse-plugin", + "dependencies": { + "@temps-sdk/plugin": "0.1.0-beta.1", + }, + "devDependencies": { + "@types/bun": "^1.0.0", + "@types/node": "^22.0.0", + "typescript": "^5.5.4", + }, + }, + }, + "packages": { + "@temps-sdk/plugin": ["@temps-sdk/plugin@0.1.0-beta.1", "", {}, "sha512-mvadvLOj7UNtXy9+p+eJv+c+p2cbOZh62I5NnKwl+aUkCuKk7Np0PZEKwdftiziO2+4SY2SbMG6A7Q7a56QiMw=="], + + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/deployment-pulse-plugin/package.json b/deployment-pulse-plugin/package.json new file mode 100644 index 0000000..4b682ee --- /dev/null +++ b/deployment-pulse-plugin/package.json @@ -0,0 +1,21 @@ +{ + "name": "typescript-deployment-pulse-plugin", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run src/index.ts", + "embed": "bun run scripts/embed-assets.ts", + "compile": "bun run embed && bun build src/index.ts --compile --outfile dist/temps-deployment-pulse-plugin", + "build": "bun run compile", + "test": "bun test src web/src" + }, + "dependencies": { + "@temps-sdk/plugin": "0.1.0-beta.1" + }, + "devDependencies": { + "@types/bun": "^1.0.0", + "@types/node": "^22.0.0", + "typescript": "^5.5.4" + } +} diff --git a/deployment-pulse-plugin/scripts/embed-assets.ts b/deployment-pulse-plugin/scripts/embed-assets.ts new file mode 100644 index 0000000..7513a34 --- /dev/null +++ b/deployment-pulse-plugin/scripts/embed-assets.ts @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import { execSync } from "node:child_process"; +import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; +import { extname, join, relative } from "node:path"; + +const webDir = join(import.meta.dirname, "..", "web"); +const distDir = join(webDir, "dist"); +const outFile = join(import.meta.dirname, "..", "src", "_embedded_ui.ts"); + +try { + statSync(join(webDir, "node_modules")); +} catch { + console.log("Installing web dependencies..."); + execSync("bun install", { cwd: webDir, stdio: "inherit" }); +} + +console.log("Building Deployment Pulse UI..."); +execSync("bun run build", { cwd: webDir, stdio: "inherit" }); + +function walk(directory: string): string[] { + return readdirSync(directory).flatMap((entry) => { + const path = join(directory, entry); + return statSync(path).isDirectory() ? walk(path) : [path]; + }); +} + +const mimeTypes: Record = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".svg": "image/svg+xml", + ".woff2": "font/woff2", +}; + +let code = `// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +// AUTO-GENERATED by scripts/embed-assets.ts -- do not edit. +import type { EmbeddedFile } from "@temps-sdk/plugin"; + +const assets = new Map(); + +`; + +for (const file of walk(distDir)) { + const path = relative(distDir, file).replace(/\\/g, "/"); + const content = readFileSync(file).toString("base64"); + code += `assets.set(${JSON.stringify(path)}, { content: Buffer.from(${JSON.stringify(content)}, "base64"), contentType: ${JSON.stringify(mimeTypes[extname(file)] ?? "application/octet-stream")}, immutable: ${path !== "index.html"} });\n`; +} + +code += "\nexport { assets as embeddedAssets };\n"; +mkdirSync(join(import.meta.dirname, "..", "src"), { recursive: true }); +writeFileSync(outFile, code); +console.log(`Embedded ${walk(distDir).length} files`); diff --git a/deployment-pulse-plugin/src/index.ts b/deployment-pulse-plugin/src/index.ts new file mode 100644 index 0000000..af50a3c --- /dev/null +++ b/deployment-pulse-plugin/src/index.ts @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import { + createManifest, + runPlugin, + type PluginContext, + type PluginEvent, + type ProjectInfo, + type RequestHandler, + type TempsPlugin, +} from "@temps-sdk/plugin"; +import { embeddedAssets } from "./_embedded_ui.js"; +import { buildOverview, summarizeProject, type ProjectPulse } from "./overview.js"; + +const MAX_CONCURRENT_PROJECT_QUERIES = 6; + +async function loadProjectPulses( + ctx: PluginContext, + projects: ProjectInfo[], +): Promise { + const results: ProjectPulse[] = []; + + for (let offset = 0; offset < projects.length; offset += MAX_CONCURRENT_PROJECT_QUERIES) { + const batch = projects.slice(offset, offset + MAX_CONCURRENT_PROJECT_QUERIES); + const pulses = await Promise.all( + batch.map(async (project): Promise => { + try { + const deployments = await ctx.temps.listDeployments(project.id, { limit: 10 }); + return summarizeProject(project, deployments); + } catch (error) { + console.error( + JSON.stringify({ + level: "warn", + message: "Could not load deployment history", + project_id: project.id, + reason: error instanceof Error ? error.message : String(error), + }), + ); + return { + ...summarizeProject(project, []), + health: "unknown", + error: "Deployment history is temporarily unavailable", + }; + } + }), + ); + results.push(...pulses); + } + + return results; +} + +function json(res: Parameters[1], status: number, body: unknown): void { + res.writeHead(status, { + "Content-Type": "application/json; charset=utf-8", + "Cache-Control": "no-store", + }); + res.end(JSON.stringify(body)); +} + +const plugin: TempsPlugin = { + manifest() { + return createManifest("deployment-pulse", "0.1.0") + .displayName("Deployment Pulse") + .description("See deployment health across every project at a glance") + .requiresDb(false) + .addNav("Deployment Pulse", "activity", "/") + .event("deployment.succeeded") + .event("deployment.failed") + .build(); + }, + + embeddedUiAssets() { + return embeddedAssets; + }, + + handler(ctx: PluginContext): RequestHandler { + return async (req, res) => { + const url = new URL(req.url ?? "/", "http://localhost"); + if (req.method !== "GET" || url.pathname !== "/overview") { + json(res, 404, { error: "Not found" }); + return; + } + + try { + const projects = await ctx.temps.listProjects(); + const pulses = await loadProjectPulses(ctx, projects); + json(res, 200, buildOverview(pulses)); + } catch (error) { + console.error( + JSON.stringify({ + level: "error", + message: "Could not build deployment overview", + reason: error instanceof Error ? error.message : String(error), + }), + ); + json(res, 502, { + error: "Deployment data is temporarily unavailable", + retryable: true, + }); + } + }; + }, + + onStart() { + console.error(JSON.stringify({ level: "info", message: "Deployment Pulse started" })); + }, + + onEvent(_ctx: PluginContext, event: PluginEvent) { + console.error( + JSON.stringify({ + level: "info", + message: "Deployment state changed", + event_type: event.event_type, + project_id: event.project_id, + }), + ); + }, + + onShutdown() { + console.error(JSON.stringify({ level: "info", message: "Deployment Pulse stopped" })); + }, +}; + +await runPlugin(plugin); diff --git a/deployment-pulse-plugin/src/overview.test.ts b/deployment-pulse-plugin/src/overview.test.ts new file mode 100644 index 0000000..e3cc8a0 --- /dev/null +++ b/deployment-pulse-plugin/src/overview.test.ts @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import { describe, expect, it } from "bun:test"; +import type { DeploymentInfo, ProjectInfo } from "@temps-sdk/plugin"; +import { buildOverview, classifyDeployment, summarizeProject } from "./overview.js"; + +const project = { + id: 7, + name: "Registry", + slug: "registry", + preset: "nextjs", + source_type: "git", +} as ProjectInfo; + +function deployment(id: number, state: string, createdAt: string): DeploymentInfo { + return { + id, + project_id: project.id, + environment_id: 1, + state, + created_at: createdAt, + }; +} + +describe("classifyDeployment", () => { + it("normalizes the states emitted by current and older Temps hosts", () => { + expect(classifyDeployment("deployed")).toBe("healthy"); + expect(classifyDeployment("COMPLETED")).toBe("healthy"); + expect(classifyDeployment("running")).toBe("active"); + expect(classifyDeployment("cancelled")).toBe("failed"); + expect(classifyDeployment("stopped")).toBe("paused"); + expect(classifyDeployment("future-state")).toBe("unknown"); + expect(classifyDeployment()).toBe("never"); + }); +}); + +describe("summarizeProject", () => { + it("uses the newest deployment and calculates the completed success rate", () => { + const pulse = summarizeProject( + project, + [ + deployment(1, "failed", "2026-08-31T08:00:00.000Z"), + deployment(3, "running", "2026-09-01T10:00:00.000Z"), + deployment(2, "deployed", "2026-09-01T09:00:00.000Z"), + ], + new Date("2026-09-01T12:00:00.000Z"), + ); + + expect(pulse.health).toBe("active"); + expect(pulse.latestDeployment?.id).toBe(3); + expect(pulse.recentFailures).toBe(1); + expect(pulse.successRate).toBe(50); + expect(pulse.deployments24h).toBe(2); + expect(pulse.failures24h).toBe(0); + }); + + it("reports a project with no deployments without inventing a success rate", () => { + const pulse = summarizeProject(project, []); + expect(pulse.health).toBe("never"); + expect(pulse.latestDeployment).toBeNull(); + expect(pulse.successRate).toBeNull(); + }); +}); + +describe("buildOverview", () => { + it("counts current health and puts projects needing attention first", () => { + const now = new Date("2026-09-01T12:00:00.000Z"); + const failed = summarizeProject( + { ...project, id: 8, name: "API" }, + [deployment(4, "failed", "2026-09-01T11:00:00.000Z")], + now, + ); + const healthy = summarizeProject( + project, + [deployment(5, "deployed", "2026-09-01T10:00:00.000Z")], + now, + ); + const overview = buildOverview( + [healthy, failed], + now, + ); + + expect(overview.summary).toMatchObject({ + total: 2, + healthy: 1, + failed: 1, + deployments24h: 2, + failures24h: 1, + }); + expect(overview.projects.map((entry) => entry.name)).toEqual(["API", "Registry"]); + }); +}); diff --git a/deployment-pulse-plugin/src/overview.ts b/deployment-pulse-plugin/src/overview.ts new file mode 100644 index 0000000..022138c --- /dev/null +++ b/deployment-pulse-plugin/src/overview.ts @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import type { DeploymentInfo, ProjectInfo } from "@temps-sdk/plugin"; + +export type HealthState = + | "healthy" + | "failed" + | "active" + | "paused" + | "never" + | "unknown"; + +export interface ProjectPulse { + id: number; + name: string; + slug: string; + preset: string; + sourceType: string; + health: HealthState; + latestDeployment: DeploymentInfo | null; + recentDeployments: number; + recentFailures: number; + deployments24h: number; + failures24h: number; + successRate: number | null; + error?: string; +} + +export interface DeploymentOverview { + generatedAt: string; + summary: { + total: number; + healthy: number; + failed: number; + active: number; + paused: number; + never: number; + unknown: number; + deployments24h: number; + failures24h: number; + }; + projects: ProjectPulse[]; +} + +const SUCCESS_STATES = new Set(["completed", "deployed", "ready", "succeeded"]); +const FAILED_STATES = new Set(["failed", "cancelled", "canceled", "error"]); +const ACTIVE_STATES = new Set([ + "pending", + "queued", + "building", + "built", + "deploying", + "running", +]); +const PAUSED_STATES = new Set(["paused", "stopped"]); + +export function classifyDeployment(state?: string): HealthState { + if (!state) return "never"; + const normalized = state.toLowerCase(); + if (SUCCESS_STATES.has(normalized)) return "healthy"; + if (FAILED_STATES.has(normalized)) return "failed"; + if (ACTIVE_STATES.has(normalized)) return "active"; + if (PAUSED_STATES.has(normalized)) return "paused"; + return "unknown"; +} + +export function summarizeProject( + project: ProjectInfo, + deployments: DeploymentInfo[], + now = new Date(), +): ProjectPulse { + const ordered = [...deployments].sort( + (a, b) => Date.parse(b.created_at) - Date.parse(a.created_at), + ); + const latestDeployment = ordered[0] ?? null; + const finished = ordered.filter((deployment) => { + const health = classifyDeployment(deployment.state); + return health === "healthy" || health === "failed"; + }); + const successes = finished.filter( + (deployment) => classifyDeployment(deployment.state) === "healthy", + ).length; + const recentFailures = finished.length - successes; + const cutoff = now.getTime() - 24 * 60 * 60 * 1000; + const deployments24h = ordered.filter( + (deployment) => Date.parse(deployment.created_at) >= cutoff, + ); + + return { + id: project.id, + name: project.name, + slug: project.slug, + preset: project.preset || "custom", + sourceType: project.source_type, + health: classifyDeployment(latestDeployment?.state), + latestDeployment, + recentDeployments: ordered.length, + recentFailures, + deployments24h: deployments24h.length, + failures24h: deployments24h.filter( + (deployment) => classifyDeployment(deployment.state) === "failed", + ).length, + successRate: + finished.length === 0 ? null : Math.round((successes / finished.length) * 100), + }; +} + +export function buildOverview( + projects: ProjectPulse[], + now = new Date(), +): DeploymentOverview { + const summary = { + total: projects.length, + healthy: 0, + failed: 0, + active: 0, + paused: 0, + never: 0, + unknown: 0, + deployments24h: 0, + failures24h: 0, + }; + + for (const project of projects) { + summary[project.health] += 1; + summary.deployments24h += project.deployments24h; + summary.failures24h += project.failures24h; + } + + const priority: Record = { + failed: 0, + active: 1, + unknown: 2, + paused: 3, + healthy: 4, + never: 5, + }; + + return { + generatedAt: now.toISOString(), + summary, + projects: [...projects].sort((a, b) => { + const healthOrder = priority[a.health] - priority[b.health]; + return healthOrder || a.name.localeCompare(b.name); + }), + }; +} diff --git a/deployment-pulse-plugin/tsconfig.json b/deployment-pulse-plugin/tsconfig.json new file mode 100644 index 0000000..64f5446 --- /dev/null +++ b/deployment-pulse-plugin/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "verbatimModuleSyntax": true + }, + "include": ["src/**/*.ts", "scripts/**/*.ts"] +} diff --git a/deployment-pulse-plugin/web/bun.lock b/deployment-pulse-plugin/web/bun.lock new file mode 100644 index 0000000..33a382c --- /dev/null +++ b/deployment-pulse-plugin/web/bun.lock @@ -0,0 +1,259 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "deployment-pulse-web", + "dependencies": { + "react": "19.1.0", + "react-dom": "19.1.0", + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.7.0", + "typescript": "^5.5.4", + "vite": "^6.4.3", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], + + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], + + "@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], + + "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.419", "", {}, "sha512-nHMPn8x4yCxCI0iSnL+LlHL5sUoUfjLXkcRIagZ4GBdrfFLFaiLNvzJWbJqZhFT9IAhw5tUSNlhggWN+otvp/A=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="], + + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + } +} diff --git a/deployment-pulse-plugin/web/index.html b/deployment-pulse-plugin/web/index.html new file mode 100644 index 0000000..52c4304 --- /dev/null +++ b/deployment-pulse-plugin/web/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Deployment Pulse + + +
+ + + diff --git a/deployment-pulse-plugin/web/package.json b/deployment-pulse-plugin/web/package.json new file mode 100644 index 0000000..2d930b9 --- /dev/null +++ b/deployment-pulse-plugin/web/package.json @@ -0,0 +1,19 @@ +{ + "name": "deployment-pulse-web", + "private": true, + "type": "module", + "scripts": { + "build": "vite build" + }, + "dependencies": { + "react": "19.1.0", + "react-dom": "19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.7.0", + "typescript": "^5.5.4", + "vite": "^6.4.3" + } +} diff --git a/deployment-pulse-plugin/web/src/App.tsx b/deployment-pulse-plugin/web/src/App.tsx new file mode 100644 index 0000000..51ea91d --- /dev/null +++ b/deployment-pulse-plugin/web/src/App.tsx @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { projectPath } from "./project-link"; + +type HealthState = "healthy" | "failed" | "active" | "paused" | "never" | "unknown"; + +interface Deployment { + id: number; + state: string; + branch?: string; + commit_sha?: string; + commit_message?: string; + created_at: string; +} + +interface ProjectPulse { + id: number; + name: string; + slug: string; + preset: string; + sourceType: string; + health: HealthState; + latestDeployment: Deployment | null; + recentDeployments: number; + recentFailures: number; + successRate: number | null; + error?: string; +} + +interface Overview { + generatedAt: string; + summary: Record & { + total: number; + deployments24h: number; + failures24h: number; + }; + projects: ProjectPulse[]; +} + +const API_URL = "/api/x/deployment-pulse/overview"; +const FILTERS: Array<{ value: "all" | HealthState; label: string }> = [ + { value: "all", label: "All" }, + { value: "failed", label: "Needs attention" }, + { value: "active", label: "In progress" }, + { value: "healthy", label: "Healthy" }, +]; + +const STATUS_LABEL: Record = { + healthy: "Healthy", + failed: "Failed", + active: "In progress", + paused: "Paused", + never: "Not deployed", + unknown: "Unavailable", +}; + +function relativeTime(value?: string): string { + if (!value) return "Never"; + const seconds = Math.max(0, Math.floor((Date.now() - Date.parse(value)) / 1000)); + if (seconds < 60) return "Just now"; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; +} + +function Icon({ name }: { name: "pulse" | "refresh" | "search" | "arrow" }) { + const paths = { + pulse: , + refresh: <>, + search: <>, + arrow: <>, + }; + return ; +} + +export function App() { + const [overview, setOverview] = useState(null); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [query, setQuery] = useState(""); + const [filter, setFilter] = useState<"all" | HealthState>("all"); + + const load = useCallback(async (background = false) => { + background ? setRefreshing(true) : setLoading(true); + try { + const response = await fetch(API_URL, { headers: { Accept: "application/json" } }); + if (!response.ok) throw new Error(`Temps returned ${response.status}`); + setOverview(await response.json()); + setError(""); + } catch (reason) { + setError(reason instanceof Error ? reason.message : "Could not load deployments"); + } finally { + setLoading(false); + setRefreshing(false); + } + }, []); + + useEffect(() => { + void load(); + const timer = window.setInterval(() => void load(true), 30_000); + return () => window.clearInterval(timer); + }, [load]); + + const projects = useMemo(() => { + const needle = query.trim().toLowerCase(); + return (overview?.projects ?? []).filter((project) => { + const matchesFilter = filter === "all" || project.health === filter; + const matchesQuery = + !needle || + [project.name, project.slug, project.preset, project.latestDeployment?.branch] + .filter(Boolean) + .some((value) => value!.toLowerCase().includes(needle)); + return matchesFilter && matchesQuery; + }); + }, [filter, overview, query]); + + if (loading) { + return

Reading deployment history…

; + } + + if (!overview) { + return ( +
+
+

Deployment data is unavailable

+

{error || "Deployment Pulse could not reach Temps."}

+ +
+ ); + } + + const attention = overview.summary.failed + overview.summary.unknown; + + return ( +
+
+
+
Live deployment health
+

{overview.summary.total === 0 ? "No projects to monitor yet." : attention === 0 ? "Everything is shipping cleanly." : `${attention} project${attention === 1 ? "" : "s"} need attention.`}

+

{overview.summary.total === 0 ? "Create or import a project and its deployment health will appear here." : `Latest status across ${overview.summary.total} projects · refreshed ${relativeTime(overview.generatedAt)}`}

+
+ +
+ +
+
0 ? "metric-alert" : "metric-clear"}`}> + Needs attention + {attention} + {overview.summary.failures24h} failed in the last 24h +
+
+ Healthy + {overview.summary.healthy} + latest deployment ready +
+
+ In progress + {overview.summary.active} + building or deploying now +
+
+ Activity + {overview.summary.deployments24h} + latest deployments in 24h +
+
+ +
+
+
+

Projects

+

Failures and active deploys are shown first.

+
+ {projects.length} shown +
+ +
+ +
+ {FILTERS.map((item) => ( + + ))} +
+
+ + {error &&
Refresh failed. Showing the last available snapshot.
} + +
+ {projects.length === 0 ? ( +
No projects match this view.
+ ) : projects.map((project) => { + const deployment = project.latestDeployment; + return ( +
+ +
+ {project.name} + {project.slug} · {project.preset} +
+
+ + {deployment?.commit_message || + (project.error ?? + (deployment ? `Deployment #${deployment.id}` : "No deployment yet"))} + + {deployment?.branch || project.sourceType}{deployment?.commit_sha ? ` · ${deployment.commit_sha.slice(0, 7)}` : ""} +
+
+ {project.successRate === null ? "—" : `${project.successRate}%`} + last {project.recentDeployments} +
+
+ {relativeTime(deployment?.created_at)} + {deployment ? `#${deployment.id}` : "No history"} +
+
{STATUS_LABEL[project.health]}
+ +
+ ); + })} +
+
+
+ ); +} diff --git a/deployment-pulse-plugin/web/src/main.tsx b/deployment-pulse-plugin/web/src/main.tsx new file mode 100644 index 0000000..2160355 --- /dev/null +++ b/deployment-pulse-plugin/web/src/main.tsx @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import React from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./styles.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/deployment-pulse-plugin/web/src/project-link.test.ts b/deployment-pulse-plugin/web/src/project-link.test.ts new file mode 100644 index 0000000..500c396 --- /dev/null +++ b/deployment-pulse-plugin/web/src/project-link.test.ts @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import { describe, expect, it } from "bun:test"; +import { projectPath } from "./project-link"; + +describe("projectPath", () => { + it("builds the console route from the project slug", () => { + expect(projectPath("observability-starter")).toBe( + "/projects/observability-starter", + ); + }); + + it("encodes unexpected characters instead of changing the route shape", () => { + expect(projectPath("customer/app")).toBe("/projects/customer%2Fapp"); + }); +}); diff --git a/deployment-pulse-plugin/web/src/project-link.ts b/deployment-pulse-plugin/web/src/project-link.ts new file mode 100644 index 0000000..53d1d65 --- /dev/null +++ b/deployment-pulse-plugin/web/src/project-link.ts @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +export function projectPath(slug: string): string { + return `/projects/${encodeURIComponent(slug)}`; +} diff --git a/deployment-pulse-plugin/web/src/styles.css b/deployment-pulse-plugin/web/src/styles.css new file mode 100644 index 0000000..dcddf5d --- /dev/null +++ b/deployment-pulse-plugin/web/src/styles.css @@ -0,0 +1,183 @@ +/* SPDX-FileCopyrightText: 2024-2026 Temps Contributors */ +/* SPDX-License-Identifier: MIT OR Apache-2.0 */ + +@import url("https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500&family=Geist:wght@400;500;600&display=swap"); + +:root { + font-family: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: #171717; + background: #fff; + font-synthesis: none; + --border: #e7e7e7; + --muted: #737373; + --surface: #fafafa; + --green: #16a363; + --red: #e5484d; + --amber: #d97706; + --blue: #0070f3; +} + +* { box-sizing: border-box; } +html { color-scheme: light dark; } +body { margin: 0; min-width: 320px; background: #fff; } +button, input { font: inherit; } +button, a { -webkit-tap-highlight-color: transparent; } +button:focus-visible, input:focus-visible, a:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; } + +.shell { width: min(1180px, calc(100% - 48px)); margin: 0 auto; padding: 32px 0 64px; } +.hero { display: flex; align-items: flex-end; justify-content: space-between; gap: 32px; padding-bottom: 28px; border-bottom: 1px solid var(--border); } +.eyebrow { display: flex; align-items: center; gap: 8px; color: var(--muted); font: 500 12px/1 "Geist Mono", monospace; letter-spacing: .04em; text-transform: uppercase; } +.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 4px #e6f7ef; } +h1 { max-width: 820px; margin: 13px 0 7px; font-size: clamp(32px, 4vw, 50px); line-height: 1.04; letter-spacing: -.045em; font-weight: 600; } +.hero p, .section-heading p { margin: 0; color: var(--muted); font-size: 14px; } +.refresh-button { display: inline-flex; align-items: center; gap: 8px; min-height: 38px; padding: 0 14px; border: 1px solid #d4d4d4; border-radius: 7px; background: #fff; color: #262626; cursor: pointer; transition: background .15s, border-color .15s, transform .15s; } +.refresh-button:hover { background: #f5f5f5; border-color: #a3a3a3; } +.refresh-button:active { transform: translateY(1px); } +.refresh-button:disabled { opacity: .55; cursor: wait; } +.refresh-button svg, .project-row a svg, .search-field svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; } +.refresh-button:disabled svg { animation: spin .8s linear infinite; } + +.metrics { display: grid; grid-template-columns: repeat(4, 1fr); border: 1px solid var(--border); border-radius: 10px; margin: 24px 0 42px; overflow: hidden; } +.metric { position: relative; min-height: 132px; padding: 19px 21px; border-right: 1px solid var(--border); background: #fff; } +.metric:last-child { border-right: 0; } +.metric-primary { background: #171717; color: white; } +.metric-primary::before { content: ""; position: absolute; inset: 16px auto 16px 0; width: 3px; border-radius: 0 999px 999px 0; background: currentColor; opacity: 0; } +.metric-label { display: block; color: var(--muted); font-size: 12px; font-weight: 500; } +.metric-primary .metric-label, .metric-primary small { color: #a3a3a3; } +.metric strong { display: block; margin: 11px 0 6px; font: 500 34px/1 "Geist Mono", monospace; letter-spacing: -.04em; } +.metric small { color: var(--muted); font-size: 12px; } + +.section-heading { display: flex; align-items: end; justify-content: space-between; margin-bottom: 17px; } +h2 { margin: 0 0 3px; font-size: 20px; letter-spacing: -.025em; } +.result-count { color: var(--muted); font: 12px/1 "Geist Mono", monospace; } +.controls { display: flex; gap: 10px; margin-bottom: 14px; } +.search-field { flex: 1; display: flex; align-items: center; gap: 9px; height: 40px; padding: 0 12px; border: 1px solid #d4d4d4; border-radius: 7px; color: var(--muted); background: #fff; } +.search-field:focus-within { border-color: #737373; box-shadow: 0 0 0 1px #737373; } +.search-field input { width: 100%; border: 0; outline: 0; color: #171717; background: transparent; font-size: 13px; } +.filters { display: flex; gap: 6px; } +.filters button { display: inline-flex; align-items: center; gap: 7px; height: 40px; padding: 0 12px; border: 1px solid #d4d4d4; border-radius: 7px; background: #fff; color: #525252; cursor: pointer; transition: .15s ease; } +.filters button:hover { color: #171717; border-color: #a3a3a3; background: #f5f5f5; } +.filters button.active { color: #fff; border-color: #171717; background: #171717; } +.filters button.active:hover { background: #292929; } +.filters button span { font: 11px/1 "Geist Mono", monospace; opacity: .7; } + +.project-list { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; } +.project-row { display: grid; grid-template-columns: 12px minmax(145px, 1.05fr) minmax(200px, 1.65fr) 80px 95px 110px 28px; align-items: center; gap: 14px; min-height: 76px; padding: 12px 16px; border-bottom: 1px solid var(--border); background: #fff; transition: background .15s ease; } +.project-row:last-child { border-bottom: 0; } +.project-row:hover { background: var(--surface); } +.status-mark { display: grid; place-items: center; width: 10px; height: 42px; } +.status-mark span { width: 3px; height: 28px; border-radius: 999px; background: #a3a3a3; } +.status-mark.healthy span { background: var(--green); } +.status-mark.failed span { background: var(--red); } +.status-mark.active span { background: var(--blue); animation: breathe 1.8s ease-in-out infinite; } +.status-mark.paused span { background: var(--amber); } +.project-identity, .deployment-copy, .success-rate, .latest-time { min-width: 0; } +.project-identity strong, .deployment-copy strong, .success-rate strong, .latest-time strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 500; } +.project-identity span, .deployment-copy span, .success-rate span, .latest-time span { display: block; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font: 11px/1.3 "Geist Mono", monospace; } +.deployment-copy strong { color: #404040; font-weight: 400; } +.status-pill { justify-self: start; display: inline-flex; align-items: center; gap: 6px; padding: 5px 8px; border: 1px solid var(--border); border-radius: 999px; background: #fff; color: #525252; font-size: 11px; white-space: nowrap; } +.status-pill span { width: 6px; height: 6px; border-radius: 50%; background: #a3a3a3; } +.status-pill.healthy span { background: var(--green); } +.status-pill.failed span { background: var(--red); } +.status-pill.active span { background: var(--blue); } +.status-pill.paused span { background: var(--amber); } +.project-row a { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 6px; color: #737373; transition: background .15s, color .15s; } +.project-row a:hover { color: #171717; background: #eaeaea; } +.warning-banner { margin-bottom: 10px; padding: 9px 12px; border: 1px solid #f5d0a6; border-radius: 7px; color: #8a4b08; background: #fff8ed; font-size: 12px; } +.no-results { padding: 48px 20px; text-align: center; color: var(--muted); font-size: 13px; } +.empty-state { display: grid; justify-items: center; align-content: center; min-height: 70vh; text-align: center; } +.empty-state h1 { font-size: 28px; margin: 18px 0 8px; } +.empty-state p { color: var(--muted); margin: 0 0 20px; } +.brand-mark { display: grid; place-items: center; width: 44px; height: 44px; border-radius: 9px; color: #fff; background: #171717; } +.brand-mark svg { width: 23px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } +.primary-button { height: 38px; padding: 0 16px; border: 0; border-radius: 7px; color: #fff; background: #171717; cursor: pointer; } +.primary-button:hover { background: #333; } +.loading-line { position: fixed; top: 0; left: 0; width: 35%; height: 2px; background: #171717; animation: loading 1.1s ease-in-out infinite alternate; } +.loading-copy { margin-top: 25vh; color: var(--muted); text-align: center; font-size: 13px; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } + +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes breathe { 50% { opacity: .35; } } +@keyframes loading { from { transform: translateX(-100%); } to { transform: translateX(280%); } } + +@media (max-width: 900px) { + .metrics { grid-template-columns: repeat(2, 1fr); } + .metric:nth-child(2) { border-right: 0; } + .metric:nth-child(-n+2) { border-bottom: 1px solid var(--border); } + .project-row { grid-template-columns: 12px minmax(130px, 1fr) minmax(180px, 1.4fr) 95px 110px 28px; } + .success-rate { display: none; } + .controls { flex-direction: column; } + .filters { overflow-x: auto; padding-bottom: 2px; } +} + +@media (max-width: 650px) { + .shell { width: min(100% - 28px, 1180px); padding-top: 22px; } + .hero { align-items: flex-start; } + h1 { font-size: 32px; } + .refresh-button span { display: none; } + .metrics { grid-template-columns: 1fr 1fr; } + .metric { min-height: 112px; padding: 15px; } + .project-row { grid-template-columns: 8px 1fr auto 28px; gap: 10px; } + .deployment-copy, .latest-time { display: none; } + .status-pill { justify-self: end; } + .filters button { padding: 0 10px; white-space: nowrap; } +} + +@media (prefers-color-scheme: dark) { + :root { + color: #f5f5f5; + background: #0a0a0b; + --border: #2a2a2d; + --muted: #a3a3aa; + --surface: #18181a; + --green: #3dd68c; + --red: #ff6369; + --amber: #f5a524; + --blue: #4c9aff; + } + + body { background: #0a0a0b; } + h1, h2 { color: #f5f5f5; } + .live-dot { box-shadow: 0 0 0 4px #123f2b; } + + .metrics, .project-list { background: #111113; } + .metric, .project-row { background: #111113; } + .metric-primary { background: #111113; color: #f5f5f5; } + .metric-primary::before { opacity: 1; } + .metric-primary.metric-clear::before { background: var(--green); } + .metric-primary.metric-alert::before { background: var(--red); } + .metric-primary .metric-label, .metric-primary small { color: var(--muted); } + .project-row:hover { background: #18181a; } + + .refresh-button, .search-field, .filters button { + border-color: #3a3a3e; + background: #111113; + color: #d4d4d8; + } + .refresh-button:hover, .filters button:hover { + border-color: #5a5a61; + background: #1b1b1e; + color: #fafafa; + } + .refresh-button:disabled { color: #a3a3aa; opacity: .7; } + .search-field:focus-within { border-color: #71717a; box-shadow: 0 0 0 1px #71717a; } + .search-field input { color: #f5f5f5; } + .search-field input::placeholder { color: #85858d; opacity: 1; } + .filters button.active { border-color: #e4e4e7; background: #e4e4e7; color: #18181b; } + .filters button.active:hover { border-color: #fafafa; background: #fafafa; color: #09090b; } + + .project-identity strong, .success-rate strong, .latest-time strong { color: #f4f4f5; } + .deployment-copy strong { color: #d4d4d8; } + .status-pill { border-color: #3a3a3e; background: #18181a; color: #d4d4d8; } + .project-row a { color: #a1a1aa; } + .project-row a:hover { background: #242427; color: #fafafa; } + + .warning-banner { border-color: #624315; background: #251b0d; color: #ffc16b; } + .brand-mark, .primary-button { background: #f4f4f5; color: #111113; } + .primary-button:hover { background: #d4d4d8; } + .loading-line { background: #f4f4f5; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } +} diff --git a/deployment-pulse-plugin/web/tsconfig.json b/deployment-pulse-plugin/web/tsconfig.json new file mode 100644 index 0000000..e935337 --- /dev/null +++ b/deployment-pulse-plugin/web/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/deployment-pulse-plugin/web/vite.config.ts b/deployment-pulse-plugin/web/vite.config.ts new file mode 100644 index 0000000..090a1d3 --- /dev/null +++ b/deployment-pulse-plugin/web/vite.config.ts @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + base: "/api/x/deployment-pulse/ui/", + build: { + outDir: "dist", + emptyOutDir: true, + }, +});