From b5155cb8d3d94e951c1b1e79b65d20f5ec7967fc Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Wed, 8 Jul 2026 03:31:00 -0400 Subject: [PATCH 1/2] chore(adr): record console gateway and webapp stack decisions Signed-off-by: Yordis Prieto --- ...pc-gateway-for-browser-product-surfaces.md | 172 ++++++++++++++++ docs/adr/0019-console-webapp-stack.md | 191 ++++++++++++++++++ docs/adr/index.md | 2 + 3 files changed, 365 insertions(+) create mode 100644 docs/adr/0018-connectrpc-gateway-for-browser-product-surfaces.md create mode 100644 docs/adr/0019-console-webapp-stack.md diff --git a/docs/adr/0018-connectrpc-gateway-for-browser-product-surfaces.md b/docs/adr/0018-connectrpc-gateway-for-browser-product-surfaces.md new file mode 100644 index 000000000..e606bf052 --- /dev/null +++ b/docs/adr/0018-connectrpc-gateway-for-browser-product-surfaces.md @@ -0,0 +1,172 @@ +--- +number: "0018" +slug: connectrpc-gateway-for-browser-product-surfaces +status: accepted +date: 2026-07-08 +--- + +# ADR 0018: ConnectRPC Gateway for Browser Product Surfaces + +## Context + +The platform is getting its first product-facing web application, the operator +console ([ADR 0019](./0019-console-webapp-stack.md)). A browser has to call +first-party platform services, and every existing first-party RPC path lives on +the NATS backbone: protobuf services bind to NATS micro +([ADR 0016](./0016-protobuf-rpc-over-nats-micro-binding.md)), the JSON-RPC +family binds to NATS subjects +([ADR 0011](./0011-jsonrpc-over-nats-binding.md)), and every signed request on +the mesh carries the AAuth NATS PoP envelope +([ADR 0017](./0017-aauth-agent-authentication.md)). + +NATS itself does not keep a browser off the backbone. `nats-server` ships a +WebSocket listener, `nats.ws` runs in browsers, and NATS has real +connection-level authentication: passwords, tokens, NKey challenge-response, +decentralized user JWTs, and auth callout, which can mint an ephemeral, +subject-scoped NATS user from an external credential such as a web session. A +browser connection to the backbone is technically achievable. + +Connection authentication is not the boundary that matters here. First-party +requests on the mesh carry the AAuth PoP envelope: each request is signed with +a `cnf.jwk`-bound private key, with nonce and content-digest bookkeeping. A +browser tab cannot provide that custody, because any key the page's scripts +can use, an injected script can use and exfiltrate. Putting browsers on the +bus would also make the internal subject namespace an internet-facing surface +whose authorization story is NATS subject permissions rather than a reviewed +method allowlist, and it would require a JavaScript reimplementation of the +ADR 0011/0016/0017 bindings that must track the Rust implementations forever. + +[ADR 0003](./0003-ai-protocol-transport-taxonomy.md) already selects the API +style for this situation. Its boundary selection order names +"browser-compatible HTTP access" as a trigger for a first-party service API and +prefers ConnectRPC for that surface. What ADR 0003 does not decide is where the +boundary lives, who holds which credentials, and how a ConnectRPC method +reaches a NATS micro endpoint. Without one rule, each product surface would +invent its own bridge, its own token custody, and its own error mapping, the +same per-call-site drift ADR 0011 and ADR 0016 exist to prevent. + +## Decision + +### 1. Product web surfaces reach the mesh only through a gateway + +A browser product surface talks to a first-party gateway service exposing +ConnectRPC over HTTPS. The gateway is a gateway in the ADR 0003 sense: a +production edge component that accepts external traffic and routes it inward, +containing a bridge onto the backbone. + +The ConnectRPC surface is generated from the same `.proto` sources that define +the backbone services ([ADR 0009](./0009-protocol-buffers-wire-contracts.md)). +Browser clients are generated with `protobuf-es` and `connect-es` from the +same Buf pipeline that generates the Rust code. There is no hand-written HTTP +client, no parallel OpenAPI document, and no GraphQL layer; the exceptions ADR +0003 allows for OpenAPI remain exceptions and are not triggered by a +first-party browser surface. + +Browsers do not connect to NATS directly, even though the WebSocket listener +and auth callout would make a scoped connection possible. Connection-level +NATS auth cannot substitute for the per-request AAuth PoP envelope, and the +envelope is exactly what a page cannot sign safely. Browser NATS access, if a +narrow read-only case ever justifies it, requires its own decision and does +not weaken this default. + +### 2. The gateway is the credential boundary + +The human operator authenticates to the gateway with an OAuth 2.0 +Authorization Code + PKCE flow. The gateway completes the code exchange, holds +the resulting tokens server-side, and issues an HttpOnly, SameSite session +cookie to the browser. The browser never receives an AAuth token, a signing +key, or a NATS credential of any kind. + +Browser auth is cookie-based, and token-in-page patterns are prohibited: no +access, refresh, ID, or session token in `localStorage`, `sessionStorage`, +IndexedDB, JavaScript-readable cookies, or long-lived JavaScript memory, and +no `Authorization: Bearer` header minted by page code. The session cookie is +an opaque identifier, not a JWT; session state lives in the gateway. Anything +JavaScript can read, an injected script can exfiltrate; an HttpOnly cookie +limits an XSS to riding the live session, which is the strictly smaller +failure. Cookie-based auth carries CSRF obligations, which the gateway owns: +`SameSite` on the cookie, strict `Origin` checking on every state-changing +request, and CORS locked to the product surface's origin. + +On the mesh, the gateway is an agent under ADR 0017: it holds its own +`aa-agent+jwt` and signing key, signs every backbone request with the Trogon +NATS PoP binding, and, when acting on behalf of an authenticated operator, +presents the operator-linked `aa-auth+jwt` in `AAuth-Auth-Token` alongside its +own agent token. Person-linked authorization therefore rides the same +mechanism every other agent on the mesh uses; the gateway adds no parallel +identity scheme. + +Auth layering is explicit. NATS auth callout (`a2a-auth-callout`) is +connection admission: it decides which clients may attach to the backbone and +scopes their subject permissions, and the gateway attaches under that +admission like any other mesh client. The AAuth PoP envelope is request +authentication, and the policy tiers are authorization. Connection admission +does not substitute for either layer above it, which is why a browser +admitted through auth callout would still be unable to make signed +first-party calls. + +### 3. The bridge is mechanical and holds no business logic + +ADR 0016 binds protobuf method names to NATS subjects deterministically, so +the gateway maps traffic without per-method invention: + +- A unary Connect RPC becomes one NATS request-reply on the bound subject. +- A server-streaming Connect RPC bridges a NATS subscription into one Connect + stream, scoped to the operator's session. +- Error mapping is canonical: the NATS micro error channel carries the + gRPC-idiom status semantics ADR 0016 defines, and Connect uses the same + canonical status codes, so the gateway translates status and message without + inventing an error vocabulary. + +The gateway performs translation, session handling, authorization screening, +and streaming fan-out. It does not aggregate, decide, or own domain rules. +When a handler needs business logic, that logic belongs in a platform service +behind the backbone, and the gateway exposes that service's method instead. + +### 4. Exposure is explicit and default-closed + +Being an ADR 0016 service does not make a method browser-reachable. The +gateway exposes an explicit allowlist of services and methods; everything else +on the backbone is unreachable from the browser surface. Adding a method to a +product surface is a reviewed gateway change, not a side effect of deploying a +backbone service. + +### 5. One gateway workload per product surface family + +A gateway is one operated workload in the ADR 0003 combined-binary sense: one +deployment unit, one telemetry identity, one security boundary. It may also +serve the static assets of its product surface when the assets share +ownership and release cadence, keeping a product surface deployable as one +service. Distinct product surfaces with different audiences, trust levels, or +release cadences get their own gateway workload rather than sharing one +allowlist and session model. + +## Consequences + +- The repository gains a new workload class, the product surface gateway. The + console gateway is its first instance. +- Browser code consumes generated Connect clients, so the `.proto` sources + remain the single wire contract from browser to backbone service. +- Trace context propagates end to end under + [ADR 0008](./0008-opentelemetry-observability.md): the browser sends + `traceparent` on every Connect call and the gateway continues that trace + onto its NATS micro requests. +- Live updates in a browser surface are Connect server streams fed by NATS + subscriptions in the gateway; product surfaces do not open their own NATS + connections. +- The gateway's session store and PoP signing make it stateful in the same + ways the A2A gateway already is; replay-store and multi-node caveats from + ADR 0017 apply to it equally. +- A future non-browser consumer that needs an explicit API surface (partner + integration, external tooling) can reuse the same ConnectRPC surface + without a new decision, because ADR 0003 already prefers ConnectRPC there. + +## References + +- [ADR 0003: AI Protocol Transport Taxonomy](./0003-ai-protocol-transport-taxonomy.md) +- [ADR 0009: Protocol Buffers Wire Contracts](./0009-protocol-buffers-wire-contracts.md) +- [ADR 0011: JSON-RPC over NATS Binding](./0011-jsonrpc-over-nats-binding.md) +- [ADR 0016: Protocol Buffers RPC over NATS micro Binding](./0016-protobuf-rpc-over-nats-micro-binding.md) +- [ADR 0017: AAuth Agent Authentication over a Trogon NATS PoP Binding](./0017-aauth-agent-authentication.md) +- [ConnectRPC protocol reference](https://connectrpc.com/docs/protocol/) +- [Protobuf-ES](https://github.com/bufbuild/protobuf-es) diff --git a/docs/adr/0019-console-webapp-stack.md b/docs/adr/0019-console-webapp-stack.md new file mode 100644 index 000000000..c1e5aa2bd --- /dev/null +++ b/docs/adr/0019-console-webapp-stack.md @@ -0,0 +1,191 @@ +--- +number: "0019" +slug: console-webapp-stack +status: accepted +date: 2026-07-08 +--- + +# ADR 0019: Console Webapp Stack + +## Context + +The platform needs an operator console: a product-facing web application for +managing agents and schedules, browsing the discovery catalog +([ADR 0012](./0012-ard-compatible-discovery-catalog.md)), and observing live +execution. It is the repository's first product web application, so its stack +choices set the default for every later TypeScript product surface. + +The console's data path is decided by +[ADR 0018](./0018-connectrpc-gateway-for-browser-product-surfaces.md): the +browser speaks ConnectRPC to a gateway that bridges onto the NATS backbone and +owns all credentials. This ADR decides everything above that boundary: the +application framework, build tooling, component system, data and form +libraries, testing, and how the application scales to more product areas +without a runtime composition scheme. + +An operator console is an authenticated, information-dense internal surface. +It has no anonymous visitors, no search-engine surface, and no +first-paint-for-logged-out-users requirement. + +## Decision + +### 1. Placement follows ADR 0005 + +The console lives at `tsworkspace/apps/console/`, with reusable TypeScript +packages under `tsworkspace/packages/`, per the layout and App Surface Rules +in [ADR 0005](./0005-polyglot-workspace-layout.md). There is no top-level +`webapps/` directory. If the console later grows desktop or mobile surfaces, +it regroups as `tsworkspace/apps/console/{web,desktop,shared}/` as ADR 0005 +already prescribes. The workspace uses pnpm, with Turborepo as the task +runner so build, test, and typecheck tasks are dependency-ordered and cached +as the package graph grows. + +Package names use the `@trogonai/` scope per ADR 0005, and the scope is +protected against public-registry collision deliberately. An npm scope is an +owned namespace, so the `@trogonai` organization must be registered on the +public npm registry (unclaimed and empty as of 2026-07-08) before these names +are treated as authoritative; once claimed, no third party can publish under +them. Independently of scope ownership, internal cross-package dependencies +use the pnpm `workspace:` protocol, which resolves only inside the workspace +and never falls back to the registry, closing the dependency-confusion path +even for an unclaimed name, and every package not deliberately published +carries `"private": true`. Directory-derived scopes such as `@tsworkspace/*` +are not used: the scope names the owning identity, not the repository layout, +and it must remain a namespace the project can actually own on the registry. + +### 2. The console is a Vite single-page application, not a meta-framework app + +The console is a client-rendered SPA built with Vite. Next.js, Nuxt, and +similar server-rendering meta-frameworks are not used. Their value is a +server tier for SEO and anonymous first paint, which this surface does not +have, and that tier would duplicate session and credential responsibilities +that ADR 0018 assigns to the gateway. The build output is static assets served +by the console gateway, keeping the product surface one deployable workload. + +If server rendering ever becomes a real requirement, the migration path is +TanStack Start, which builds on the same router and Vite toolchain. + +### 3. React with a TanStack-first library rule + +The console is a React application. When a needed capability has a viable +TanStack option, the console uses it before considering alternatives: + +- Routing: TanStack Router with file-based route trees. +- Server state: TanStack Query over the generated Connect clients. Query owns + every server-derived value; no server data is copied into client stores. +- Tables: TanStack Table, with TanStack Virtual when row counts require + virtualization. +- Forms: TanStack Form. Form owns edit state; submission goes through a + Connect mutation and Query invalidation. +- Client-only state: component-local state by default. If shared client state + accumulates, TanStack Store before any other state library. + +Deviating from a viable TanStack option requires a stated reason in the code +review that introduces the deviation. + +### 4. UI components are shadcn on Base UI, and Radix is prohibited + +Styling is Tailwind CSS. Components follow the shadcn model (source-owned +components generated into the repository, not a packaged component +dependency) built on Base UI primitives. Exactly one headless primitive layer +is allowed in the dependency tree: `@radix-ui/*` packages must not appear, +directly or transitively. Community shadcn components that import Radix are +ported to Base UI rather than adopted as-is. The workspace enforces the ban +with tooling rather than review vigilance. + +### 5. Validation is zod at the boundary, per the ADR 0009 conversion rule + +Runtime validation uses zod, wired into TanStack Form through Standard +Schema. Generated protobuf types do not leak primitive obsession into +application code: screens and forms convert generated messages into richer +edit models at the boundary when validation, units, identity, or invariants +matter, the same conversion rule +[ADR 0009](./0009-protocol-buffers-wire-contracts.md) sets for domain code. + +### 6. Generated Connect clients come from the existing Buf pipeline + +`protobuf-es` and `connect-es` code generation is added to the repository's +`buf.gen.yaml`, emitting a generated protocol package under +`tsworkspace/packages/`. The `.proto` sources under `proto/` remain the only +wire contract; the console defines no hand-written request or response +shapes. + +### 7. Testing is unit-first, per ADR 0010's posture + +- Vitest with Testing Library for unit and component tests. +- Connect's router transport for API mocking, so tests exercise the same + generated service interfaces the app uses instead of intercepting HTTP. +- Playwright for end-to-end coverage of the flows that cannot be trusted to + unit tests, primarily authentication and live streaming. + +### 8. Lint and format run on the Oxc toolchain + +Linting is oxlint and formatting is oxfmt, run workspace-wide, replacing the +ESLint + Prettier combination. One Rust toolchain covers both jobs with near +instant runs, which matches the repository's tooling posture, and oxlint's +default rule set applies without a config sprawl. Generated files +(`routeTree.gen.ts`) are excluded from formatting. Adopting ESLint later is +warranted only if a needed rule class (for example exhaustive React hooks +checks) is missing from oxlint, and that adoption is additive rather than a +replacement. + +### 9. Telemetry follows ADR 0008 + +The console uses the OpenTelemetry Web SDK, exports OTLP to the same +collector path the platform uses, and propagates `traceparent` on every +Connect call through a client interceptor so one operator action traces from +the browser through the gateway onto the backbone +([ADR 0008](./0008-opentelemetry-observability.md)). + +### 10. The console scales by vertical slices, not microfrontends + +Each product area (agents, schedules, catalog, execution) is a vertical +slice: its own package or route subtree, lazily loaded through the router, +with screens, queries, and forms owned by the slice. Slices do not import +each other's internals; contact happens through shared packages or route +navigation. + +Runtime composition schemes (module federation, microfrontends) are not +used. They solve independent deploy cadence across many teams, which this +surface does not have, at the permanent cost of duplicated dependencies, +singleton version skew, and design drift. The reopening conditions are: + +- Multiple teams needing independent deploy cadence into one surface, which + would justify revisiting composition. +- Untrusted third-party UI, which gets sandboxed iframes with a message + bridge, not module federation, because the problem is isolation. + +A different audience, trust level, or release cadence is a new app under +`tsworkspace/apps/` with its own gateway workload (ADR 0018), sharing code +through workspace packages at build time, never through runtime federation. + +## Consequences + +- The first product surface establishes `tsworkspace/` with `apps/console/` + and the shared `packages/` split, so later TypeScript surfaces inherit a + working layout instead of a greenfield decision. +- End-to-end typing holds from `.proto` to screen: generated Connect clients, + typed routes, typed forms, and zod-validated edit models. +- The Radix prohibition keeps one primitive layer's focus, portal, and + dismissal semantics across the console, at the cost of occasionally porting + a community component to Base UI. +- The TanStack-first rule removes a per-feature library debate and keeps one + vendor's conventions across routing, data, tables, and forms. +- Chart tooling is deliberately not pinned here; it is a reversible choice + to make when the first chart lands. +- A future schema-driven UI (rendering service-declared config and status + surfaces from the ADR 0012 discovery catalog) is the preferred path for + third-party extensibility, keeping extension a matter of publishing + schemas rather than shipping JavaScript into the console. + +## References + +- [ADR 0005: Polyglot Workspace Layout](./0005-polyglot-workspace-layout.md) +- [ADR 0008: OpenTelemetry Observability](./0008-opentelemetry-observability.md) +- [ADR 0009: Protocol Buffers Wire Contracts](./0009-protocol-buffers-wire-contracts.md) +- [ADR 0010: Unit Tests First, Testcontainers Only When Necessary](./0010-testcontainers-for-infrastructure-tests.md) +- [ADR 0012: ARD-Compatible Discovery Catalog](./0012-ard-compatible-discovery-catalog.md) +- [ADR 0018: ConnectRPC Gateway for Browser Product Surfaces](./0018-connectrpc-gateway-for-browser-product-surfaces.md) +- [TanStack](https://tanstack.com/) +- [Base UI](https://base-ui.com/) +- [shadcn/ui](https://ui.shadcn.com/) diff --git a/docs/adr/index.md b/docs/adr/index.md index a106725fe..9450ff49e 100644 --- a/docs/adr/index.md +++ b/docs/adr/index.md @@ -22,3 +22,5 @@ future implementation work. - [ADR 0015: Rust TLS Library](./0015-rust-tls-library.md) - [ADR 0016: Protocol Buffers RPC over NATS micro Binding](./0016-protobuf-rpc-over-nats-micro-binding.md) - [ADR 0017: AAuth Agent Authentication over a Trogon NATS PoP Binding](./0017-aauth-agent-authentication.md) +- [ADR 0018: ConnectRPC Gateway for Browser Product Surfaces](./0018-connectrpc-gateway-for-browser-product-surfaces.md) +- [ADR 0019: Console Webapp Stack](./0019-console-webapp-stack.md) From 09235ee0fd629008d9359f4e57778bf774d1d574 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Wed, 8 Jul 2026 03:31:00 -0400 Subject: [PATCH 2/2] feat(console): scaffold console webapp workspace Signed-off-by: Yordis Prieto --- .github/workflows/ci-web.yml | 68 + .mise.toml | 14 +- README.md | 1 + tsworkspace/.gitignore | 6 + tsworkspace/.npmrc | 1 + tsworkspace/.oxfmtrc.json | 26 + tsworkspace/apps/console/components.json | 25 + tsworkspace/apps/console/index.html | 12 + tsworkspace/apps/console/package.json | 57 + tsworkspace/apps/console/public/favicon.ico | Bin 0 -> 3870 bytes .../apps/console/src/components/ui/button.tsx | 50 + .../apps/console/src/components/ui/card.tsx | 70 + .../apps/console/src/components/ui/input.tsx | 20 + .../apps/console/src/components/ui/label.tsx | 18 + tsworkspace/apps/console/src/lib/utils.ts | 6 + tsworkspace/apps/console/src/main.tsx | 19 + tsworkspace/apps/console/src/router.tsx | 22 + .../apps/console/src/routes/__root.tsx | 33 + tsworkspace/apps/console/src/routes/index.tsx | 5 + .../console/src/screens/home-screen.test.tsx | 12 + .../apps/console/src/screens/home-screen.tsx | 18 + tsworkspace/apps/console/src/styles.css | 130 + tsworkspace/apps/console/src/test/setup.ts | 1 + tsworkspace/apps/console/tsconfig.json | 27 + tsworkspace/apps/console/tsr.config.json | 3 + tsworkspace/apps/console/vite.config.ts | 16 + tsworkspace/package.json | 19 + tsworkspace/pnpm-lock.yaml | 5644 +++++++++++++++++ tsworkspace/pnpm-workspace.yaml | 3 + tsworkspace/turbo.json | 23 + 30 files changed, 6346 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci-web.yml create mode 100644 tsworkspace/.gitignore create mode 100644 tsworkspace/.npmrc create mode 100644 tsworkspace/.oxfmtrc.json create mode 100644 tsworkspace/apps/console/components.json create mode 100644 tsworkspace/apps/console/index.html create mode 100644 tsworkspace/apps/console/package.json create mode 100644 tsworkspace/apps/console/public/favicon.ico create mode 100644 tsworkspace/apps/console/src/components/ui/button.tsx create mode 100644 tsworkspace/apps/console/src/components/ui/card.tsx create mode 100644 tsworkspace/apps/console/src/components/ui/input.tsx create mode 100644 tsworkspace/apps/console/src/components/ui/label.tsx create mode 100644 tsworkspace/apps/console/src/lib/utils.ts create mode 100644 tsworkspace/apps/console/src/main.tsx create mode 100644 tsworkspace/apps/console/src/router.tsx create mode 100644 tsworkspace/apps/console/src/routes/__root.tsx create mode 100644 tsworkspace/apps/console/src/routes/index.tsx create mode 100644 tsworkspace/apps/console/src/screens/home-screen.test.tsx create mode 100644 tsworkspace/apps/console/src/screens/home-screen.tsx create mode 100644 tsworkspace/apps/console/src/styles.css create mode 100644 tsworkspace/apps/console/src/test/setup.ts create mode 100644 tsworkspace/apps/console/tsconfig.json create mode 100644 tsworkspace/apps/console/tsr.config.json create mode 100644 tsworkspace/apps/console/vite.config.ts create mode 100644 tsworkspace/package.json create mode 100644 tsworkspace/pnpm-lock.yaml create mode 100644 tsworkspace/pnpm-workspace.yaml create mode 100644 tsworkspace/turbo.json diff --git a/.github/workflows/ci-web.yml b/.github/workflows/ci-web.yml new file mode 100644 index 000000000..642524d1a --- /dev/null +++ b/.github/workflows/ci-web.yml @@ -0,0 +1,68 @@ +name: CI Web + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + branches: [main] + paths: + - 'tsworkspace/**' + - '.mise.toml' + - '.github/workflows/ci-web.yml' + +concurrency: + group: ci-web-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + package_json_file: tsworkspace/package.json + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 26 + cache: pnpm + cache-dependency-path: tsworkspace/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: tsworkspace + + - name: Format check + run: pnpm fmt:check + working-directory: tsworkspace + + - name: Lint + run: pnpm lint + working-directory: tsworkspace + + - name: Typecheck + run: pnpm typecheck + working-directory: tsworkspace + + - name: Build + run: pnpm build + working-directory: tsworkspace + + - name: Test + run: pnpm test + working-directory: tsworkspace + + - name: Primitive layer policy + run: pnpm check:primitives + working-directory: tsworkspace diff --git a/.mise.toml b/.mise.toml index 7506ecbd7..837018d85 100644 --- a/.mise.toml +++ b/.mise.toml @@ -20,19 +20,27 @@ run = "docker compose -f devops/docker/compose/compose.yml up -d" [tasks.build] description = "Build all" -depends = ["build:rust", "build:docs"] +depends = ["build:rust", "build:docs", "build:web"] [tasks."build:docs"] dir = "docs" run = "pnpm docs:build" +[tasks."build:web"] +dir = "tsworkspace" +run = "pnpm build" + +[tasks."check:web"] +dir = "tsworkspace" +run = "pnpm fmt:check && pnpm lint && pnpm typecheck" + [tasks."build:rust"] dir = "rsworkspace" run = "cargo build" [tasks.check] -description = "Check Rust" -depends = ["check:rust"] +description = "Check workspaces" +depends = ["check:rust", "check:web"] [tasks."check:rust"] dir = "rsworkspace" diff --git a/README.md b/README.md index 42d523ca5..200f2c2df 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,4 @@ - [`docs/`](docs/) - Documentation website and contribution workflow. - [`rsworkspace/`](rsworkspace/) - Rust components and crate documentation. +- [`tsworkspace/`](tsworkspace/) - TypeScript workspace, including the console webapp. diff --git a/tsworkspace/.gitignore b/tsworkspace/.gitignore new file mode 100644 index 000000000..300176e7e --- /dev/null +++ b/tsworkspace/.gitignore @@ -0,0 +1,6 @@ +coverage +playwright-report +test-results +.tanstack +*.local +**/routeTree.gen.ts diff --git a/tsworkspace/.npmrc b/tsworkspace/.npmrc new file mode 100644 index 000000000..cffe8cdef --- /dev/null +++ b/tsworkspace/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/tsworkspace/.oxfmtrc.json b/tsworkspace/.oxfmtrc.json new file mode 100644 index 000000000..1fef4afd8 --- /dev/null +++ b/tsworkspace/.oxfmtrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "sortImports": {}, + "sortTailwindcss": { + "functions": ["clsx"] + }, + "ignorePatterns": [ + ".next/**", + "dist/**", + "node_modules/**", + "coverage/**", + "storybook-static/**", + "playwright-report/**", + "test-results/**", + "*.min.js", + "pnpm-lock.yaml", + "*.hbs", + "**/routeTree.gen.ts" + ] +} diff --git a/tsworkspace/apps/console/components.json b/tsworkspace/apps/console/components.json new file mode 100644 index 000000000..7f1011e4f --- /dev/null +++ b/tsworkspace/apps/console/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide", + "rtl": false, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/tsworkspace/apps/console/index.html b/tsworkspace/apps/console/index.html new file mode 100644 index 000000000..fd0a05a27 --- /dev/null +++ b/tsworkspace/apps/console/index.html @@ -0,0 +1,12 @@ + + + + + + @trogonai/console + + +
+ + + diff --git a/tsworkspace/apps/console/package.json b/tsworkspace/apps/console/package.json new file mode 100644 index 000000000..30c065f65 --- /dev/null +++ b/tsworkspace/apps/console/package.json @@ -0,0 +1,57 @@ +{ + "name": "@trogonai/console", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "generate-routes": "tsr generate", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run", + "typecheck": "tsr generate && tsc --noEmit", + "check:primitives": "sh -c '! grep -q \"@radix-ui/\" ../../pnpm-lock.yaml'", + "lint": "oxlint ." + }, + "dependencies": { + "@base-ui/react": "1.6.0", + "@tailwindcss/vite": "4.3.2", + "@tanstack/react-devtools": "0.10.8", + "@tanstack/react-form": "1.33.0", + "@tanstack/react-query": "5.101.2", + "@tanstack/react-router": "1.170.17", + "@tanstack/react-router-devtools": "1.167.0", + "@tanstack/react-table": "8.21.3", + "@tanstack/router-plugin": "1.168.19", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "lucide-react": "0.545.0", + "react": "19.2.7", + "react-dom": "19.2.7", + "tailwind-merge": "3.6.0", + "tailwindcss": "4.3.2", + "tw-animate-css": "1.4.0", + "zod": "4.4.3" + }, + "devDependencies": { + "@tanstack/devtools-vite": "0.8.1", + "@tanstack/router-cli": "1.167.18", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@types/node": "22.20.0", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.3", + "jsdom": "28.1.0", + "shadcn": "4.13.0", + "typescript": "6.0.3", + "vite": "8.1.3", + "vitest": "4.1.10" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "lightningcss" + ] + } +} diff --git a/tsworkspace/apps/console/public/favicon.ico b/tsworkspace/apps/console/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/tsworkspace/apps/console/src/components/ui/button.tsx b/tsworkspace/apps/console/src/components/ui/button.tsx new file mode 100644 index 000000000..25aecb672 --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/button.tsx @@ -0,0 +1,50 @@ +import { Button as ButtonPrimitive } from '@base-ui/react/button'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/80', + outline: + 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: + 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50', + destructive: + 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + icon: 'size-8', + 'icon-xs': + "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", + 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', + 'icon-lg': 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +function Button({ + className, + variant = 'default', + size = 'default', + ...props +}: ButtonPrimitive.Props & VariantProps) { + return ; +} + +export { Button, buttonVariants }; diff --git a/tsworkspace/apps/console/src/components/ui/card.tsx b/tsworkspace/apps/console/src/components/ui/card.tsx new file mode 100644 index 000000000..a7e96f7c9 --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/card.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl', + className, + )} + {...props} + /> + ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/tsworkspace/apps/console/src/components/ui/input.tsx b/tsworkspace/apps/console/src/components/ui/input.tsx new file mode 100644 index 000000000..f88cf7a9c --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import { Input as InputPrimitive } from '@base-ui/react/input'; +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { + return ( + + ); +} + +export { Input }; diff --git a/tsworkspace/apps/console/src/components/ui/label.tsx b/tsworkspace/apps/console/src/components/ui/label.tsx new file mode 100644 index 000000000..b0776c37c --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Label({ className, ...props }: React.ComponentProps<'label'>) { + return ( +