From 89eddd2ad05bc7b1a37353f6989a3d5797903397 Mon Sep 17 00:00:00 2001 From: Johnny 5 Date: Sun, 7 Jun 2026 19:23:26 +0000 Subject: [PATCH] Issue 17: StartOS 0.4.0 packaging - Dockerfile: multi-stage build (Node frontend + Python backend) - StartOS manifest, interfaces, backups, version graph - start.sh: auto-generates session secret, creates first approver on boot - Backend serves built frontend as SPA in production - Single data volume for SQLite DB, platform backups cover it --- .gitignore | 4 + LICENSE | 2 +- backend/ten31portal/main.py | 15 + deploy/.gitignore | 2 + deploy/Dockerfile | 40 +++ deploy/icon.png | Bin 0 -> 856 bytes deploy/package-lock.json | 345 +++++++++++++++++++++ deploy/package.json | 17 + deploy/start.sh | 38 +++ deploy/startos/actions/index.ts | 4 + deploy/startos/backups.ts | 5 + deploy/startos/dependencies.ts | 3 + deploy/startos/index.ts | 8 + deploy/startos/init/index.ts | 16 + deploy/startos/install/versionGraph.ts | 7 + deploy/startos/install/versions/index.ts | 2 + deploy/startos/install/versions/v_0_1_0.ts | 16 + deploy/startos/interfaces.ts | 24 ++ deploy/startos/main.ts | 39 +++ deploy/startos/manifest/index.ts | 32 ++ deploy/startos/sdk.ts | 4 + deploy/tsconfig.json | 14 + 22 files changed, 636 insertions(+), 1 deletion(-) create mode 100644 deploy/.gitignore create mode 100644 deploy/Dockerfile create mode 100644 deploy/icon.png create mode 100644 deploy/package-lock.json create mode 100644 deploy/package.json create mode 100644 deploy/start.sh create mode 100644 deploy/startos/actions/index.ts create mode 100644 deploy/startos/backups.ts create mode 100644 deploy/startos/dependencies.ts create mode 100644 deploy/startos/index.ts create mode 100644 deploy/startos/init/index.ts create mode 100644 deploy/startos/install/versionGraph.ts create mode 100644 deploy/startos/install/versions/index.ts create mode 100644 deploy/startos/install/versions/v_0_1_0.ts create mode 100644 deploy/startos/interfaces.ts create mode 100644 deploy/startos/main.ts create mode 100644 deploy/startos/manifest/index.ts create mode 100644 deploy/startos/sdk.ts create mode 100644 deploy/tsconfig.json diff --git a/.gitignore b/.gitignore index 0a93a42..399e9e1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ frontend/dist/ # IDE .vscode/ .idea/ + +# Deploy +deploy/node_modules/ +deploy/javascript/ diff --git a/LICENSE b/LICENSE index 86a7680..5fbef5c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 smallblocks +Copyright (c) 2026 Ten31 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/backend/ten31portal/main.py b/backend/ten31portal/main.py index ae2d05f..70f59cf 100644 --- a/backend/ten31portal/main.py +++ b/backend/ten31portal/main.py @@ -1,9 +1,13 @@ """Ten31Portal FastAPI application.""" +import os from contextlib import asynccontextmanager +from pathlib import Path from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware +from starlette.responses import FileResponse from ten31portal.config import SESSION_SECRET from ten31portal.db_init import run_migrations @@ -38,6 +42,17 @@ def health() -> dict[str, str]: return {"status": "ok"} +# Serve built frontend in production (when static/ dir exists next to the app) +_static_dir = Path(__file__).resolve().parent.parent / "static" +if _static_dir.is_dir(): + @app.get("/{path:path}") + async def serve_spa(path: str): + file = _static_dir / path + if file.is_file(): + return FileResponse(file) + return FileResponse(_static_dir / "index.html") + + def cli() -> None: import uvicorn uvicorn.run("ten31portal.main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000..30d033e --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +javascript/ diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..25a2af6 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,40 @@ +# --- Frontend build stage --- +FROM node:20-slim AS frontend-build + +WORKDIR /build/frontend +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --include=dev +COPY frontend/ ./ +RUN npx vite build + +# --- Backend stage --- +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Python deps +COPY backend/pyproject.toml ./ +COPY backend/ten31portal/ ./ten31portal/ +COPY backend/alembic/ ./alembic/ +COPY backend/alembic.ini ./ +RUN pip install --no-cache-dir . + +# Copy built frontend +COPY --from=frontend-build /build/frontend/dist ./static/ + +# Startup script +COPY deploy/start.sh ./start.sh +RUN chmod +x ./start.sh + +# Data volume mount point +RUN mkdir -p /data + +ENV TEN31_DB_PATH=/data/portal.db +ENV TEN31_SESSION_SECRET=change-me + +EXPOSE 8000 + +CMD ["./start.sh"] diff --git a/deploy/icon.png b/deploy/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..396d542bb3984e92926c325523282a08256ffd1a GIT binary patch literal 856 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJD{c~2L|kcv5PuP8DyCV&_iK3oRo7zR&QKbLh* G2~7Yk?tza0 literal 0 HcmV?d00001 diff --git a/deploy/package-lock.json b/deploy/package-lock.json new file mode 100644 index 0000000..66005fd --- /dev/null +++ b/deploy/package-lock.json @@ -0,0 +1,345 @@ +{ + "name": "ten31portal-startos", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ten31portal-startos", + "version": "0.1.0", + "dependencies": { + "@start9labs/start-sdk": "^0.4.0-beta.58" + }, + "devDependencies": { + "@types/node": "^22.19.0", + "@vercel/ncc": "^0.38.4", + "typescript": "^5.9.3" + } + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@start9labs/start-sdk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.4.0.tgz", + "integrity": "sha512-PFfO7tV9nzQFZL3KXaZyf16C5VZtM+dCDlRhLHpmwssTKtcjyCEhBrB9locuS2yFqu69rj+5kLFzCWDHeRRibg==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@noble/curves": "^1.8.2", + "@noble/hashes": "^1.7.2", + "@types/ini": "^4.1.1", + "deep-equality-data-structures": "^2.0.0", + "fast-xml-parser": "^5.5.6", + "ini": "^5.0.0", + "isomorphic-fetch": "^3.0.0", + "mime": "^4.0.7", + "yaml": "^2.7.1", + "zod": "^4.3.6", + "zod-deep-partial": "^1.2.0" + } + }, + "node_modules/@types/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz", + "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/deep-equality-data-structures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz", + "integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==", + "license": "MIT", + "dependencies": { + "object-hash": "^3.0.0" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-deep-partial": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz", + "integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==", + "license": "MIT", + "peerDependencies": { + "zod": "^4.1.13" + } + } + } +} diff --git a/deploy/package.json b/deploy/package.json new file mode 100644 index 0000000..15254e8 --- /dev/null +++ b/deploy/package.json @@ -0,0 +1,17 @@ +{ + "name": "ten31portal-startos", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "^0.4.0-beta.58" + }, + "devDependencies": { + "@types/node": "^22.19.0", + "@vercel/ncc": "^0.38.4", + "typescript": "^5.9.3" + } +} diff --git a/deploy/start.sh b/deploy/start.sh new file mode 100644 index 0000000..e40650a --- /dev/null +++ b/deploy/start.sh @@ -0,0 +1,38 @@ +#!/bin/sh +set -e + +# Generate session secret if not set or default +if [ "$TEN31_SESSION_SECRET" = "change-me" ]; then + if [ -f /data/.session-secret ]; then + export TEN31_SESSION_SECRET=$(cat /data/.session-secret) + else + export TEN31_SESSION_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))") + echo "$TEN31_SESSION_SECRET" > /data/.session-secret + chmod 600 /data/.session-secret + fi +fi + +# Create first approver on first boot if no users exist +if [ ! -f /data/.initialized ]; then + echo "First boot: running migrations and creating default approver..." + + # Set default credentials (user can change via CLI later) + ADMIN_NAME="${TEN31_ADMIN_NAME:-Admin}" + ADMIN_EMAIL="${TEN31_ADMIN_EMAIL:-admin@ten31.com}" + ADMIN_PASSWORD="${TEN31_ADMIN_PASSWORD:-changeme}" + + python3 -m ten31portal.cli create-user \ + --name "$ADMIN_NAME" \ + --email "$ADMIN_EMAIL" \ + --role approver \ + --password "$ADMIN_PASSWORD" || true + + touch /data/.initialized + echo "Default approver created: $ADMIN_EMAIL" +fi + +# Serve frontend static files from the backend +exec uvicorn ten31portal.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-level info diff --git a/deploy/startos/actions/index.ts b/deploy/startos/actions/index.ts new file mode 100644 index 0000000..f06c5d9 --- /dev/null +++ b/deploy/startos/actions/index.ts @@ -0,0 +1,4 @@ +import { sdk } from '../sdk' + +// No custom actions for v1. Config is handled at install time. +export const actions = sdk.Actions.of() diff --git a/deploy/startos/backups.ts b/deploy/startos/backups.ts new file mode 100644 index 0000000..0a90b1e --- /dev/null +++ b/deploy/startos/backups.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const { createBackup, restoreInit } = sdk.setupBackups( + async ({ effects }) => sdk.Backups.ofVolumes('main'), +) diff --git a/deploy/startos/dependencies.ts b/deploy/startos/dependencies.ts new file mode 100644 index 0000000..f77d698 --- /dev/null +++ b/deploy/startos/dependencies.ts @@ -0,0 +1,3 @@ +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies(async () => ({})) diff --git a/deploy/startos/index.ts b/deploy/startos/index.ts new file mode 100644 index 0000000..1643d3a --- /dev/null +++ b/deploy/startos/index.ts @@ -0,0 +1,8 @@ +export { createBackup } from './backups' +export { main } from './main' +export { init, uninit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versionGraph } from './install/versionGraph' +export const manifest = buildManifest(versionGraph, sdkManifest) diff --git a/deploy/startos/init/index.ts b/deploy/startos/init/index.ts new file mode 100644 index 0000000..f69244d --- /dev/null +++ b/deploy/startos/init/index.ts @@ -0,0 +1,16 @@ +import { sdk } from '../sdk' +import { setDependencies } from '../dependencies' +import { setInterfaces } from '../interfaces' +import { versionGraph } from '../install/versionGraph' +import { actions } from '../actions' +import { restoreInit } from '../backups' + +export const init = sdk.setupInit( + restoreInit, + versionGraph, + setInterfaces, + setDependencies, + actions, +) + +export const uninit = sdk.setupUninit(versionGraph) diff --git a/deploy/startos/install/versionGraph.ts b/deploy/startos/install/versionGraph.ts new file mode 100644 index 0000000..5a4fad7 --- /dev/null +++ b/deploy/startos/install/versionGraph.ts @@ -0,0 +1,7 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { current, other } from './versions' + +export const versionGraph = VersionGraph.of({ + current, + other, +}) diff --git a/deploy/startos/install/versions/index.ts b/deploy/startos/install/versions/index.ts new file mode 100644 index 0000000..2dbbcfb --- /dev/null +++ b/deploy/startos/install/versions/index.ts @@ -0,0 +1,2 @@ +export { v_0_1_0 as current } from './v_0_1_0' +export const other = [] diff --git a/deploy/startos/install/versions/v_0_1_0.ts b/deploy/startos/install/versions/v_0_1_0.ts new file mode 100644 index 0000000..e798a4a --- /dev/null +++ b/deploy/startos/install/versions/v_0_1_0.ts @@ -0,0 +1,16 @@ +import { VersionInfo } from '@start9labs/start-sdk' +import * as fs from 'fs' + +export const v_0_1_0 = VersionInfo.of({ + version: '0.1.0:0', + releaseNotes: { + en_US: 'Initial release. Internal fund administration and quarterly valuation sign-off.', + }, + migrations: { + up: async ({ effects }) => { + // DB initialization handled by Alembic on app startup. + // First approver account created by start.sh if no users exist. + }, + down: async ({ effects }) => {}, + }, +}) diff --git a/deploy/startos/interfaces.ts b/deploy/startos/interfaces.ts new file mode 100644 index 0000000..51c2d1b --- /dev/null +++ b/deploy/startos/interfaces.ts @@ -0,0 +1,24 @@ +import { sdk } from './sdk' + +const appPort = 8000 + +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + const uiMulti = sdk.MultiHost.of(effects, 'ui-multi') + const uiMultiOrigin = await uiMulti.bindPort(appPort, { + protocol: 'http', + }) + const ui = sdk.createInterface(effects, { + name: 'Web UI', + id: 'ui', + description: 'Ten31Portal fund administration interface', + type: 'ui', + masked: false, + schemeOverride: null, + username: null, + path: '', + query: {}, + }) + + const uiReceipt = await uiMultiOrigin.export([ui]) + return [uiReceipt] +}) diff --git a/deploy/startos/main.ts b/deploy/startos/main.ts new file mode 100644 index 0000000..9957a44 --- /dev/null +++ b/deploy/startos/main.ts @@ -0,0 +1,39 @@ +import { sdk } from './sdk' + +const appPort = 8000 + +export const main = sdk.setupMain(async ({ effects }) => { + console.info('Starting Ten31Portal') + + const subcontainer = await sdk.SubContainer.of( + effects, + { imageId: 'main' }, + sdk.Mounts.of().mountVolume({ + volumeId: 'main', + subpath: null, + mountpoint: '/data', + readonly: false, + }), + 'ten31portal-sub', + ) + + return sdk.Daemons.of(effects).addDaemon('primary', { + subcontainer, + exec: { + command: ['sh', '-c', '/app/start.sh'], + env: { + TEN31_DB_PATH: '/data/portal.db', + TEN31_SESSION_SECRET: process.env.TEN31_SESSION_SECRET || 'change-me', + }, + }, + ready: { + display: 'Web Interface', + fn: () => + sdk.healthCheck.checkPortListening(effects, appPort, { + successMessage: 'Ten31Portal is ready', + errorMessage: 'Ten31Portal is starting...', + }), + }, + requires: [], + }) +}) diff --git a/deploy/startos/manifest/index.ts b/deploy/startos/manifest/index.ts new file mode 100644 index 0000000..b0389cd --- /dev/null +++ b/deploy/startos/manifest/index.ts @@ -0,0 +1,32 @@ +import { setupManifest } from '@start9labs/start-sdk' + +export const manifest = setupManifest({ + id: 'ten31portal', + title: 'Ten31Portal', + license: 'MIT', + packageRepo: 'https://github.com/smallblocks/Ten31Portal', + upstreamRepo: 'https://github.com/smallblocks/Ten31Portal', + marketingUrl: 'https://ten31.com', + donationUrl: 'https://ten31.com', + docsUrls: ['https://github.com/smallblocks/Ten31Portal/blob/main/README.md'], + description: { + short: 'Internal fund administration and valuation sign-off', + long: 'System of record for Ten31 entities, holdings, positions, and quarterly valuation sign-off. Replaces Carta for internal use.', + }, + volumes: ['main'], + images: { + main: { + source: { dockerBuild: { dockerfile: './Dockerfile', workdir: '.' } }, + arch: ['x86_64', 'aarch64'], + }, + }, + alerts: { + install: 'After installation, access the web UI to log in. A default approver account is created on first boot using the configured credentials.', + update: null, + uninstall: 'Uninstalling will remove all fund data. Create a backup first.', + restore: null, + start: null, + stop: null, + }, + dependencies: {}, +}) diff --git a/deploy/startos/sdk.ts b/deploy/startos/sdk.ts new file mode 100644 index 0000000..4bf7c2c --- /dev/null +++ b/deploy/startos/sdk.ts @@ -0,0 +1,4 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' + +export const sdk = StartSdk.of().withManifest(manifest).build(true) diff --git a/deploy/tsconfig.json b/deploy/tsconfig.json new file mode 100644 index 0000000..c5579de --- /dev/null +++ b/deploy/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "outDir": "./javascript", + "rootDir": "./startos" + }, + "include": ["startos/**/*.ts"] +}