From 6f393f5661c91fdeb9f1bcb8c02f3bf7507faae7 Mon Sep 17 00:00:00 2001 From: Gallay Lajos Date: Sun, 29 Mar 2026 13:47:05 +0200 Subject: [PATCH 01/36] improved dog fooding tests --- e2e/dogfooding.spec.ts | 75 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/e2e/dogfooding.spec.ts b/e2e/dogfooding.spec.ts index 973b7a3..b3ea573 100644 --- a/e2e/dogfooding.spec.ts +++ b/e2e/dogfooding.spec.ts @@ -15,22 +15,20 @@ test('DOG FOODING TIME - Create a service that uses the StackCraft GitHub reposi This stack is used to test the dogfooding of the StackCraft application. It is used to test the following features: - Creating a stack + - Adding realistic prerequisites (Node.js, Yarn, Git, environment variables) + - Configuring environment variables (plain text + confidential) + - Adding a local .env file override - Creating a service that uses the StackCraft GitHub repository - Cloning, installing, building and running the service ### Test Steps 1. Create a stack - 2. Create a service that uses the StackCraft GitHub repository + 2. Create a service with prerequisites, env vars, file overrides, and the StackCraft GitHub repository 3. Clone, install, build and run the service 4. Verify that the service is running 5. Verify that the service is logging to the console - 6. Verify that the service is accessible via the browser - 7. Verify that the service is accessible via the API - 8. Remove the service - 9. Remove the stack - 10. Verify that the stack and service are removed - 11. The dog has eaten the food. Woof woof! + 6. The dog has eaten the food. Woof woof! ` @@ -65,6 +63,69 @@ It is used to test the following features: await page.locator('input[name="workingDirectory"]').fill(workingDirectory) await page.locator('input[name="runCommand"]').fill('yarn start:service') + // --- Add realistic prerequisites inline --- + + const prereqForm = page.locator('shade-prerequisite-form') + + const typeSelect = page.locator('shade-select').filter({ has: page.locator('input[name="type"]') }) + + // Prerequisite 1: Node.js >= 22 + await page.locator('button', { hasText: 'Add Prerequisite' }).click() + await expect(prereqForm).toBeVisible() + await prereqForm.locator('input[name="name"]').fill('Node.js >= 22') + await typeSelect.locator('.select-trigger').click() + await typeSelect.locator('.dropdown-item', { hasText: 'Node.js' }).click() + await prereqForm.locator('input[name="minimumVersion"]').fill('22.0.0') + await prereqForm.locator('button', { hasText: 'Add' }).click() + await expect(page.locator('shade-noty-list')).toContainText('"Node.js >= 22" was added.') + + // Prerequisite 2: Yarn >= 4 + await page.locator('button', { hasText: 'Add Prerequisite' }).click() + await expect(prereqForm).toBeVisible() + await prereqForm.locator('input[name="name"]').fill('Yarn >= 4') + await typeSelect.locator('.select-trigger').click() + await typeSelect.locator('.dropdown-item', { hasText: 'Yarn' }).click() + await prereqForm.locator('input[name="minimumVersion"]').fill('4.0.0') + await prereqForm.locator('button', { hasText: 'Add' }).click() + await expect(page.locator('shade-noty-list')).toContainText('"Yarn >= 4" was added.') + + // Prerequisite 3: Git + await page.locator('button', { hasText: 'Add Prerequisite' }).click() + await expect(prereqForm).toBeVisible() + await prereqForm.locator('input[name="name"]').fill('Git') + await typeSelect.locator('.select-trigger').click() + await typeSelect.locator('.dropdown-item', { hasText: 'Git' }).first().click() + await prereqForm.locator('button', { hasText: 'Add' }).click() + await expect(page.locator('shade-noty-list')).toContainText('"Git" was added.') + + // Prerequisite 4: MOCK_API_KEY (plain text environment variable) + await page.locator('button', { hasText: 'Add Prerequisite' }).click() + await expect(prereqForm).toBeVisible() + await prereqForm.locator('input[name="name"]').fill('Mock API Key') + await typeSelect.locator('.select-trigger').click() + await typeSelect.locator('.dropdown-item', { hasText: 'Environment Variable' }).click() + await prereqForm.locator('input[name="variableName"]').fill('MOCK_API_KEY') + await prereqForm.locator('button', { hasText: 'Add' }).click() + await expect(page.locator('shade-noty-list')).toContainText('"Mock API Key" was added.') + + // Prerequisite 5: STACK_CRAFT_ENCRYPTION_KEY (confidential environment variable) + await page.locator('button', { hasText: 'Add Prerequisite' }).click() + await expect(prereqForm).toBeVisible() + await prereqForm.locator('input[name="name"]').fill('Encryption Key') + await typeSelect.locator('.select-trigger').click() + await typeSelect.locator('.dropdown-item', { hasText: 'Environment Variable' }).click() + await prereqForm.locator('input[name="variableName"]').fill('STACK_CRAFT_ENCRYPTION_KEY') + await prereqForm.locator('input[name="isSensitive"]').check() + await prereqForm.locator('button', { hasText: 'Add' }).click() + await expect(page.locator('shade-noty-list')).toContainText('"Encryption Key" was added.') + + // --- Add local .env file override --- + await page.locator('button', { hasText: 'Add Local File' }).click() + await page.locator('input[placeholder="Relative path (e.g. .env.local)"]').fill('.env') + await page + .locator('textarea[placeholder="File content (secret)"]') + .fill('STACK_CRAFT_ENCRYPTION_KEY=e2e-dogfooding-test-key') + // Add StackCraft GitHub repository inline await page.locator('button', { hasText: 'New' }).click() const repoForm = page.locator('shade-github-repo-form') From 27c7d528d09cc9c644f0546ea4ffa874104bb141 Mon Sep 17 00:00:00 2001 From: Gallay Lajos Date: Sun, 29 Mar 2026 15:31:03 +0200 Subject: [PATCH 02/36] SOLID refactors, documentation updates --- CONTRIBUTING.md | 135 +++++++ README.md | 108 +++++- common/schemas/identity-api.json | 15 +- common/schemas/install-api.json | 6 +- common/schemas/services-api.json | 24 +- common/schemas/system-api.json | 72 ++-- common/src/apis/identity.ts | 8 + common/src/apis/install.ts | 2 + common/src/apis/services.ts | 8 + common/src/apis/system.ts | 15 + docs/troubleshooting.md | 143 +++++++ frontend/README.md | 53 ++- frontend/src/components/body.tsx | 4 +- frontend/src/components/header.tsx | 1 + frontend/src/components/layout.tsx | 39 +- frontend/src/components/sidebar.tsx | 2 +- frontend/src/pages/not-found.tsx | 23 ++ .../utils/apply-client-find-options.spec.ts | 79 ++++ package.json | 2 +- service/src/app-models/data-store/models.ts | 137 +++++++ .../app-models/data-store/setup-data-store.ts | 131 +------ .../actions/service-lifecycle-action.ts | 4 +- .../check-env-availability-action.spec.ts | 76 ++-- .../actions/health-check-action.spec.ts | 48 +++ .../system/actions/health-check-action.ts | 25 ++ .../system/setup-system-rest-api.ts | 4 + service/src/middleware/request-logger.ts | 30 ++ service/src/service.ts | 9 +- service/src/services/process-manager.ts | 359 ++++-------------- service/src/services/process-runner.ts | 124 ++++++ service/src/services/service-env-resolver.ts | 79 ++++ .../services/service-graph-resolver.spec.ts | 98 +++++ .../src/services/service-graph-resolver.ts | 57 +++ service/src/shutdown-handler.ts | 10 +- service/src/utils/domain-error.spec.ts | 46 +++ service/src/utils/domain-error.ts | 36 ++ service/src/utils/resolve-path.spec.ts | 32 ++ vitest.config.mts | 7 +- 38 files changed, 1526 insertions(+), 525 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/troubleshooting.md create mode 100644 frontend/src/pages/not-found.tsx create mode 100644 frontend/src/utils/apply-client-find-options.spec.ts create mode 100644 service/src/app-models/data-store/models.ts create mode 100644 service/src/app-models/system/actions/health-check-action.spec.ts create mode 100644 service/src/app-models/system/actions/health-check-action.ts create mode 100644 service/src/middleware/request-logger.ts create mode 100644 service/src/services/process-runner.ts create mode 100644 service/src/services/service-env-resolver.ts create mode 100644 service/src/services/service-graph-resolver.spec.ts create mode 100644 service/src/services/service-graph-resolver.ts create mode 100644 service/src/utils/domain-error.spec.ts create mode 100644 service/src/utils/domain-error.ts create mode 100644 service/src/utils/resolve-path.spec.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6b07069 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,135 @@ +# Contributing to Stack Craft + +## Development Setup + +1. Clone the repository and install dependencies: + +```bash +git clone https://github.com/furystack/stack-craft.git +cd stack-craft +yarn +``` + +2. Start PostgreSQL: + +```bash +docker compose up -d +``` + +3. Create your environment file: + +```bash +cp .env.example .env +``` + +4. Start the backend and frontend in separate terminals: + +```bash +yarn start:service # Terminal 1 +yarn start:frontend # Terminal 2 +``` + +## Project Structure + +``` +stack-craft/ +├── common/ # Shared API type definitions, models, and generated schemas +├── frontend/ # Shades-based SPA (Vite dev server on :8080) +├── service/ # FuryStack REST backend (Node.js on :9090) +├── e2e/ # Playwright end-to-end tests +└── docs/ # Documentation +``` + +## Code Style + +The project uses **ESLint** and **Prettier** for code quality and formatting. Both run automatically on staged files via Husky pre-commit hooks. + +```bash +yarn lint # Run ESLint +yarn format # Format all files with Prettier +yarn format:check # Check formatting without modifying files +``` + +Key conventions: + +- **File names:** kebab-case (e.g. `service-pipeline.ts`) +- **Components:** PascalCase exports in kebab-case files +- **Types:** Prefer `type` over `interface` +- **No `any`:** Use `unknown`, generics, or union types instead + +See `.cursor/rules/` for detailed coding guidelines. + +## Testing + +```bash +yarn test # Run Vitest unit tests with coverage +yarn test:e2e # Run Playwright E2E tests (requires running DB) +``` + +- Unit tests are co-located with source files (`*.spec.ts`) +- Coverage thresholds are enforced in `vitest.config.mts` +- Add tests for new functionality before submitting a PR + +## Schema Generation + +When you modify API types in `common/src/apis/` or models in `common/src/models/`, regenerate the JSON schemas: + +```bash +yarn build # Must build common first +yarn create-schemas # Regenerate schemas +``` + +## Versioning and Changelogs + +This project uses [Yarn's release workflow](https://yarnpkg.com/features/release-workflow) for version management and changelogs. Both are required before opening a PR. + +### 1. Bump versions + +Decide which packages need a version bump and at what level (patch/minor/major): + +```bash +yarn bumpVersions # Interactive prompt to select packages and bump type +``` + +This creates version manifest files that Yarn tracks. You can verify the state with: + +```bash +yarn version check # Validates that version bumps are configured +``` + +### 2. Create changelog drafts + +Generate changelog draft files for each bumped package: + +```bash +yarn changelog create # Creates .yarn/changelogs/{package}.{id}.md files +yarn changelog create -f # Force-recreate (useful if drafts are stale) +``` + +### 3. Fill the changelog entries + +Edit the generated `.yarn/changelogs/*.md` files. Each file has section placeholders -- fill in the relevant ones describing what changed and why. Write for end users, not as a git log. + +### 4. Validate + +```bash +yarn changelog check # Validates entries are filled and match the version type +``` + +### Applying releases (maintainers only) + +When merging to a release branch, the release pipeline runs: + +```bash +yarn applyReleaseChanges # Applies version bumps, merges changelogs, and formats +``` + +## Pull Request Process + +1. Create a feature branch from the latest `develop` +2. Make your changes with tests +3. Ensure `yarn build`, `yarn lint`, and `yarn test` all pass +4. Bump versions (`yarn bumpVersions`) and fill changelog entries (`yarn changelog create`, then edit the drafts) +5. Verify with `yarn changelog check` +6. Open a PR against `develop` +7. PRs must pass CI checks (build, lint, test, E2E, changelog check, version check) diff --git a/README.md b/README.md index 29d57ca..61f42f5 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,108 @@ # Stack Craft -Example web app with common type API definitions, a FuryStack-based backend service and a Shades-based single page application. +A web application for managing development stacks, services, prerequisites, and repositories. Built with a [FuryStack](https://github.com/furystack/furystack) backend and a [Shades](https://github.com/furystack/furystack/tree/develop/packages/shades)-based single page application. -# Usage +## Features -1. Clone the repository -1. Install the dependencies with `yarn` -1. Start PostgreSQL (e.g. `docker compose up -d`) -1. Copy `.env.example` to `.env` and adjust if needed -1. Start the frontend and the backend service with `yarn start` (you can stop / start them individually, check the NPM scripts for further details) +- **Stack management** -- group related services, repositories, and prerequisites into stacks +- **Service lifecycle** -- start, stop, restart, build, and install services with real-time log streaming +- **Prerequisite checks** -- define and evaluate system prerequisites before running services +- **Repository integration** -- clone and manage GitHub repositories with branch switching +- **Environment variables** -- manage per-stack and per-service environment configuration with encrypted secrets +- **Import / Export** -- share stack configurations across environments +- **MCP server** -- interact with Stack Craft via [Model Context Protocol](https://modelcontextprotocol.io/) for AI assistant integration + +## Prerequisites + +- [Node.js](https://nodejs.org/) >= 22 +- [Yarn](https://yarnpkg.com/) 4 (included via `packageManager` in `package.json`) +- [Docker](https://www.docker.com/) (for PostgreSQL, or provide your own instance) + +## Getting Started + +```bash +# Clone and install +git clone https://github.com/furystack/stack-craft.git +cd stack-craft +yarn + +# Start PostgreSQL +docker compose up -d + +# Create your environment file +cp .env.example .env + +# Start the backend and frontend +yarn start:service # Backend on http://localhost:9090 +yarn start:frontend # Frontend on http://localhost:8080 +``` + +Open http://localhost:8080 in your browser. On first launch, the installer wizard will guide you through creating an admin account. + +## Configuration + +Environment variables can be set in the `.env` file at the project root. + +| Variable | Default | Description | +| ---------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | +| `DATABASE_URL` | `postgres://stackcraft:stackcraft@localhost:5433/stackcraft` | PostgreSQL connection string | +| `APP_SERVICE_PORT` | `9090` | Backend HTTP server port | +| `MCP_PORT` | `9091` | MCP server port | +| `STACK_CRAFT_ENCRYPTION_KEY` | *(auto-generated)* | Base64-encoded 256-bit key for encrypting sensitive values. If unset, a key file is created in `~/.stack-craft/` | ## Docker -When running the Docker image, pass the `DATABASE_URL` environment variable: +Build and run the Docker image: ```bash -docker run -e DATABASE_URL=postgres://user:password@host:5433/stackcraft furystack/stack-craft +docker build -t stack-craft . + +docker run -p 9090:9090 \ + -e DATABASE_URL=postgres://user:password@host:5432/stackcraft \ + stack-craft ``` -# Testing +The pre-built image is also available on Docker Hub: + +```bash +docker run -p 9090:9090 \ + -e DATABASE_URL=postgres://user:password@host:5432/stackcraft \ + furystack/stack-craft +``` + +## Project Structure + +``` +stack-craft/ +├── common/ # Shared API type definitions and models +├── frontend/ # Shades-based SPA (Vite) +├── service/ # FuryStack REST backend (Node.js) +└── e2e/ # Playwright end-to-end tests +``` + +## Testing + +```bash +# Unit tests (Vitest) +yarn test + +# End-to-end tests (requires running PostgreSQL + DATABASE_URL) +yarn test:e2e +``` + +## Available Scripts + +| Script | Description | +| --------------------- | --------------------------------------------- | +| `yarn start:service` | Start the backend service | +| `yarn start:frontend` | Start the Vite dev server for the frontend | +| `yarn build` | Build all packages | +| `yarn test` | Run Vitest unit tests | +| `yarn test:e2e` | Run Playwright E2E tests | +| `yarn lint` | Run ESLint | +| `yarn format` | Format code with Prettier | +| `yarn create-schemas` | Regenerate JSON schemas from TypeScript types | + +## License -- You can execute the example Vitest tests with `yarn test` -- You can execute E2E tests with `yarn test:e2e` (requires a running PostgreSQL instance and `DATABASE_URL` set) +[GPL-2.0-only](LICENSE) diff --git a/common/schemas/identity-api.json b/common/schemas/identity-api.json index 03ed913..228c6db 100644 --- a/common/schemas/identity-api.json +++ b/common/schemas/identity-api.json @@ -16,7 +16,8 @@ } }, "required": ["result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Checks whether the current session is authenticated" }, "GetCurrentUserAction": { "type": "object", @@ -26,7 +27,8 @@ } }, "required": ["result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Returns the currently authenticated user's profile" }, "User": { "type": "object", @@ -65,7 +67,8 @@ } }, "required": ["result", "body"], - "additionalProperties": false + "additionalProperties": false, + "description": "Authenticates with username/password and returns the user profile" }, "LogoutAction": { "type": "object", @@ -73,7 +76,8 @@ "result": {} }, "required": ["result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Ends the current session" }, "PasswordResetAction": { "type": "object", @@ -103,7 +107,8 @@ } }, "required": ["result", "body"], - "additionalProperties": false + "additionalProperties": false, + "description": "Changes the current user's password" }, "IdentityApi": { "type": "object", diff --git a/common/schemas/install-api.json b/common/schemas/install-api.json index c46e57d..4ec367f 100644 --- a/common/schemas/install-api.json +++ b/common/schemas/install-api.json @@ -23,7 +23,8 @@ } }, "required": ["result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Returns whether the application needs initial setup or is already installed" }, "InstallAction": { "type": "object", @@ -53,7 +54,8 @@ } }, "required": ["result", "body"], - "additionalProperties": false + "additionalProperties": false, + "description": "Performs initial application setup and creates the first admin user" }, "InstallApi": { "type": "object", diff --git a/common/schemas/services-api.json b/common/schemas/services-api.json index c623823..481be21 100644 --- a/common/schemas/services-api.json +++ b/common/schemas/services-api.json @@ -270,7 +270,8 @@ } }, "required": ["result", "body"], - "additionalProperties": false + "additionalProperties": false, + "description": "Creates a new service definition with optional configuration" }, "ServiceView": { "type": "object", @@ -689,7 +690,8 @@ } }, "required": ["url", "result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Triggers a lifecycle action (start, stop, restart, install, build, pull, setup, update) on a service" }, "ApplyServiceFilesEndpoint": { "type": "object", @@ -734,7 +736,8 @@ } }, "required": ["url", "body", "result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Writes shared and local service files to disk, optionally for a single file" }, "ServiceLogsEndpoint": { "type": "object", @@ -779,7 +782,8 @@ } }, "required": ["url", "query", "result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Retrieves recent log entries for a service, optionally filtered by process UID or search text" }, "ServiceLogEntry": { "type": "object", @@ -832,7 +836,8 @@ } }, "required": ["url", "result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Deletes all stored log entries for a service" }, "ServiceHistoryEndpoint": { "type": "object", @@ -871,7 +876,8 @@ } }, "required": ["url", "query", "result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Retrieves the state transition history for a service" }, "ServiceStateHistory": { "type": "object", @@ -990,7 +996,8 @@ } }, "required": ["url", "result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Lists local and remote branches for the service's linked repository" }, "ServiceCheckoutEndpoint": { "type": "object", @@ -1030,7 +1037,8 @@ } }, "required": ["url", "body", "result"], - "additionalProperties": false + "additionalProperties": false, + "description": "Switches the service's repository to a different branch" }, "ServicesApi": { "type": "object", diff --git a/common/schemas/system-api.json b/common/schemas/system-api.json index af53375..b65a811 100644 --- a/common/schemas/system-api.json +++ b/common/schemas/system-api.json @@ -1,6 +1,39 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "HealthCheckResult": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["ok", "degraded"] + }, + "uptime": { + "type": "number" + }, + "version": { + "type": "string" + }, + "database": { + "type": "string", + "enum": ["connected", "disconnected"] + } + }, + "required": ["status", "uptime", "version", "database"], + "additionalProperties": false, + "description": "Backend health status including uptime and database connectivity" + }, + "HealthCheckEndpoint": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/HealthCheckResult" + } + }, + "required": ["result"], + "additionalProperties": false, + "description": "Returns the current health status of the service" + }, "CheckEnvAvailabilityEndpoint": { "type": "object", "properties": { @@ -25,40 +58,21 @@ } }, "required": ["result", "body"], - "additionalProperties": false + "additionalProperties": false, + "description": "Checks whether the specified environment variables are set on the host system" }, "SystemApi": { "type": "object", "properties": { "GET": { "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "result": {}, - "url": {}, - "query": {}, - "body": {}, - "headers": {}, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "deprecated": { - "type": "boolean" - }, - "summary": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["result"], - "additionalProperties": false - } + "properties": { + "/system/health": { + "$ref": "#/definitions/HealthCheckEndpoint" + } + }, + "required": ["/system/health"], + "additionalProperties": false }, "POST": { "type": "object", @@ -281,7 +295,7 @@ } } }, - "required": ["POST"], + "required": ["GET", "POST"], "additionalProperties": false } } diff --git a/common/src/apis/identity.ts b/common/src/apis/identity.ts index 8e1039a..c8ab9b6 100644 --- a/common/src/apis/identity.ts +++ b/common/src/apis/identity.ts @@ -1,11 +1,19 @@ import type { RestApi } from '@furystack/rest' import type { User } from '../models/user.js' +/** Checks whether the current session is authenticated */ export type IsAuthenticatedAction = { result: { isAuthenticated: boolean } } + +/** Returns the currently authenticated user's profile */ export type GetCurrentUserAction = { result: User } + +/** Authenticates with username/password and returns the user profile */ export type LoginAction = { result: User; body: { username: string; password: string } } + +/** Ends the current session */ export type LogoutAction = { result: unknown } +/** Changes the current user's password */ export type PasswordResetAction = { result: { success: boolean } body: { diff --git a/common/src/apis/install.ts b/common/src/apis/install.ts index 7823211..d943ec7 100644 --- a/common/src/apis/install.ts +++ b/common/src/apis/install.ts @@ -6,8 +6,10 @@ export type InstallStateResponse = { state: InstallState } +/** Returns whether the application needs initial setup or is already installed */ export type GetServiceStatusAction = { result: InstallStateResponse } +/** Performs initial application setup and creates the first admin user */ export type InstallAction = { result: { success: boolean }; body: { username: string; password: string } } export interface InstallApi extends RestApi { diff --git a/common/src/apis/services.ts b/common/src/apis/services.ts index b6b8aff..f78f5b9 100644 --- a/common/src/apis/services.ts +++ b/common/src/apis/services.ts @@ -10,6 +10,7 @@ export type ServiceDefinitionWritableFields = Omit export type ServiceWritableFields = ServiceDefinitionWritableFields & Omit +/** Creates a new service definition with optional configuration */ export type PostServiceEndpoint = { result: ServiceView body: WithOptionalId @@ -17,36 +18,43 @@ export type PostServiceEndpoint = { export type PatchServiceEndpoint = PatchEndpoint +/** Triggers a lifecycle action (start, stop, restart, install, build, pull, setup, update) on a service */ export type ServiceActionEndpoint = { url: { id: string }; result: { success: boolean; serviceId: string } } +/** Writes shared and local service files to disk, optionally for a single file */ export type ApplyServiceFilesEndpoint = { url: { id: string } body: { relativePath?: string } result: { success: boolean; serviceId: string; applied: string[] } } +/** Retrieves recent log entries for a service, optionally filtered by process UID or search text */ export type ServiceLogsEndpoint = { url: { id: string } query: { lines?: number; processUid?: string; search?: string } result: { entries: ServiceLogEntry[] } } +/** Deletes all stored log entries for a service */ export type ClearServiceLogsEndpoint = { url: { id: string } result: { success: boolean } } +/** Retrieves the state transition history for a service */ export type ServiceHistoryEndpoint = { url: { id: string } query: { limit?: number } result: { entries: ServiceStateHistory[] } } +/** Lists local and remote branches for the service's linked repository */ export type ServiceBranchesEndpoint = { url: { id: string } result: { currentBranch: string; local: string[]; remote: string[] } } +/** Switches the service's repository to a different branch */ export type ServiceCheckoutEndpoint = { url: { id: string } body: { branch: string } diff --git a/common/src/apis/system.ts b/common/src/apis/system.ts index 9e97ed1..a5199a5 100644 --- a/common/src/apis/system.ts +++ b/common/src/apis/system.ts @@ -1,11 +1,26 @@ import type { RestApi } from '@furystack/rest' +/** Backend health status including uptime and database connectivity */ +export type HealthCheckResult = { + status: 'ok' | 'degraded' + uptime: number + version: string + database: 'connected' | 'disconnected' +} + +/** Returns the current health status of the service */ +export type HealthCheckEndpoint = { result: HealthCheckResult } + +/** Checks whether the specified environment variables are set on the host system */ export type CheckEnvAvailabilityEndpoint = { result: Record body: { variableNames: string[] } } export interface SystemApi extends RestApi { + GET: { + '/system/health': HealthCheckEndpoint + } POST: { '/system/check-env-availability': CheckEnvAvailabilityEndpoint } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..9281187 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,143 @@ +# Troubleshooting Guide + +## Common Startup Issues + +### Database Connection Failed + +**Symptom:** The service exits with a PostgreSQL connection error on startup. + +**Causes:** + +- PostgreSQL is not running +- `DATABASE_URL` is missing or incorrect in the `.env` file +- The database or user does not exist + +**Resolution:** + +1. Ensure PostgreSQL is running: `docker compose up -d` +2. Verify the connection string in `.env` matches your PostgreSQL setup +3. Default connection string: `postgres://stackcraft:stackcraft@localhost:5433/stackcraft` + +### Port Already in Use + +**Symptom:** `EADDRINUSE` error on startup. + +**Causes:** + +- Another instance of the service or another application is using port 9090 (or 8080 for frontend) + +**Resolution:** + +1. Stop the other process using the port +2. Or set a different port via the `APP_SERVICE_PORT` environment variable in `.env` + +### Missing Environment File + +**Symptom:** Service starts but cannot connect to the database. + +**Resolution:** + +1. Copy the example file: `cp .env.example .env` +2. Adjust values if your PostgreSQL setup differs from defaults + +--- + +## Service Management Issues + +### Service Stuck in "Starting" or "Installing" State + +**Symptom:** A managed service shows a stale status that never resolves. + +**Cause:** The backend was restarted while a service operation was in progress. The state was persisted but the process no longer exists. + +**Resolution:** On startup, Stack Craft automatically reconciles stale states. Restart the Stack Craft backend and the stuck service should return to a normal state. + +### Clone/Pull Fails + +**Symptom:** Cloning or pulling a repository fails with a git error. + +**Common causes:** + +- The repository URL is invalid or inaccessible +- Git is not installed on the system +- Authentication is required but not configured +- The target directory has conflicting content + +**Resolution:** + +1. Verify the repository URL in the repository settings +2. Ensure `git` is available in the system `PATH` +3. For private repositories, ensure SSH keys or credentials are configured +4. Check the service logs for the specific git error message + +### Prerequisite Check Fails + +**Symptom:** A prerequisite shows as "not satisfied" even though the tool is installed. + +**Common causes:** + +- The command runs in a restricted environment without access to the full `PATH` +- The expected version pattern does not match the installed version + +**Resolution:** + +1. Check the prerequisite configuration (command, expected output pattern) +2. Verify the tool is accessible from a clean shell (not just your user profile) +3. Review the check output in the prerequisite details for the actual command output + +--- + +## Viewing Logs + +### Service Logs (Managed Services) + +Service stdout/stderr is captured in-memory and available via: + +- **UI:** Navigate to the service detail page and click the "Logs" tab +- **API:** `GET /api/services/:id/logs?lines=300&search=error` +- **MCP:** Use the `get_service_logs` tool + +**Note:** Service logs are stored in-memory with a limit of 50,000 entries per service. Logs are lost when the Stack Craft backend restarts. + +### Application Logs (Stack Craft Backend) + +The backend logs to stdout via the FuryStack `VerboseConsoleLogger`. In production, pipe stdout to your preferred log aggregator: + +```bash +# Docker +docker logs + +# systemd +journalctl -u stack-craft + +# File redirect +yarn start:service >> /var/log/stack-craft.log 2>&1 +``` + +### Health Check + +Use the health endpoint to verify the service is running and the database is connected: + +```bash +curl http://localhost:9090/api/system/health +# {"status":"ok","uptime":12345,"version":"1.0.3","database":"connected"} +``` + +--- + +## Environment Variables Reference + +| Variable | Default | Description | +| ---------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | +| `DATABASE_URL` | `postgres://stackcraft:stackcraft@localhost:5433/stackcraft` | PostgreSQL connection string | +| `APP_SERVICE_PORT` | `9090` | Backend HTTP server port | +| `MCP_PORT` | `9091` | MCP server port | +| `STACK_CRAFT_ENCRYPTION_KEY` | *(auto-generated)* | Base64-encoded 256-bit key for encrypting sensitive values. If unset, a key file is created in `~/.stack-craft/` | + +--- + +## Getting Help + +- Check the service logs for detailed error messages +- Use the MCP server (`localhost:9091`) with an AI assistant for interactive troubleshooting +- File issues at https://github.com/furystack/stack-craft/issues diff --git a/frontend/README.md b/frontend/README.md index 4256a14..2d92cd1 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,26 +1,47 @@ -# Introduction +# Stack Craft Frontend -TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project. +Shades-based single page application for Stack Craft. -# Getting Started +## Tech Stack -TODO: Guide users through getting your code up and running on their own system. In this section you can talk about: +- **[Shades](https://github.com/furystack/furystack/tree/develop/packages/shades)** -- FuryStack's web component framework +- **[Vite](https://vite.dev/)** -- Build tool and dev server +- **TypeScript** -- Strict mode enabled -1. Installation process -2. Software dependencies -3. Latest releases -4. API references +## Development -# Build and Test +```bash +# Start the dev server (port 8080) +yarn start -TODO: Describe and show how to build your code and run the tests. +# Build for production +yarn build +``` -# Contribute +The dev server proxies API requests to the backend on port 9090. Make sure the backend is running before starting the frontend. -TODO: Explain how other users and developers can contribute to make your code better. +## Project Structure -If you want to learn more about creating good readme files then refer the following [guidelines](https://www.visualstudio.com/en-us/docs/git/create-a-readme). You can also seek inspiration from the below readme files: +``` +frontend/src/ +├── components/ # Reusable UI components +│ ├── entity-forms/ # Form components for creating/editing entities +│ └── ... +├── pages/ # Page-level components (one per route) +│ ├── dashboard/ +│ ├── services/ +│ ├── stacks/ +│ ├── wizards/ +│ └── ... +├── services/ # Business logic and API client services +│ └── api-clients/ # Typed REST API clients +├── utils/ # Utility functions +└── index.tsx # Application entry point +``` -- [ASP.NET Core](https://github.com/aspnet/Home) -- [Visual Studio Code](https://github.com/Microsoft/vscode) -- [Chakra Core](https://github.com/Microsoft/ChakraCore) +## Key Patterns + +- **Dependency injection:** Services are accessed via `injector.getInstance(ServiceClass)` +- **Reactive state:** `useObservable` subscribes to `ObservableValue` instances from services +- **Entity sync:** Real-time data updates via WebSocket through `@furystack/entity-sync-client` +- **Routing:** `NestedRouter` with typed routes defined in `components/app-routes.tsx` diff --git a/frontend/src/components/body.tsx b/frontend/src/components/body.tsx index 21de290..908d215 100644 --- a/frontend/src/components/body.tsx +++ b/frontend/src/components/body.tsx @@ -1,7 +1,7 @@ import type { Injector } from '@furystack/inject' import { createComponent, NestedRouter, Shade } from '@furystack/shades' -import { Dashboard } from '../pages/dashboard/index.js' import { Init, Offline } from '../pages/index.js' +import { NotFound } from '../pages/not-found.js' import { SessionService } from '../services/session.js' import { appRoutes } from './app-routes.js' @@ -15,7 +15,7 @@ export const Body = Shade<{ style?: Partial; injector?: Inj {(() => { switch (sessionState) { case 'authenticated': - return } /> + return } /> case 'offline': return default: diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx index 0f6ac87..6471a5f 100644 --- a/frontend/src/components/header.tsx +++ b/frontend/src/components/header.tsx @@ -49,6 +49,7 @@ export const Header = Shade({ size="small" onclick={() => injector.getInstance(SessionService).logout()} startIcon={} + title="Sign out of your account" > Log Out diff --git a/frontend/src/components/layout.tsx b/frontend/src/components/layout.tsx index 66f3698..ff7e4f3 100644 --- a/frontend/src/components/layout.tsx +++ b/frontend/src/components/layout.tsx @@ -43,6 +43,43 @@ export const Layout = Shade({ return } + if (installState === 'error') { + return ( +
+

Unable to Connect

+

+ Could not reach the Stack Craft service. Please check that the backend is running and try again. +

+ +
+ ) + } + if (installState === 'needsInstall') { return (
@@ -66,7 +103,7 @@ export const Layout = Shade({ return (
- +
) diff --git a/frontend/src/components/sidebar.tsx b/frontend/src/components/sidebar.tsx index 1047d8e..17adf1b 100644 --- a/frontend/src/components/sidebar.tsx +++ b/frontend/src/components/sidebar.tsx @@ -255,7 +255,7 @@ export const Sidebar = Shade<{ injector?: Injector }>({ ) as StackView[] return ( -