commit 0342b5fb9d50db0f5235c57c4b7cebe2848df646 Author: ponzischeme89 Date: Wed May 6 22:38:06 2026 +1200 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..29e9f27 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.svelte-kit +build +npm-debug.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8d8a22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +.svelte-kit/ +build/ + +.env + +*.log +*.err + +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b127b19 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ + +ARG PUBLIC_SITE_URL=http://localhost:8080 +ENV PUBLIC_SITE_URL=${PUBLIC_SITE_URL} + +RUN npm ci + +COPY . . + +RUN npm run build + +FROM node:22-alpine + +WORKDIR /app + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=3000 + +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev +COPY --from=builder /app/build ./build +COPY --from=builder /app/static ./static + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:3000/ || exit 1 + +CMD ["node", "build"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..aee4790 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# Lean 101 Website + +This repo runs as a server-rendered SvelteKit app with a custom Mongo-backed CMS at `/admin`. + +## Stack + +- `SvelteKit` +- `@sveltejs/adapter-node` +- `MongoDB` via the native `mongodb` driver +- `docker compose` for local and server runtime + +## What changed + +- The public site now reads page content from MongoDB when `MONGODB_URI` and `MONGODB_DB` are set. +- If MongoDB is not configured, the homepage falls back to the local seed file at `src/lib/content/homepage.json`. +- `/admin` is a custom password-protected editor for: + - pages + - movable page sections + - blog posts +- The homepage seed is automatically migrated into the `site_pages` collection the first time the app connects to MongoDB. + +## Project layout + +```text +. +├── Dockerfile +├── docker-compose.yml +├── package.json +├── scripts/ +│ └── deploy-do.sh +├── src/ +│ ├── app.css +│ ├── app.html +│ ├── lib/ +│ │ ├── content/homepage.json +│ │ ├── components/SitePage.svelte +│ │ ├── server/auth.js +│ │ ├── server/content.js +│ │ └── site.js +│ └── routes/ +│ ├── +layout.js +│ ├── +page.server.js +│ ├── +page.svelte +│ ├── [slug]/+page.server.js +│ ├── [slug]/+page.svelte +│ ├── admin/+page.server.js +│ ├── admin/+page.svelte +│ ├── blog/+page.server.js +│ ├── blog/+page.svelte +│ ├── blog/[slug]/+page.server.js +│ ├── blog/[slug]/+page.svelte +│ ├── robots.txt/+server.js +│ └── sitemap.xml/+server.js +└── static/assets/ + ├── lean101-isotipo.png + └── lean101-logotipo.png +``` + +## Environment variables + +Required for the CMS: + +- `MONGODB_URI` +- `MONGODB_DB` +- `ADMIN_PASSWORD` +- `ADMIN_SESSION_SECRET` + +Required for correct canonical/meta URLs: + +- `PUBLIC_SITE_URL` + +Example `.env`: + +```env +PUBLIC_SITE_URL=http://localhost:8080 +MONGODB_URI=mongodb://localhost:27017 +MONGODB_DB=lean101 +ADMIN_PASSWORD=replace-this +ADMIN_SESSION_SECRET=replace-this-with-a-long-random-string +``` + +## Run locally with Docker + +```bash +docker compose up --build +``` + +The site will be available at `http://localhost:8080`. + +The app server listens on port `3000` inside the container and is mapped to `8080` on the host. + +## Run locally without Docker + +```bash +npm install +npm run dev +``` + +The Vite dev server will run on its normal local port, usually `http://localhost:5173`. + +## Production build + +```bash +npm run build +npm run preview +``` + +`npm run preview` now runs the built Node server with `node build`. + +## Custom CMS + +Visit `/admin`. + +The CMS is intentionally simple, but it now supports structured content: + +- one password +- multiple pages +- reorderable sections within each page +- blog drafts and published posts + +Current collections: + +- `site_pages` +- `blog_posts` + +Current section types: + +- `hero` +- `services` +- `process` +- `outcomes` +- `why` +- `richText` +- `cta` + +Published blog posts are available under `/blog`, and individual pages are available at `/`. + +## DigitalOcean deployment script + +After copying this project folder onto a droplet, you can run: + +```bash +sudo ./scripts/deploy-do.sh --domain yourdomain.com --email you@example.com --with-www +``` + +The script: + +- installs Docker, the compose plugin, nginx, and certbot +- writes `.env` with `PUBLIC_SITE_URL=https://yourdomain.com` +- starts the compose stack +- configures system nginx to reverse proxy to the Docker app on `127.0.0.1:8080` +- requests and installs a Let's Encrypt certificate if `--email` is provided + +If you use the CMS in production, make sure the `.env` file on the server also includes: + +- `MONGODB_URI` +- `MONGODB_DB` +- `ADMIN_PASSWORD` +- `ADMIN_SESSION_SECRET` + +## Notes + +- The current contact CTA still uses `mailto:hello@lean-101.com` +- The seed homepage remains in `src/lib/content/homepage.json` +- Live page and blog content is stored in MongoDB once the database env vars are configured diff --git a/assets/lean101-isotipo.png b/assets/lean101-isotipo.png new file mode 100644 index 0000000..f372b59 Binary files /dev/null and b/assets/lean101-isotipo.png differ diff --git a/assets/lean101-logotipo.png b/assets/lean101-logotipo.png new file mode 100644 index 0000000..fba82e9 Binary files /dev/null and b/assets/lean101-logotipo.png differ diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..4f99a1e --- /dev/null +++ b/deploy.sh @@ -0,0 +1,500 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +REPO_URL="" +BRANCH="main" +REF="" +DEPLOY_PATH="" +COMPOSE_FILE="" +PROJECT_NAME="" +SERVICE_NAME="" +NGINX_SOURCE="" +NGINX_TARGET="" +NGINX_COMPOSE_FILE="" +NGINX_PROJECT_NAME="" +MAINTENANCE_HOST_DIR="" +MAINTENANCE_FLAG_PATH="" +VERIFY_URL="https://www.goodwalk.co.nz/api/health" +SKIP_SITE_CHECK=0 + +usage() { + cat <<'EOF' +Usage: + deploy-from-git.sh --repo-url --branch --deploy-path --compose-file --project-name + deploy-from-git.sh --repo-url [--branch ] [--ref ] --deploy-path --compose-file --project-name [--service ] + deploy-from-git.sh --repo-url [--branch ] [--ref ] --deploy-path --compose-file --project-name \ + [--service ] [--nginx-source ] [--nginx-target ] \ + [--nginx-compose-file ] [--nginx-project-name ] \ + [--maintenance-host-dir ] [--maintenance-flag ] \ + [--verify-url ] [--skip-site-check] + +This script clones or fetches the application repo on the server, exports the +homepage content payload, updates only the main Goodwalk compose project, and +optionally updates the shared nginx stack plus maintenance mode handling. + +Authentication for private HTTPS repos is expected to come from ~/.netrc, +git-credential, or another Git-supported credential mechanism already present +on the server. +EOF +} + +fail() { + echo "[deploy-git] ERROR: $*" >&2 + exit 1 +} + +assert_command() { + command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' is not installed on the server" +} + +run_homepage_export() { + local export_script="$1" + local output_path="$2" + + if command -v node >/dev/null 2>&1; then + node --experimental-strip-types "$export_script" "$output_path" + return + fi + + echo "[deploy-git] Host node not found; exporting homepage content via temporary node:22-alpine container" + docker run --rm \ + -v "$CHECKOUT_DIR:/app" \ + -w /app \ + node:22-alpine \ + node --experimental-strip-types "${export_script#/app/}" "${output_path#/app/}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo-url) + REPO_URL="${2:-}" + shift 2 + ;; + --branch) + BRANCH="${2:-}" + shift 2 + ;; + --ref) + REF="${2:-}" + shift 2 + ;; + --deploy-path) + DEPLOY_PATH="${2:-}" + shift 2 + ;; + --compose-file) + COMPOSE_FILE="${2:-}" + shift 2 + ;; + --project-name) + PROJECT_NAME="${2:-}" + shift 2 + ;; + --service) + SERVICE_NAME="${2:-}" + shift 2 + ;; + --nginx-source) + NGINX_SOURCE="${2:-}" + shift 2 + ;; + --nginx-target) + NGINX_TARGET="${2:-}" + shift 2 + ;; + --nginx-compose-file) + NGINX_COMPOSE_FILE="${2:-}" + shift 2 + ;; + --nginx-project-name) + NGINX_PROJECT_NAME="${2:-}" + shift 2 + ;; + --maintenance-host-dir) + MAINTENANCE_HOST_DIR="${2:-}" + shift 2 + ;; + --maintenance-flag) + MAINTENANCE_FLAG_PATH="${2:-}" + shift 2 + ;; + --verify-url) + VERIFY_URL="${2:-}" + shift 2 + ;; + --skip-site-check) + SKIP_SITE_CHECK=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "Unknown argument: $1" + ;; + esac +done + +[[ -n "$REPO_URL" ]] || fail "--repo-url is required" +[[ -n "$BRANCH" ]] || fail "--branch is required" +[[ -n "$DEPLOY_PATH" ]] || fail "--deploy-path is required" +[[ -n "$COMPOSE_FILE" ]] || fail "--compose-file is required" +[[ -n "$PROJECT_NAME" ]] || fail "--project-name is required" +if [[ -n "$SERVICE_NAME" ]]; then + SERVICE_NAME="$(printf '%s' "$SERVICE_NAME" | xargs)" +fi +[[ "$DEPLOY_PATH" != "/" ]] || fail "Refusing to deploy to /" + +nginx_args=("$NGINX_SOURCE" "$NGINX_TARGET" "$NGINX_COMPOSE_FILE" "$NGINX_PROJECT_NAME") +nginx_args_present=0 +for value in "${nginx_args[@]}"; do + if [[ -n "$value" ]]; then + nginx_args_present=1 + break + fi +done + +if (( nginx_args_present )); then + [[ -n "$NGINX_SOURCE" ]] || fail "--nginx-source is required when nginx deployment is enabled" + [[ -n "$NGINX_TARGET" ]] || fail "--nginx-target is required when nginx deployment is enabled" + [[ -n "$NGINX_COMPOSE_FILE" ]] || fail "--nginx-compose-file is required when nginx deployment is enabled" + [[ -n "$NGINX_PROJECT_NAME" ]] || fail "--nginx-project-name is required when nginx deployment is enabled" + [[ -n "$MAINTENANCE_HOST_DIR" ]] || fail "--maintenance-host-dir is required when nginx deployment is enabled" + [[ -n "$MAINTENANCE_FLAG_PATH" ]] || fail "--maintenance-flag is required when nginx deployment is enabled" +fi + +assert_command git +assert_command docker +if docker compose version >/dev/null 2>&1; then + COMPOSE_CMD=(docker compose) +elif command -v docker-compose >/dev/null 2>&1; then + COMPOSE_CMD=(docker-compose) +else + fail "Docker Compose is not installed on the server" +fi + +WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/goodwalk-git-deploy.XXXXXX")" +CHECKOUT_DIR="$WORK_DIR/repo" +PAYLOAD_DIR="$WORK_DIR/payload" +MAINTENANCE_ACTIVE=0 + +clear_maintenance_flag() { + if (( MAINTENANCE_ACTIVE )) && (( nginx_args_present )); then + echo "[deploy-git] Clearing maintenance flag at $MAINTENANCE_FLAG_PATH" + rm -f "$MAINTENANCE_FLAG_PATH" || true + MAINTENANCE_ACTIVE=0 + fi +} + +cleanup() { + clear_maintenance_flag + rm -rf "$WORK_DIR" +} + +copy_checkout_to_payload() { + mkdir -p "$PAYLOAD_DIR" + + if command -v rsync >/dev/null 2>&1; then + rsync -a \ + --exclude '.git' \ + --exclude '.env' \ + --exclude '.env.*' \ + "$CHECKOUT_DIR"/ "$PAYLOAD_DIR"/ + return + fi + + while IFS= read -r -d '' item; do + relative_path="${item#"$CHECKOUT_DIR"/}" + + case "$relative_path" in + .git|.git/*|.env|.env.*) + continue + ;; + esac + + destination="$PAYLOAD_DIR/$relative_path" + + if [[ -d "$item" ]]; then + mkdir -p "$destination" + continue + fi + + mkdir -p "$(dirname "$destination")" + cp -f "$item" "$destination" + done < <(find "$CHECKOUT_DIR" -mindepth 1 -print0) +} + +copy_payload_to_deploy() { + mkdir -p "$DEPLOY_PATH" + + if command -v rsync >/dev/null 2>&1; then + rsync -a \ + --exclude '.env' \ + --exclude '.env.*' \ + "$PAYLOAD_DIR"/ "$DEPLOY_PATH"/ + return + fi + + while IFS= read -r -d '' item; do + relative_path="${item#"$PAYLOAD_DIR"/}" + + if [[ "$relative_path" == ".env" || "$relative_path" == .env.* ]]; then + continue + fi + + destination="$DEPLOY_PATH/$relative_path" + + if [[ -d "$item" ]]; then + mkdir -p "$destination" + continue + fi + + mkdir -p "$(dirname "$destination")" + cp -f "$item" "$destination" + done < <(find "$PAYLOAD_DIR" -mindepth 1 -print0) +} + +merge_env_file() { + local template="$1" + local live="$2" + + [[ -f "$template" ]] || { echo "[deploy-git] No env template at $template, skipping merge"; return 0; } + [[ -f "$live" ]] || { echo "[deploy-git] No live .env at $live, skipping merge"; return 0; } + + local added diffs backup + added="$(mktemp)" + diffs="$(mktemp)" + backup="${live}.bak.$(date -u +%Y%m%dT%H%M%SZ)" + + awk -v live="$live" -v added_log="$added" -v diff_log="$diffs" ' + function trim(s) { sub(/^[ \t]+/,"",s); sub(/[ \t]+$/,"",s); return s } + BEGIN { + while ((getline line < live) > 0) { + if (line ~ /^[ \t]*#/ || line ~ /^[ \t]*$/) continue + eq = index(line, "=") + if (eq == 0) continue + k = trim(substr(line, 1, eq-1)) + v = substr(line, eq+1) + live_keys[k] = v + live_seen[k] = 1 + } + close(live) + } + /^[ \t]*#/ || /^[ \t]*$/ { next } + { + eq = index($0, "=") + if (eq == 0) next + k = trim(substr($0, 1, eq-1)) + v = substr($0, eq+1) + if (!(k in live_seen)) { + print k "=" v >> added_log + } else if (live_keys[k] != v) { + print k " (template=" v " | live=" live_keys[k] ")" >> diff_log + } + } + ' "$template" + + if [[ -s "$added" ]]; then + cp "$live" "$backup" + echo "[deploy-git] Adding env keys present in template but missing from $live:" + sed 's/^/ + /' "$added" + { + printf '\n# Appended by deploy-from-git.sh on %s from deploy.env.template\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + cat "$added" + } >> "$live" + echo "[deploy-git] Backup of previous .env written to $backup" + else + echo "[deploy-git] .env is up to date with template (no missing keys)" + fi + + if [[ -s "$diffs" ]]; then + echo "[deploy-git] NOTE: these keys exist in both files but values differ. Live values are PRESERVED:" + sed 's/^/ ! /' "$diffs" + echo "[deploy-git] If a live value is stale, edit $live and re-deploy." + fi + + rm -f "$added" "$diffs" +} + +check_site() { + if (( SKIP_SITE_CHECK )) || [[ -z "$VERIFY_URL" ]]; then + return 0 + fi + + echo "[deploy-git] Checking production site: $VERIFY_URL" + + if command -v curl >/dev/null 2>&1; then + local http_code + if http_code="$(curl -fsS -o /dev/null -w '%{http_code}' --max-time 30 -L "$VERIFY_URL" 2>/dev/null)"; then + echo "[deploy-git] Site responded with HTTP $http_code" + else + echo "[deploy-git] WARNING: production site check failed for $VERIFY_URL" >&2 + fi + return 0 + fi + + if command -v wget >/dev/null 2>&1; then + if wget --spider --server-response --timeout=30 "$VERIFY_URL" >/tmp/goodwalk-site-check.$$ 2>&1; then + awk '/^ HTTP\// { code=$2 } END { if (code != "") printf "[deploy-git] Site responded with HTTP %s\n", code }' /tmp/goodwalk-site-check.$$ + else + echo "[deploy-git] WARNING: production site check failed for $VERIFY_URL" >&2 + fi + rm -f /tmp/goodwalk-site-check.$$ + return 0 + fi + + echo "[deploy-git] WARNING: curl/wget not available; skipping site check" >&2 +} + +trap cleanup EXIT + +echo "[deploy-git] Deploying main Goodwalk stack from Git" +echo "[deploy-git] Repo URL: $REPO_URL" +echo "[deploy-git] Branch: $BRANCH" +if [[ -n "$REF" ]]; then + echo "[deploy-git] Requested ref: $REF" +fi +echo "[deploy-git] Target deployment path: $DEPLOY_PATH" +echo "[deploy-git] Compose file: $COMPOSE_FILE" +echo "[deploy-git] Docker project: $PROJECT_NAME" +if [[ -n "$SERVICE_NAME" ]]; then + echo "[deploy-git] Target service: $SERVICE_NAME" +fi +if (( nginx_args_present )); then + echo "[deploy-git] Nginx config source: $NGINX_SOURCE" + echo "[deploy-git] Nginx config target: $NGINX_TARGET" + echo "[deploy-git] Nginx compose file: $NGINX_COMPOSE_FILE" + echo "[deploy-git] Nginx project: $NGINX_PROJECT_NAME" + echo "[deploy-git] Maintenance host dir: $MAINTENANCE_HOST_DIR" + echo "[deploy-git] Maintenance flag path: $MAINTENANCE_FLAG_PATH" +fi + +echo "[deploy-git] Cloning repository into: $CHECKOUT_DIR" +git clone "$REPO_URL" "$CHECKOUT_DIR" +git -C "$CHECKOUT_DIR" fetch --tags --prune origin + +if [[ -n "$REF" ]]; then + git -C "$CHECKOUT_DIR" checkout --detach "$REF" +else + git -C "$CHECKOUT_DIR" checkout -B "$BRANCH" "origin/$BRANCH" +fi + +DEPLOYED_REVISION="$(git -C "$CHECKOUT_DIR" rev-parse HEAD)" +echo "[deploy-git] Using repo revision: $DEPLOYED_REVISION" + +EXPORT_SCRIPT="$CHECKOUT_DIR/scripts/export-homepage-content.mjs" +[[ -f "$EXPORT_SCRIPT" ]] || fail "Homepage export script not found: $EXPORT_SCRIPT" +echo "[deploy-git] Exporting current homepage content for PostgreSQL sync" +run_homepage_export "/app/scripts/export-homepage-content.mjs" "/app/deploy-data/homepage-content.json" + +echo "[deploy-git] Preparing deployment payload" +copy_checkout_to_payload + +[[ -f "$PAYLOAD_DIR/$COMPOSE_FILE" ]] || fail "Compose file missing from repo checkout: $COMPOSE_FILE" + +if [[ -f "$DEPLOY_PATH/.env" ]]; then + echo "[deploy-git] Preserving existing $DEPLOY_PATH/.env" +fi + +echo "[deploy-git] Copying application files into $DEPLOY_PATH" +copy_payload_to_deploy + +[[ -f "$DEPLOY_PATH/$COMPOSE_FILE" ]] || fail "Compose file missing after copy: $DEPLOY_PATH/$COMPOSE_FILE" + +if [[ ! -f "$DEPLOY_PATH/.env" ]]; then + if [[ -f "$DEPLOY_PATH/deploy.env.template" ]]; then + echo "[deploy-git] No remote .env found. Creating $DEPLOY_PATH/.env from deploy.env.template" + cp "$DEPLOY_PATH/deploy.env.template" "$DEPLOY_PATH/.env" + else + fail "Remote .env is missing and deploy.env.template was not present" + fi +fi + +merge_env_file "$DEPLOY_PATH/deploy.env.template" "$DEPLOY_PATH/.env" + +cd "$DEPLOY_PATH" + +echo "[deploy-git] Validating compose configuration" +"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" config >/dev/null + +if [[ -n "$SERVICE_NAME" ]]; then + AVAILABLE_SERVICES="$("${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" config --services)" + if ! grep -Fxq "$SERVICE_NAME" <<<"$AVAILABLE_SERVICES"; then + fail "Service '$SERVICE_NAME' was not found in $COMPOSE_FILE. Available services: $(tr '\n' ',' <<<"$AVAILABLE_SERVICES" | sed 's/,$//')" + fi +fi + +if (( nginx_args_present )); then + [[ -f "$DEPLOY_PATH/$NGINX_SOURCE" ]] || fail "Nginx config missing from deployment payload: $DEPLOY_PATH/$NGINX_SOURCE" + [[ -f "$NGINX_COMPOSE_FILE" ]] || fail "Nginx compose file was not found on the server: $NGINX_COMPOSE_FILE" + + MAINTENANCE_HTML_SRC="$DEPLOY_PATH/nginx/maintenance.html" + MAINTENANCE_LOGO_SRC="$DEPLOY_PATH/nginx/logo.png" + [[ -f "$MAINTENANCE_HTML_SRC" ]] || fail "Maintenance page missing from deployment payload: $MAINTENANCE_HTML_SRC" + [[ -f "$MAINTENANCE_LOGO_SRC" ]] || fail "Maintenance logo missing from deployment payload: $MAINTENANCE_LOGO_SRC" + + NGINX_CID="$(docker ps -qf name=^nginx$ | head -n1 || true)" + [[ -n "$NGINX_CID" ]] || fail "Shared nginx container is not running (expected name 'nginx'). Bring it up before deploying." + + if ! docker inspect -f '{{range .Mounts}}{{.Source}}|{{.Destination}}{{println}}{{end}}' "$NGINX_CID" \ + | grep -Fxq "${MAINTENANCE_HOST_DIR}|/var/www/maintenance"; then + fail "nginx container is missing the maintenance bind mount. + Expected: ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro + One-time setup on the droplet: + mkdir -p ${MAINTENANCE_HOST_DIR}/m + # add this volume to ${NGINX_COMPOSE_FILE}: + # - ${MAINTENANCE_HOST_DIR}:/var/www/maintenance:ro + ${COMPOSE_CMD[*]} -p ${NGINX_PROJECT_NAME} -f ${NGINX_COMPOSE_FILE} up -d" + fi + + FLAG_DIR="$(dirname "$MAINTENANCE_FLAG_PATH")" + [[ -d "$FLAG_DIR" ]] || fail "Maintenance flag directory does not exist on host: $FLAG_DIR" + + echo "[deploy-git] Writing maintenance assets to host bind dir: $MAINTENANCE_HOST_DIR" + mkdir -p "$MAINTENANCE_HOST_DIR/m" + install -m 0644 "$MAINTENANCE_HTML_SRC" "$MAINTENANCE_HOST_DIR/maintenance.html" + install -m 0644 "$MAINTENANCE_LOGO_SRC" "$MAINTENANCE_HOST_DIR/m/logo.png" + + echo "[deploy-git] Updating shared nginx config (pre-rebuild) so maintenance routing is active" + mkdir -p "$(dirname "$NGINX_TARGET")" + cp "$DEPLOY_PATH/$NGINX_SOURCE" "$NGINX_TARGET" + + echo "[deploy-git] Validating nginx configuration" + "${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -t + + echo "[deploy-git] Reloading shared nginx so the new config (incl. maintenance routing) is live" + "${COMPOSE_CMD[@]}" -p "$NGINX_PROJECT_NAME" -f "$NGINX_COMPOSE_FILE" exec -T nginx nginx -s reload + + echo "[deploy-git] Engaging maintenance page via host flag: $MAINTENANCE_FLAG_PATH" + : > "$MAINTENANCE_FLAG_PATH" + MAINTENANCE_ACTIVE=1 +fi + +if [[ -n "$SERVICE_NAME" ]]; then + echo "[deploy-git] Stopping only the Goodwalk service: $SERVICE_NAME" + "${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop "$SERVICE_NAME" || true + + echo "[deploy-git] Rebuilding and starting only the Goodwalk service: $SERVICE_NAME" + "${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build "$SERVICE_NAME" +else + echo "[deploy-git] Stopping only the Goodwalk project containers" + "${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" stop || true + + echo "[deploy-git] Rebuilding and starting only the Goodwalk project containers" + "${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" up -d --build --remove-orphans +fi + +echo "[deploy-git] Current Goodwalk container status" +"${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" ps + +if [[ -z "$SERVICE_NAME" || "$SERVICE_NAME" == "app" || "$SERVICE_NAME" == "db" ]]; then + echo "[deploy-git] Syncing homepage content into PostgreSQL" + "${COMPOSE_CMD[@]}" -p "$PROJECT_NAME" -f "$COMPOSE_FILE" exec -T app node scripts/sync-homepage-content.mjs +fi + +clear_maintenance_flag +check_site + +echo "[deploy-git] Remote deployment finished" +echo "[deploy-git] Deployed revision: $DEPLOYED_REVISION" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..94c7844 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + lean101: + build: + context: . + args: + PUBLIC_SITE_URL: ${PUBLIC_SITE_URL:-http://localhost:8080} + container_name: lean101-website + environment: + PUBLIC_SITE_URL: ${PUBLIC_SITE_URL:-http://localhost:8080} + MONGODB_URI: ${MONGODB_URI:-} + MONGODB_DB: ${MONGODB_DB:-} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-} + ADMIN_SESSION_SECRET: ${ADMIN_SESSION_SECRET:-} + ports: + - "8080:3000" + restart: unless-stopped diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..027bb64 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "types": ["node"] + } +} diff --git a/lean-101-website-html-v3/README.md b/lean-101-website-html-v3/README.md new file mode 100644 index 0000000..2ac24fe --- /dev/null +++ b/lean-101-website-html-v3/README.md @@ -0,0 +1,152 @@ +# Lean101 — Website source files (v3.5) + +Hi Matt — these are the latest HTML/CSS/JS files for the Lean101 website. Drop-in replacement for the current site, restructured into a 4-page setup, with photos and the latest engagement-section restructure. + +--- + +## File structure + +``` +lean101-website/ +├── index.html — Homepage +├── services/ +│ ├── consulting.html — Process Excellence & Transformation +│ ├── coaching.html — Coaching & Training +│ └── digital-solutions.html — Digital Solutions +├── assets/ +│ ├── lean101-isotipo.png — Hexagon mark only (used in nav) +│ └── lean101-logotipo.png — Full wordmark (used in mobile drawer + footer) +└── README.md — This file +``` + +Every page is fully self-contained — all CSS is inlined in a ` + + + + + + + + + + + +
+ Smarter Improvement Solutions +

+ Continuous improvement,
+ made simple. +

+

+ Practical consulting, coaching and analytics to help businesses eliminate waste, + improve performance, and build a culture that keeps improving — long after we've gone. +

+ + + +
+ Team collaborating on a process improvement workshop +
+
+
+
Efficiency
+
+35%
+
+
+
Cost savings
+
−27%
+
+
+
Lead time
+
−44%
+
+
+
Engagement
+
+41%
+
+
+
+ + +
+
+
+
What we do
+

Three services. One outcome: improvement that lasts.

+

+ Frameworks aren't the goal — results are. Every engagement combines + consulting, capability building, and digital solutions — sized to what your team can sustain. +

+
+ +
+
+
+ Consulting + Consultant mapping out a process on a whiteboard +
+
+

Process Excellence & Transformation

+

+ We diagnose where work is breaking down, design optimised processes, and + run the change with you — not just for you. +

+ + Learn more + + +
+
+ +
+
+ Coaching & Training + Coach working with a small team in a workshop setting +
+
+

Building capability and culture

+

+ Tailored coaching and bespoke training that turns Lean theory into everyday + practice. Your team owns the improvement. +

+ + Learn more + + +
+
+ +
+
+ Digital Solutions + Custom software being developed on a laptop +
+
+

Custom apps, dashboards & automation

+

+ From Power BI dashboards to custom web apps that replace clunky spreadsheets — we build tailored software that streamlines your operations and makes data work for you. +

+ + Learn more + + +
+
+
+
+
+ + +
+
+
+
How it works
+

A clear path from waste to flow.

+

+ Every engagement runs four cycles — adapted to your scale, industry and team capability. The cycles repeat as the business evolves; what stays constant is the partnership underneath. +

+
+ +
+
+
+
Step 01
+

Diagnose

+

+ Process mapping, audits and a maturity assessment. We walk your operation alongside you, talk to the people doing the work, and quantify where the friction sits. +

+
+ By the end: a clear map of where time, quality and cost are leaking — and which fix moves the dial first. +
+
+ + + +
+
Step 02
+

Design & pilot

+

+ We co-design the future state with your team, then run a focused pilot in one area. Real data, real people, real results — before we touch the rest of the business. +

+
+ By the end: a proven future state, validated with hard data on a focused pilot. +
+
+ + + +
+
Step 03
+

Roll out

+

+ Once the pilot proves the gains, we scale the approach across other teams, sites or business units — adapted to each context, with the same disciplined method. +

+
+ By the end: the gains scaled across teams, sites or business units — same method, each context. +
+
+ + + +
+
Step 04
+

Sustain

+

+ Coaching, dashboards and train-the-trainer programs so the improvement becomes how your team operates — without us in the room. +

+
+ By the end: the improvement runs without us — your team owns it, the data proves it. +
+
+
+ + +
+
+ + +
+
+ The Partnership +

The four-step cycle happens inside an ongoing partnership. We stay alongside you across cycles — same team, same methods, learning your business as it evolves. Continuous improvement, made continuous.

+
+
+
+
+
+ + +
+
+
+
+
What good looks like
+

The five things continuous improvement moves.

+

+ Industry benchmarks for organisations that commit to a structured improvement program. Whatever your industry, these are the levers that shift. +

+
+ + Book a call + + +
+ + +
+
+ + +
+
+
+
+
Why Lean 101
+

One framework. Customised to your business.

+

+ Lean 101 is a one-on-one partnership. We bring globally recognised + frameworks — Lean, Six Sigma, continuous improvement best practice — + and adapt them to how your business actually runs. Every company, + every industry is different, so we work alongside you to solve the + problems that matter to you — not deliver a generic playbook. +

+
+ +
    +
  • +
    + +
    +
    +

    Globally recognised frameworks, practically applied

    +

    Lean, Six Sigma, and continuous improvement best practice — adapted to how your business actually runs, not lifted from a generic playbook.

    +
    +
  • +
  • +
    + +
    +
    +

    Customised to each business, not off the shelf

    +

    Every business is different. Focused coaching, a single process redesign, or full transformation — we shape the engagement around your priorities, capacity, and budget.

    +
    +
  • +
  • +
    + +
    +
    +

    We build capability, not dependency

    +

    Our success metric is the day you no longer need us. Train-the-trainer, embedded dashboards, and clear playbooks make the improvement stick.

    +
    +
  • +
  • +
    + +
    +
    +

    Data-led from day one

    +

    Power BI dashboards and automated reporting come standard. You see what's working, what isn't, and what to do next — in real time.

    +
    +
  • +
+
+
+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
1 30-min intro call
+ +
2 1-hour site visit
+ +
3 Tailored partnership
+
+ + +
+
+
+ + + + + + + + diff --git a/lean-101-website-html-v3/services/coaching.html b/lean-101-website-html-v3/services/coaching.html new file mode 100644 index 0000000..8661083 --- /dev/null +++ b/lean-101-website-html-v3/services/coaching.html @@ -0,0 +1,2082 @@ + + + + + +Coaching & Training — Lean 101 + + + + + + + + + + + + + + + +
+
+
+ Coaching & Training +

Building capability and culture.

+

+ Tailored coaching and bespoke training that turns Lean theory into everyday practice. Your team owns the improvement — and keeps building on it long after we've gone. +

+ +
+
+ Coaching conversation between two people in a working environment +
+
+
+ + +
+
+
+ Workshop with team members collaborating around laptops +
+
+
Who it's for
+

If any of these sound familiar, this is for you.

+

+ Most businesses we coach have smart people who want to improve. The problem isn't will — it's method. These are the things leaders most often say when they reach out. +

+
+ +
+
+
+

"Improvement only happens when I'm in the room. The moment I step out, it goes back to how it was."

+
+
+
+ +
+
+ The leader-bottleneck + Capability lives in one person — and only one person. +
+
+
+ +
+
+

"My team has experience. They've been here years. But when something goes wrong they just... work around it. They don't dig."

+
+
+
+ +
+
+ Smart people, no framework + Experience without a method for getting to root cause. +
+
+
+ +
+
+

"We had a consultant come in two years ago. The slides were great. The improvements lasted about a quarter. We're back where we started."

+
+
+
+ +
+
+ The slide-deck graveyard + Past help left process, not capability. The team never owned it. +
+
+
+ +
+
+

"I don't want to keep paying consultants. I want my team to be the consultants — for ourselves, and for whoever joins next."

+
+
+
+ +
+
+ Self-sustaining capability + The goal isn't a method, it's an internal improvement habit. +
+
+
+
+
+
+ + +
+
+
+
The framework
+

Learn it. Adapt it. Teach it.

+

+ Real capability develops in three stages — first you follow the method, then you adapt it to your context, then you teach the next team. We borrow this from the Toyota Way, where it's called Shu-Ha-Ri. +

+
+ +
+
+ +
+
+
01
+
+
+
Stage 1 · Learn the method Shu
+

Follow the form. Build the muscle.

+

The team learns the methods exactly as taught — A3 problem-solving, 5 Whys, value-stream mapping, structured root-cause analysis. The goal at this stage isn't creativity. It's fluency in the form, before improvising.

+
+ What it sounds like: "We're using the method, even when it feels slow. We're getting better at it." +
+
+
+ +
+
+
02
+
+
+
Stage 2 · Adapt to your context Ha
+

Understand why the method works. Then adjust it.

+

The team starts seeing why each step exists — and what to bend, skip, or extend for their own situation. Coaching shifts from instruction to discussion. Methods become tools they apply, not rules they follow.

+
+ What it sounds like: "We're solving real problems with the methods — and adjusting them to fit how we actually work." +
+
+
+ +
+
+
03
+
+
+
Stage 3 · Teach the next team Ri
+

The method becomes how you think. Then you teach it.

+

Your most capable people are now coaches themselves. They run workshops, mentor others, and propagate the method across the business. They create new approaches when the situation calls for it. The improvement engine has gone internal.

+
+ What it sounds like: "We're teaching the next team without us in the room. Improvement is just how we work now." +
+
+
+
+ +
+ Why this approach? Because the goal isn't a slide deck of methods — it's a team that can teach the next team. Most consultancies stop at Stage 1: a binder of tools, no real ownership. We're built around getting you to Stage 3 and stepping back. +
+
+
+ + +
+
+
+
What this looks like in practice
+

Where Coaching & Training delivers real value.

+

+ Two illustrative scenarios showing what kind of work this looks like and what kind of capability typically develops. Real programs are shaped to your team and the problems they're solving. +

+
+ +
+
+
+ Senior leader in a one-on-one coaching context +
+ + + Leadership development + +

Building Lean leaders, not just Lean teams.

+

Senior leaders sponsor improvement but don't lead it. Improvement happens to the team, not through it. We coach 3–6 senior leaders one-on-one, helping them shift from giving answers to asking the right structured questions — and run their own A3 cycles on real business problems.

+
+
+
3–6*
+
Leaders coached to fluency
+
+
+
2–4×*
+
More improvement cycles per quarter
+
+
+
+ +
+
+ Team in a structured discussion around a table +
+ + + Frontline team capability + +

Turning a frontline team into structured problem-solvers.

+

Smart, experienced team but no shared method. Same issues recur. A 12-week program: workshops on A3, 5 Whys, and value-stream mapping; weekly coaching cycles where the team brings real problems and we coach them through structured solving.

+
+
+
8–15*
+
Team members trained to fluency
+
+
+
5–12*
+
Real problems solved during program
+
+
+
+
+ +
+
+ + +
+
+
+
The engagement
+

A focused program. Often, the start of a partnership.

+

+ Most coaching engagements begin as a focused program — a workshop series, a capability build, or a train-the-trainer rollout. The capability work that follows tends to grow into an ongoing partnership over time. +

+
+ + +
+
+ + +
+

+ *About these ranges. These are illustrative scenarios drawn from typical capability-building work — not specific client results. Real programs are shaped to your team's starting capability and the business problems they're solving. +

+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
+
+ + + + + + + + + + diff --git a/lean-101-website-html-v3/services/consulting.html b/lean-101-website-html-v3/services/consulting.html new file mode 100644 index 0000000..e4fb667 --- /dev/null +++ b/lean-101-website-html-v3/services/consulting.html @@ -0,0 +1,1915 @@ + + + + + +Process Excellence & Transformation — Lean 101 + + + + + + + + + + + + + + + +
+
+
+ Consulting +

Process Excellence & Transformation.

+

+ We diagnose where work is breaking down, design optimised processes, and run the change with you — not just for you. Practical Lean and Six Sigma applied to how your business actually runs. +

+ +
+
+ Operations team at work in a real workplace +
+
+
+ + +
+
+
+ Team in a working session, problem-solving on a whiteboard +
+
+
Who it's for
+

Built for businesses where the process used to work — and now doesn't.

+

+ Most of our consulting clients aren't in crisis. They've grown, things have got more complex, and the way work flows has stopped keeping up. If any of these sound familiar, this is for you. +

+
+ +
+
+
+ +
+

Things take longer than they used to

+

Lead times have crept up. Customers wait longer. Your team is busy but throughput hasn't kept pace with demand.

+
+
+
+ +
+

Errors are repeating, not improving

+

The same defects, the same rework, the same complaints — month after month. Fixes are reactive, not structural.

+
+
+
+ +
+

You sense waste, but can't quantify it

+

You know there's slack in the system. Hand-offs, waiting, duplicate work. But you don't have data to prove where, or how much.

+
+
+
+ +
+

You've grown — and the system hasn't kept up

+

What worked at 20 people doesn't work at 80. The same roles do more jobs, the same processes carry more load, and the cracks are starting to show.

+
+
+
+
+ + +
+
+
+
The framework
+

Lean Six Sigma, applied with discipline.

+

+ Process Excellence engagements run on the DMAIC framework — five phases proven across decades of operational improvement work, adapted to your scale and context. +

+
+ +
+
+
D
+
+

Define

+

Scope the problem and the goal. Anchor the engagement to a measurable target before any work begins.

+
+
+
+
M
+
+

Measure

+

Capture the current state with hard data — cycle times, defects, hand-offs, costs. Evidence over opinion.

+
+
+
+
A
+
+

Analyse

+

Find the root causes. Separate symptoms from structural drivers using value-stream mapping and analysis.

+
+
+
+
I
+
+

Improve

+

Design and pilot the fix with your team. Prove it works on a focused area before scaling.

+
+
+
+
C
+
+

Control

+

Lock in the gains. Standard work and visual controls hold the improvement — and feed the next cycle.

+
+
+
+ +
+ Why DMAIC? It's the most widely-adopted process improvement framework in the world — used across manufacturing, healthcare, and service industries for over 30 years. We apply it well, adapted to small-to-mid businesses where most consultancies overcomplicate it. +
+
+
+ + +
+
+
+
What this looks like in practice
+

Where Process Excellence delivers real value.

+

+ Two illustrative scenarios showing what kind of work this looks like and what kind of results typically follow. Real engagements are scoped to your specific operation. +

+
+ +
+
+
+ Manufacturing line at work +
+ + + Manufacturing & operations + +

Reducing lead time on a manual production line.

+

Customers waiting, work piling up, the team busy but not productive. We value-stream map the production flow, identify hand-off bottlenecks, and redesign standard work to eliminate wait states between stations.

+
+
+
−30 to −50%*
+
Lead time reduction
+
+
+
+20 to +40%*
+
Throughput on the same headcount
+
+
+
+ +
+
+ Office team reviewing process workflows +
+ + + Services & admin + +

Cutting cycle time on quote-to-cash.

+

Deals taking weeks to quote, errors slipping through, sales chasing internal teams for status. We map the full quote-to-invoice flow across departments, eliminate duplicate hand-offs, and standardise the request-to-approval flow.

+
+
+
−40 to −70%*
+
Cycle time, quote to invoice
+
+
+
−50 to −80%*
+
Errors and rework
+
+
+
+
+ +
+
+ + +
+
+
+
The engagement
+

A focused project. Often, the start of a partnership.

+

+ Most engagements begin as a focused project — small, medium, or large depending on scope. The work that delivers tends to grow into an ongoing partnership over time. +

+
+ + +
+
+ + +
+

+ *About these ranges. These are illustrative scenarios drawn from typical Lean improvement work — not specific client results. Real engagements are tailored to your business; scope, sector, and starting capability all shape what's achievable. +

+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
+
+ + + + + + + + + + diff --git a/lean-101-website-html-v3/services/digital-solutions.html b/lean-101-website-html-v3/services/digital-solutions.html new file mode 100644 index 0000000..e1884f9 --- /dev/null +++ b/lean-101-website-html-v3/services/digital-solutions.html @@ -0,0 +1,2183 @@ + + + + + +Digital Solutions — Lean 101 + + + + + + + + + + + + + + + +
+
+
+ Digital Solutions +

Custom apps, dashboards & automation.

+

+ From Power BI dashboards to custom web apps that replace clunky spreadsheets — we build tailored software that streamlines your operations and makes data work for you. Built by people who understand the operations they're improving. +

+ +
+
+ Operational dashboard with charts and live data +
+
+
+ + +
+
+
+ Person reviewing data and analytics on a screen +
+
+
Who it's for
+

Built for businesses where the spreadsheets have outgrown themselves.

+

+ Most of our Digital Solutions work starts as: "we're running this on Excel and it's gotten out of hand." Custom software built for your operations replaces the duct tape — without ripping up what already works. +

+
+ +
+
+
+ +
+

Critical processes run on a single Excel file

+

One person knows the formulas. The file emails around as v17, v18, v18-final, v18-final-FINAL. When that person is on leave, work stops.

+
+
+
+ +
+

Reporting takes a day, not a click

+

End-of-month, someone manually pulls data from three systems, copies it into a deck, formats charts, sends it round. By the time it lands, it's already old.

+
+
+
+ +
+

You're paying for SaaS that doesn't fit

+

You bought an off-the-shelf tool that does 60% of what you need, and your team works around the missing 40% in spreadsheets. The cost compounds — money out the door, time wasted on workarounds.

+
+
+
+ +
+

The data exists — but no one can see it

+

Your CRM, accounting system, and project tools all hold useful data. But there's no single view that answers the simple operational questions you ask every week.

+
+
+
+
+ + + +
+
+
+
What we build
+

Three kinds of builds, one disciplined method.

+

+ Most Digital Solutions engagements land in one of three categories. Each is built the same way — understand the work, ship a focused MVP, iterate with users, hand it over fully owned. +

+
+ +
+
+ +
+ Dashboards +

Operational visibility, refreshed automatically.

+

Power BI dashboards that pull from your real systems and answer the questions you ask each week — production output, lead times, defect rates, capacity, financial performance. End-of-month reporting goes from a day to a click.

+
    +
  • Real-time data from your existing systems
  • +
  • Tailored views per role (operators, supervisors, leadership)
  • +
  • Automated email digests and alerts
  • +
+
+
+ +
+ +
+ Web apps +

Custom apps that replace clunky spreadsheets.

+

Tailored web applications for the workflows that have outgrown Excel — order tracking, scheduling, inventory, quality logs, audit checklists. Multi-user, role-aware, accessible from anywhere, no more "v18-final-FINAL.xlsx."

+
    +
  • One source of truth, no more conflicting copies
  • +
  • Mobile-friendly so frontline teams can use it on the floor
  • +
  • Full audit trail of who changed what and when
  • +
+
+
+ +
+ +
+ Automation +

Repetitive admin work, automated away.

+

The copy-paste between systems, the manual report generation, the daily reconciliation — all the work nobody wants to do but somebody has to. Automated using Power Automate, Python scripts, or whatever tool fits the job.

+
    +
  • Hours of admin work removed from your team's week
  • +
  • Fewer errors from manual data entry
  • +
  • Triggered notifications when things need human attention
  • +
+
+
+
+ + +
+
+ How we build +

Same four phases, every build.

+
+
+
+
+
+ + + + +
+
01
+
+
Understand the work
+

We watch the process before we touch code. Talk to the people who'll use it. Document what's actually happening today, not what the org chart says.

+
+
+
+
+ + + + + + +
+
02
+
+
Ship the MVP
+

A focused first version, working on real data within weeks. Not pretty yet, but real — running in your environment, doing one job well.

+
+
+
+
+ + + + +
+
03
+
+
Iterate with users
+

Your team uses it. We watch how. Friction surfaces fast. We fix what matters, drop what doesn't, add the features that earn their place.

+
+
+
+
+ + + + +
+
04
+
+
Hand it over
+

Documentation, admin training, source code, deployment guide. Your team can run, extend, and modify the tool — we're a call away, not a dependency.

+
+
+
+
+
+ + +
+
+
+
What this looks like in practice
+

Where Digital Solutions delivers real value.

+

+ Two illustrative scenarios showing what kind of build this looks like and what kind of impact typically follows. Real projects are scoped to your operations, systems, and team. +

+
+ +
+
+
+ Live operational dashboard with charts +
+ + + Operational dashboard + +

Replacing manual reporting with live Power BI dashboards.

+

End-of-month reporting takes a day, the data's old by the time it's seen, decisions are made on gut. We connect your existing systems to a Power BI workspace, design role-aware views for operators, supervisors, and leadership, and set up automated daily refresh.

+
+
+
−90 to −95%*
+
Time spent compiling reports
+
+
+
3–6 weeks*
+
Typical build time
+
+
+
+ +
+
+ Custom application interface on a clean screen +
+ + + Custom web application + +

Replacing a critical spreadsheet with a custom web app.

+

A single Excel file runs a critical process, one person knows the formulas, the file emails around as v17, v18, v18-final. We map the actual workflow, build a tailored multi-user web app with role-based access, and migrate live data — running the new tool alongside the spreadsheet during cutover.

+
+
+
−60 to −80%*
+
Errors from manual data entry
+
+
+
8–16 weeks*
+
Typical build time
+
+
+
+
+ +
+
+ + +
+
+
+
The engagement
+

A focused build. Often, the start of a partnership.

+

+ Most builds begin as a focused project — a dashboard, a custom app, or a multi-system integration. The work that delivers tends to grow into an ongoing technology partnership as your operations evolve and tooling needs grow. +

+
+ + +
+
+ + +
+

+ *About these ranges. These are illustrative scenarios drawn from typical custom-software builds for SMBs — not specific client results. Real projects are scoped to your existing systems, integrations needed, and the workflow the tool needs to support. +

+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
+
+ + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..af5d545 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2377 @@ +{ + "name": "lean101-website", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lean101-website", + "version": "1.0.0", + "dependencies": { + "@sveltejs/adapter-node": "^5.5.4", + "mongodb": "^7.2.0", + "mongodb-memory-server": "^11.0.1" + }, + "devDependencies": { + "@sveltejs/kit": "^2.37.0", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@types/node": "^25.6.0", + "svelte": "^5.38.7", + "svelte-check": "^4.3.1", + "vite": "^7.1.4" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.9.tgz", + "integrity": "sha512-RXSxsokhAF/4nWys8An8npsqOI33Ex1Hlzqjw2pZOO+GKtMAR2noGnUdsFiGwsaO/xXI+56mtjTmDA3JXJsvmA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", + "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.59.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.58.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.58.0.tgz", + "integrity": "sha512-kT9GCN8yJTkCK1W+Gi/bvGooWAM7y7WXP+yd+rf6QOIjyoK1ERPrMwSufXJUNu2pMWIqruhFvmz+LbOqsEmKmA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", + "license": "MIT", + "dependencies": { + "obug": "^2.1.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/devalue": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", + "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", + "license": "MIT" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", + "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz", + "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.2.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.0.1.tgz", + "integrity": "sha512-nUlKovSJZBh7q5hPsewFRam9H66D08Ne18nyknkNalfXMPtK1Og3kOcuqQhcX88x/pghSZPIJHrLbxNFW3OWiw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "mongodb-memory-server-core": "11.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.0.1.tgz", + "integrity": "sha512-IcIb2S9Xf7Lmz43Z1ZujMqNg7PU5Q7yn+4wOnu7l6pfeGPkEmlqzV1hIbroVx8s4vXhPB1oMGC1u8clW7aj3Xw==", + "license": "MIT", + "dependencies": { + "async-mutex": "^0.5.0", + "camelcase": "^6.3.0", + "debug": "^4.4.3", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.11", + "https-proxy-agent": "^7.0.6", + "mongodb": "^7.0.0", + "new-find-package-json": "^2.0.0", + "semver": "^7.7.3", + "tar-stream": "^3.1.7", + "tslib": "^2.8.1", + "yauzl": "^3.2.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/new-find-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz", + "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.55.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz", + "integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", + "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/yauzl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f9af0a8 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "lean101-website", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "node build", + "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "mongo:demo": "node scripts/mongo-demo.mjs" + }, + "devDependencies": { + "@sveltejs/kit": "^2.37.0", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@types/node": "^25.6.0", + "svelte": "^5.38.7", + "svelte-check": "^4.3.1", + "vite": "^7.1.4" + }, + "dependencies": { + "@sveltejs/adapter-node": "^5.5.4", + "mongodb": "^7.2.0", + "mongodb-memory-server": "^11.0.1" + } +} diff --git a/scripts/deploy-do.sh b/scripts/deploy-do.sh new file mode 100644 index 0000000..a50ec37 --- /dev/null +++ b/scripts/deploy-do.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + sudo ./scripts/deploy-do.sh --domain example.com [options] + +Required: + --domain DOMAIN Primary domain name, for example lean-101.com + +Optional: + --email EMAIL Email for Let's Encrypt. If omitted, TLS is skipped. + --app-dir PATH Project directory on the server. + Default: current repo root + --app-port PORT Local Docker port exposed by compose. + Default: 8080 + --with-www Also configure and request TLS for www.DOMAIN + --skip-certbot Skip Let's Encrypt even if --email is provided + --help Show this help + +Examples: + sudo ./scripts/deploy-do.sh --domain lean-101.com --email ops@example.com --with-www + sudo ./scripts/deploy-do.sh --domain lean-101.com --skip-certbot +EOF +} + +require_root() { + if [[ "${EUID}" -ne 0 ]]; then + echo "Run this script with sudo or as root." + exit 1 + fi +} + +require_command() { + local command_name="$1" + if ! command -v "${command_name}" >/dev/null 2>&1; then + echo "Missing required command: ${command_name}" + exit 1 + fi +} + +pick_compose_package() { + if apt-cache show docker-compose-plugin >/dev/null 2>&1; then + echo "docker-compose-plugin" + return + fi + + if apt-cache show docker-compose-v2 >/dev/null 2>&1; then + echo "docker-compose-v2" + return + fi + + echo "" +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +DOMAIN="" +EMAIL="" +APP_DIR="${REPO_ROOT}" +APP_PORT="8080" +WITH_WWW="false" +SKIP_CERTBOT="false" + +while [[ $# -gt 0 ]]; do + case "$1" in + --domain) + DOMAIN="${2:-}" + shift 2 + ;; + --email) + EMAIL="${2:-}" + shift 2 + ;; + --app-dir) + APP_DIR="${2:-}" + shift 2 + ;; + --app-port) + APP_PORT="${2:-}" + shift 2 + ;; + --with-www) + WITH_WWW="true" + shift + ;; + --skip-certbot) + SKIP_CERTBOT="true" + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" + usage + exit 1 + ;; + esac +done + +if [[ -z "${DOMAIN}" ]]; then + echo "--domain is required." + usage + exit 1 +fi + +require_root +require_command apt-get + +if [[ ! -f "${APP_DIR}/docker-compose.yml" ]]; then + echo "Could not find docker-compose.yml in ${APP_DIR}" + exit 1 +fi + +if [[ ! -f "${APP_DIR}/Dockerfile" ]]; then + echo "Could not find Dockerfile in ${APP_DIR}" + exit 1 +fi + +APT_PACKAGES=( + software-properties-common + docker.io + nginx + certbot + python3-certbot-nginx +) + +echo "Installing server packages..." +apt-get update +DEBIAN_FRONTEND=noninteractive apt-get install -y software-properties-common +add-apt-repository -y universe +apt-get update + +COMPOSE_PACKAGE="$(pick_compose_package)" + +if [[ -z "${COMPOSE_PACKAGE}" ]]; then + echo "Could not find a Docker Compose package via apt." + echo "Expected either docker-compose-plugin or docker-compose-v2." + exit 1 +fi + +APT_PACKAGES+=("${COMPOSE_PACKAGE}") +DEBIAN_FRONTEND=noninteractive apt-get install -y "${APT_PACKAGES[@]}" + +echo "Enabling services..." +systemctl enable --now docker +systemctl enable --now nginx + +if command -v ufw >/dev/null 2>&1; then + echo "Configuring UFW rules..." + ufw allow OpenSSH >/dev/null 2>&1 || true + ufw allow 'Nginx Full' >/dev/null 2>&1 || true +fi + +PUBLIC_SITE_URL="https://${DOMAIN}" +echo "Writing ${APP_DIR}/.env..." +cat > "${APP_DIR}/.env" < "${APP_DIR}/docker-compose.override.yml" < "${NGINX_SITE}" < { + await mongo.stop(); + process.exit(0); +}; + +process.on('SIGINT', shutdown); +process.on('SIGTERM', shutdown); diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..eb17d1d --- /dev/null +++ b/src/app.css @@ -0,0 +1,34 @@ +:root { + color-scheme: light; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +img { + display: block; + max-width: 100%; +} + +button, +input, +textarea, +select { + font: inherit; +} + +a { + color: inherit; +} diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..c5565e1 --- /dev/null +++ b/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/lib/components/LegacyHtmlPage.svelte b/src/lib/components/LegacyHtmlPage.svelte new file mode 100644 index 0000000..bb8bd56 --- /dev/null +++ b/src/lib/components/LegacyHtmlPage.svelte @@ -0,0 +1,82 @@ + + + + {page.title} + {#if page.description} + + {/if} + + + + + + + + + + + + + + + + + + + + {#if page.structuredData} + + {/if} + + +
+ {@html page.body} +
diff --git a/src/lib/components/SitePage.svelte b/src/lib/components/SitePage.svelte new file mode 100644 index 0000000..24c03e6 --- /dev/null +++ b/src/lib/components/SitePage.svelte @@ -0,0 +1,379 @@ + + + + {page.meta.pageTitle} + + + + + + + + + + + + + + + + + + + + {#if structuredData} + + {/if} + + + + +{#each page.sections as section} + {#if section.type === 'hero'} + {@const hero = section.data} +
+ {hero.eyebrow} +

+ {hero.titleLead} + {#if hero.titleEmphasis} +
+ {hero.titleEmphasis} + {/if} +

+

{hero.subtext}

+ + +
+ {hero.image.alt} + {#if hero.stats?.length} +
+ {#each hero.stats as stat} +
+
{stat.label}
+
{stat.value}
+
+ {/each} +
+ {/if} +
+
+ {:else if section.type === 'services'} + {@const block = section.data} +
+
+
+
{block.eyebrow}
+

{block.title}

+

{block.lede}

+
+ +
+ {#each block.cards as card} +
+
+ {card.tag} + {card.image.alt} +
+
+

{card.title}

+

{card.description}

+
+
+ {/each} +
+
+
+ {:else if section.type === 'process'} + {@const block = section.data} +
+
+
+
{block.eyebrow}
+

{block.title}

+

{block.lede}

+
+ +
+ {#each block.steps as step, index} +
+
{step.label}
+

{step.title}

+

{step.description}

+
{step.outcome}
+
+ + {#if index < block.steps.length - 1} + + {/if} + {/each} +
+
+
+ {:else if section.type === 'outcomes'} + {@const block = section.data} +
+
+
+
{block.eyebrow}
+

{block.title}

+

{block.lede}

+
+ +
+ {#each block.items as item, index} +
+
+ {#if index === 0} + + {:else if index === 1} + + {:else if index === 2} + + {:else if index === 3} + + {:else} + + {/if} +
+ {item.number} +

{item.title}

+

{item.description}

+
+ {/each} +
+
+
+ {:else if section.type === 'why'} + {@const block = section.data} +
+
+
+
+
{block.eyebrow}
+

{block.title}

+

{block.lede}

+
+ +
    + {#each block.items as item, index} +
  • +
    + {#if index === 0} + + {:else if index === 1} + + {:else if index === 2} + + {:else} + + {/if} +
    +
    +

    {item.title}

    +

    {item.description}

    +
    +
  • + {/each} +
+
+
+
+ {:else if section.type === 'richText'} + {@const block = section.data} +
+
+ {#if block.eyebrow} +
{block.eyebrow}
+ {/if} + {#if block.title} +

{block.title}

+ {/if} +
+ {#each toParagraphs(block.body) as paragraph} +

{paragraph}

+ {/each} +
+
+
+ {:else if section.type === 'cta'} + {@const block = section.data} +
+
+
+

+ {block.titleLead} {block.titleEmphasis} {block.titleTail} +

+

{block.body}

+ +
+ {#each block.flowSteps as step, index} +
{index + 1} {step}
+ {#if index < block.flowSteps.length - 1} + + {/if} + {/each} +
+ + +
+
+
+ {/if} +{/each} + +
+ +
diff --git a/src/lib/content/homepage.json b/src/lib/content/homepage.json new file mode 100644 index 0000000..8d02fc8 --- /dev/null +++ b/src/lib/content/homepage.json @@ -0,0 +1,251 @@ +{ + "meta": { + "pageTitle": "Lean 101 - Smarter Improvement Solutions", + "metaDescription": "Practical consulting, coaching and analytics to help businesses eliminate waste, improve performance, and build a culture of continuous improvement.", + "socialDescription": "Practical consulting, coaching and analytics to help businesses eliminate waste, improve performance, and build a culture that keeps improving.", + "contactEmail": "hello@lean-101.com", + "linkedinUrl": "https://www.linkedin.com/company/lean101/", + "copyrightYear": "2026", + "location": "Based in Melbourne, working worldwide." + }, + "nav": { + "links": [ + { + "label": "Services", + "href": "#services" + }, + { + "label": "How it works", + "href": "#process" + }, + { + "label": "Why Lean 101", + "href": "#why" + }, + { + "label": "Contact", + "href": "#contact" + } + ], + "ctaLabel": "Book a call", + "ctaHref": "#contact" + }, + "hero": { + "eyebrow": "Smarter Improvement Solutions", + "titleLead": "Continuous improvement,", + "titleEmphasis": "made simple.", + "subtext": "Practical consulting, coaching and analytics to help businesses eliminate waste, improve performance, and build a culture that keeps improving - long after we've gone.", + "primaryCta": { + "label": "Book a free intro call", + "href": "#contact" + }, + "secondaryCta": { + "label": "See how it works", + "href": "#process" + }, + "image": { + "src": "https://images.unsplash.com/photo-1542744173-8e7e53415bb0?auto=format&fit=crop&w=1600&q=80", + "alt": "Team collaborating on a process improvement workshop" + }, + "stats": [ + { + "label": "Efficiency", + "value": "+35%", + "tone": "up" + }, + { + "label": "Cost savings", + "value": "-27%", + "tone": "warn" + }, + { + "label": "Engagement", + "value": "+41%", + "tone": "morale" + } + ] + }, + "services": { + "eyebrow": "What we do", + "title": "Three services. One outcome: improvement that lasts.", + "lede": "Frameworks aren't the goal - results are. Every engagement combines consulting, capability building, and digital solutions - sized to what your team can sustain.", + "cards": [ + { + "tag": "Consulting", + "tagTone": "default", + "title": "Process Excellence & Transformation", + "description": "We diagnose where work is breaking down, design optimised processes, and run the change with you - not just for you.", + "image": { + "src": "https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=900&q=80", + "alt": "Consultant mapping out a process on a whiteboard" + } + }, + { + "tag": "Coaching & Training", + "tagTone": "orange", + "title": "Building capability and culture", + "description": "Tailored coaching and bespoke training that turns Lean theory into everyday practice. Your team owns the improvement.", + "image": { + "src": "https://images.unsplash.com/photo-1543269865-cbf427effbad?auto=format&fit=crop&w=900&q=80", + "alt": "Coach working with a small team in a workshop setting" + } + }, + { + "tag": "Digital Solutions", + "tagTone": "charcoal", + "title": "Custom apps, dashboards & automation", + "description": "From Power BI dashboards to custom web apps that replace clunky spreadsheets - we build tailored software that streamlines your operations and makes data work for you.", + "image": { + "src": "https://images.unsplash.com/photo-1551434678-e076c223a692?auto=format&fit=crop&w=900&q=80", + "alt": "Custom software being developed on a laptop" + } + } + ] + }, + "process": { + "eyebrow": "How it works", + "title": "A clear path from waste to flow.", + "lede": "Every engagement follows globally recognised improvement frameworks - adapted to your scale, industry and team capability.", + "steps": [ + { + "label": "Step 01", + "title": "Diagnose", + "description": "Process mapping, audits and a maturity assessment. We walk your operation alongside you, talk to the people doing the work, and quantify where the friction sits.", + "outcome": "By the end: a clear map of where time, quality and cost are leaking - and which fix moves the dial first." + }, + { + "label": "Step 02", + "title": "Design & pilot", + "description": "We co-design the future state with your team, then run a focused pilot in one area. Real data, real people, real results - before we touch the rest of the business.", + "outcome": "By the end: a proven future state, validated with hard data on a focused pilot." + }, + { + "label": "Step 03", + "title": "Roll out", + "description": "Once the pilot proves the gains, we scale the approach across other teams, sites or business units - adapted to each context, with the same disciplined method.", + "outcome": "By the end: the gains scaled across teams, sites or business units - same method, each context." + }, + { + "label": "Step 04", + "title": "Sustain", + "description": "Coaching, dashboards and train-the-trainer programs so the improvement becomes how your team operates - without us in the room.", + "outcome": "By the end: the improvement runs without us - your team owns it, the data proves it." + } + ] + }, + "outcomes": { + "eyebrow": "What good looks like", + "title": "The five things continuous improvement moves.", + "lede": "Industry benchmarks for organisations that commit to a structured improvement program. Whatever your industry, these are the levers that shift.", + "items": [ + { + "number": "01", + "title": "Cycle time", + "description": "Get work through faster - fewer handoffs, less waiting, shorter lead times." + }, + { + "number": "02", + "title": "Quality", + "description": "Fewer errors, less rework, happier customers - fixed at the source, not at the end." + }, + { + "number": "03", + "title": "Cost", + "description": "Eliminate waste from how work is done - and the savings drop straight to the bottom line." + }, + { + "number": "04", + "title": "Morale", + "description": "When people fix the things that frustrate them, engagement and retention follow." + }, + { + "number": "05", + "title": "Safety", + "description": "Standard work and visual controls reduce incidents - protecting people and uptime alike." + } + ] + }, + "why": { + "eyebrow": "Why Lean 101", + "title": "One framework. Customised to your business.", + "lede": "Lean 101 is a one-on-one partnership. We bring globally recognised frameworks - Lean, Six Sigma, continuous improvement best practice - and adapt them to how your business actually runs. Every company, every industry is different, so we work alongside you to solve the problems that matter to you - not deliver a generic playbook.", + "items": [ + { + "title": "Globally recognised frameworks, practically applied", + "description": "Lean, Six Sigma, and continuous improvement best practice - adapted to how your business actually runs, not lifted from a generic playbook." + }, + { + "title": "Customised to each business, not off the shelf", + "description": "Every business is different. Focused coaching, a single process redesign, or full transformation - we shape the engagement around your priorities, capacity, and budget." + }, + { + "title": "We build capability, not dependency", + "description": "Our success metric is the day you no longer need us. Train-the-trainer, embedded dashboards, and clear playbooks make the improvement stick." + }, + { + "title": "Data-led from day one", + "description": "Power BI dashboards and automated reporting come standard. You see what's working, what isn't, and what to do next - in real time." + } + ] + }, + "cta": { + "titleLead": "Let's find", + "titleEmphasis": "your", + "titleTail": "first improvement.", + "body": "Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together - and from there we'll scope a tailored partnership.", + "flowSteps": [ + "30-min intro call", + "1-hour site visit", + "Tailored partnership" + ], + "primaryCta": { + "label": "Book a free intro call", + "href": "mailto:hello@lean-101.com" + }, + "secondaryCta": { + "label": "hello@lean-101.com", + "href": "mailto:hello@lean-101.com" + } + }, + "footer": { + "brandTagline": "For businesses that want change to last.", + "servicesLinks": [ + { + "label": "Consulting", + "href": "#services" + }, + { + "label": "Coaching & Training", + "href": "#services" + }, + { + "label": "Analytics", + "href": "#services" + } + ], + "companyLinks": [ + { + "label": "About", + "href": "#why" + }, + { + "label": "Approach", + "href": "#process" + }, + { + "label": "Contact", + "href": "#contact" + } + ], + "connectLinks": [ + { + "label": "LinkedIn", + "href": "https://www.linkedin.com/company/lean101/" + }, + { + "label": "Email", + "href": "mailto:hello@lean-101.com" + } + ] + } +} diff --git a/src/lib/server/auth.js b/src/lib/server/auth.js new file mode 100644 index 0000000..90c6493 --- /dev/null +++ b/src/lib/server/auth.js @@ -0,0 +1,109 @@ +import crypto from 'node:crypto'; +import { env } from '$env/dynamic/private'; + +export const ADMIN_COOKIE_NAME = 'lean101_admin_session'; +const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 7; + +/** @param {string} value */ +function base64urlEncode(value) { + return Buffer.from(value).toString('base64url'); +} + +/** @param {string} value */ +function base64urlDecode(value) { + return Buffer.from(value, 'base64url').toString('utf8'); +} + +function getSessionSecret() { + return env.ADMIN_SESSION_SECRET || env.SESSION_SECRET || ''; +} + +/** @param {string} payload */ +function signPayload(payload) { + return crypto.createHmac('sha256', getSessionSecret()).update(payload).digest('base64url'); +} + +export function isAdminConfigured() { + return Boolean(env.ADMIN_PASSWORD && getSessionSecret()); +} + +export function getAdminConfigIssues() { + const issues = []; + + if (!env.ADMIN_PASSWORD) { + issues.push('Set ADMIN_PASSWORD to enable login.'); + } + + if (!getSessionSecret()) { + issues.push('Set ADMIN_SESSION_SECRET or SESSION_SECRET to sign admin sessions.'); + } + + return issues; +} + +export function createAdminSessionToken() { + const payload = JSON.stringify({ + exp: Date.now() + SESSION_TTL_MS + }); + const encodedPayload = base64urlEncode(payload); + const signature = signPayload(encodedPayload); + return `${encodedPayload}.${signature}`; +} + +/** @param {string} password */ +export function isValidAdminPassword(password) { + if (!env.ADMIN_PASSWORD) { + return false; + } + + const expected = Buffer.from(env.ADMIN_PASSWORD); + const received = Buffer.from(password || ''); + + if (expected.length !== received.length) { + return false; + } + + return crypto.timingSafeEqual(expected, received); +} + +/** @param {string | undefined} token */ +export function verifyAdminSessionToken(token) { + if (!token || !isAdminConfigured()) { + return false; + } + + const [encodedPayload, signature] = token.split('.'); + + if (!encodedPayload || !signature) { + return false; + } + + const expectedSignature = signPayload(encodedPayload); + const expected = Buffer.from(expectedSignature); + const received = Buffer.from(signature); + + if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) { + return false; + } + + try { + const payload = JSON.parse(base64urlDecode(encodedPayload)); + return typeof payload.exp === 'number' && payload.exp > Date.now(); + } catch { + return false; + } +} + +/** + * @param {URL} url + * @returns {{ path: string, httpOnly: boolean, sameSite: 'lax', secure: boolean, maxAge: number }} + */ +export function getAdminCookieOptions(url) { + return { + path: '/admin', + httpOnly: true, + sameSite: /** @type {'lax'} */ ('lax'), + secure: url.protocol === 'https:', + maxAge: SESSION_TTL_MS / 1000 + }; +} diff --git a/src/lib/server/content.js b/src/lib/server/content.js new file mode 100644 index 0000000..6c50370 --- /dev/null +++ b/src/lib/server/content.js @@ -0,0 +1,486 @@ +// @ts-nocheck +import { env } from '$env/dynamic/private'; +import { MongoClient } from 'mongodb'; +import homepageSeed from '$lib/content/homepage.json'; + +const PAGE_COLLECTION = 'site_pages'; +const BLOG_COLLECTION = 'blog_posts'; +const LEGACY_COLLECTION = 'site_content'; +const LEGACY_HOMEPAGE_KEY = 'homepage'; + +/** @type {Promise | undefined} */ +let mongoClientPromise; + +function clone(value) { + return structuredClone(value); +} + +/** @param {unknown} value */ +function isPlainObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** @param {string | undefined} value */ +function slugify(value) { + return String(value || '') + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function buildDefaultNav() { + return clone(homepageSeed.nav); +} + +function buildDefaultFooter() { + return clone(homepageSeed.footer); +} + +function buildSection(id, type, name, anchor, data) { + return { + id, + type, + name, + anchor, + data: clone(data) + }; +} + +function buildHomepagePageSeed() { + return { + slug: 'home', + title: 'Homepage', + path: '/', + meta: clone(homepageSeed.meta), + nav: buildDefaultNav(), + footer: buildDefaultFooter(), + sections: [ + buildSection('hero', 'hero', 'Hero', null, homepageSeed.hero), + buildSection('services', 'services', 'Services', 'services', homepageSeed.services), + buildSection('process', 'process', 'How It Works', 'process', homepageSeed.process), + buildSection('outcomes', 'outcomes', 'Outcomes', null, homepageSeed.outcomes), + buildSection('why', 'why', 'Why Lean 101', 'why', homepageSeed.why), + buildSection('cta', 'cta', 'Contact CTA', 'contact', homepageSeed.cta) + ] + }; +} + +function buildGenericPageTemplate(input = {}) { + const slug = slugify(input.slug) || 'new-page'; + const title = input.title || 'New page'; + return { + slug, + title, + path: slug === 'home' ? '/' : `/${slug}`, + meta: { + pageTitle: title, + metaDescription: '', + socialDescription: '', + contactEmail: homepageSeed.meta.contactEmail, + linkedinUrl: homepageSeed.meta.linkedinUrl, + copyrightYear: homepageSeed.meta.copyrightYear, + location: homepageSeed.meta.location + }, + nav: buildDefaultNav(), + footer: buildDefaultFooter(), + sections: [ + buildSection('hero', 'hero', 'Hero', null, { + eyebrow: 'Page intro', + titleLead: title, + titleEmphasis: '', + subtext: 'Add the opening message for this page.', + primaryCta: { label: 'Contact us', href: '#contact' }, + secondaryCta: { label: 'Learn more', href: '#content' }, + image: { + src: homepageSeed.hero.image.src, + alt: homepageSeed.hero.image.alt + }, + stats: [] + }), + buildSection('content', 'richText', 'Rich Text', 'content', { + eyebrow: 'Overview', + title: title, + body: + 'Add your page content here. Separate paragraphs with a blank line.\n\nUse this section for general content pages.' + }), + buildSection('cta', 'cta', 'Contact CTA', 'contact', clone(homepageSeed.cta)) + ] + }; +} + +function buildBlogPostTemplate(input = {}) { + const slug = slugify(input.slug || input.title) || 'new-post'; + const title = input.title || 'New blog post'; + return { + slug, + title, + excerpt: 'Add a short summary for this post.', + coverImage: homepageSeed.hero.image.src, + coverAlt: homepageSeed.hero.image.alt, + author: 'Lean 101', + publishedAt: new Date().toISOString(), + status: 'draft', + body: + 'Start writing here.\n\nSeparate paragraphs with a blank line for simple formatting.', + tags: ['insight'] + }; +} + +function createLegacyHomePage(content) { + const seed = buildHomepagePageSeed(); + return { + ...seed, + meta: { ...seed.meta, ...(content.meta || {}) }, + nav: { ...seed.nav, ...(content.nav || {}) }, + footer: { ...seed.footer, ...(content.footer || {}) }, + sections: seed.sections.map((section) => { + const legacyData = content[section.id]; + return legacyData ? { ...section, data: clone(legacyData) } : section; + }) + }; +} + +/** + * @param {unknown} seedValue + * @param {unknown} incomingValue + * @returns {unknown} + */ +function mergeWithSeed(seedValue, incomingValue) { + if (Array.isArray(seedValue)) { + return Array.isArray(incomingValue) ? incomingValue : clone(seedValue); + } + + if (!isPlainObject(seedValue)) { + return incomingValue ?? seedValue; + } + + /** @type {Record} */ + const merged = {}; + const seedObject = /** @type {Record} */ (seedValue); + const incomingObject = /** @type {Record} */ ( + isPlainObject(incomingValue) ? incomingValue : {} + ); + + for (const key of Object.keys(seedObject)) { + merged[key] = mergeWithSeed(seedObject[key], incomingObject[key]); + } + + for (const key of Object.keys(incomingObject)) { + if (!(key in merged)) { + merged[key] = incomingObject[key]; + } + } + + return merged; +} + +/** @param {unknown[]} sections */ +function normalizeSections(sections) { + if (!Array.isArray(sections)) { + return []; + } + + return sections + .filter(isPlainObject) + .map((section, index) => { + const value = /** @type {Record} */ (section); + const type = typeof value.type === 'string' ? value.type : 'richText'; + return { + id: typeof value.id === 'string' ? value.id : `${type}-${index + 1}`, + type, + name: + typeof value.name === 'string' + ? value.name + : type.charAt(0).toUpperCase() + type.slice(1), + anchor: typeof value.anchor === 'string' && value.anchor ? value.anchor : null, + data: isPlainObject(value.data) ? clone(value.data) : {} + }; + }); +} + +/** @param {unknown} page */ +export function normalizePageDocument(page) { + const input = isPlainObject(page) ? /** @type {Record} */ (page) : {}; + const seed = + slugify(/** @type {string | undefined} */ (input.slug)) === 'home' + ? buildHomepagePageSeed() + : buildGenericPageTemplate({ + slug: /** @type {string | undefined} */ (input.slug), + title: /** @type {string | undefined} */ (input.title) + }); + + const slug = slugify(/** @type {string | undefined} */ (input.slug)) || seed.slug; + const title = typeof input.title === 'string' && input.title.trim() ? input.title.trim() : seed.title; + + const normalized = { + slug, + title, + path: slug === 'home' ? '/' : `/${slug}`, + meta: /** @type {any} */ (mergeWithSeed(seed.meta, input.meta)), + nav: /** @type {any} */ (mergeWithSeed(seed.nav, input.nav)), + footer: /** @type {any} */ (mergeWithSeed(seed.footer, input.footer)), + sections: normalizeSections( + Array.isArray(input.sections) && input.sections.length ? input.sections : seed.sections + ) + }; + + return normalized; +} + +/** @param {unknown} post */ +export function normalizeBlogPost(post) { + const input = isPlainObject(post) ? /** @type {Record} */ (post) : {}; + const seed = buildBlogPostTemplate({ + slug: /** @type {string | undefined} */ (input.slug), + title: /** @type {string | undefined} */ (input.title) + }); + + const normalized = /** @type {any} */ (mergeWithSeed(seed, input)); + normalized.slug = slugify(normalized.slug || normalized.title) || seed.slug; + normalized.title = String(normalized.title || seed.title).trim() || seed.title; + normalized.status = normalized.status === 'published' ? 'published' : 'draft'; + normalized.tags = Array.isArray(normalized.tags) + ? normalized.tags.map((tag) => String(tag).trim()).filter(Boolean) + : []; + normalized.path = `/blog/${normalized.slug}`; + return normalized; +} + +export function createBlankPage(input = {}) { + return normalizePageDocument(buildGenericPageTemplate(input)); +} + +export function createBlankBlogPost(input = {}) { + return normalizeBlogPost(buildBlogPostTemplate(input)); +} + +export function hasDatabaseConfig() { + return Boolean(env.MONGODB_URI && env.MONGODB_DB); +} + +async function getMongoDb() { + if (!hasDatabaseConfig()) { + return null; + } + + if (!mongoClientPromise) { + const mongoUri = /** @type {string} */ (env.MONGODB_URI); + mongoClientPromise = new MongoClient(mongoUri).connect(); + } + + const client = await mongoClientPromise; + return client.db(/** @type {string} */ (env.MONGODB_DB)); +} + +async function getSafeDb() { + try { + return await getMongoDb(); + } catch { + return null; + } +} + +async function ensurePageSeeds(db) { + const pagesCollection = db.collection(PAGE_COLLECTION); + const existingHome = await pagesCollection.findOne({ slug: 'home' }); + + if (existingHome) { + return; + } + + const legacyCollection = db.collection(LEGACY_COLLECTION); + const legacyHome = await legacyCollection.findOne({ key: LEGACY_HOMEPAGE_KEY }); + const homePage = legacyHome?.content + ? createLegacyHomePage(legacyHome.content) + : buildHomepagePageSeed(); + const now = new Date(); + + await pagesCollection.insertOne({ + ...homePage, + createdAt: now, + updatedAt: now + }); +} + +export async function listPages() { + const db = await getSafeDb(); + + if (!db) { + return [buildHomepagePageSeed()]; + } + + await ensurePageSeeds(db); + + const pages = await db + .collection(PAGE_COLLECTION) + .find({}) + .sort({ slug: 1 }) + .toArray(); + + return pages + .map((page) => normalizePageDocument(page)) + .sort((a, b) => { + if (a.slug === 'home') return -1; + if (b.slug === 'home') return 1; + return a.title.localeCompare(b.title); + }); +} + +/** @param {string} slug */ +export async function getPageBySlug(slug) { + const normalizedSlug = slugify(slug) || 'home'; + const db = await getSafeDb(); + + if (!db) { + return normalizedSlug === 'home' ? buildHomepagePageSeed() : null; + } + + await ensurePageSeeds(db); + const page = await db.collection(PAGE_COLLECTION).findOne({ slug: normalizedSlug }); + return page ? normalizePageDocument(page) : null; +} + +/** @param {unknown} input */ +export async function savePage(input) { + const db = await getMongoDb(); + + if (!db) { + throw new Error('Database is not configured. Set MONGODB_URI and MONGODB_DB.'); + } + + const page = normalizePageDocument(input); + const now = new Date(); + + await db.collection(PAGE_COLLECTION).updateOne( + { slug: page.slug }, + { + $set: { + ...page, + updatedAt: now + }, + $setOnInsert: { + createdAt: now + } + }, + { upsert: true } + ); + + return { + page, + updatedAt: now + }; +} + +/** @param {string} slug */ +export async function deletePage(slug) { + const normalizedSlug = slugify(slug); + + if (!normalizedSlug || normalizedSlug === 'home') { + throw new Error('The homepage cannot be deleted.'); + } + + const db = await getMongoDb(); + + if (!db) { + throw new Error('Database is not configured. Set MONGODB_URI and MONGODB_DB.'); + } + + await db.collection(PAGE_COLLECTION).deleteOne({ slug: normalizedSlug }); +} + +export async function listBlogPosts(includeDrafts = true) { + const db = await getSafeDb(); + + if (!db) { + return []; + } + + const filter = includeDrafts ? {} : { status: 'published' }; + const posts = await db + .collection(BLOG_COLLECTION) + .find(filter) + .sort({ publishedAt: -1, title: 1 }) + .toArray(); + + return posts.map((post) => normalizeBlogPost(post)); +} + +/** @param {string} slug */ +export async function getBlogPostBySlug(slug, includeDrafts = false) { + const normalizedSlug = slugify(slug); + const db = await getSafeDb(); + + if (!db || !normalizedSlug) { + return null; + } + + const filter = includeDrafts + ? { slug: normalizedSlug } + : { slug: normalizedSlug, status: 'published' }; + const post = await db.collection(BLOG_COLLECTION).findOne(filter); + return post ? normalizeBlogPost(post) : null; +} + +/** @param {unknown} input */ +export async function saveBlogPost(input) { + const db = await getMongoDb(); + + if (!db) { + throw new Error('Database is not configured. Set MONGODB_URI and MONGODB_DB.'); + } + + const post = normalizeBlogPost(input); + const now = new Date(); + + await db.collection(BLOG_COLLECTION).updateOne( + { slug: post.slug }, + { + $set: { + ...post, + updatedAt: now + }, + $setOnInsert: { + createdAt: now + } + }, + { upsert: true } + ); + + return { + post, + updatedAt: now + }; +} + +/** @param {string} slug */ +export async function deleteBlogPost(slug) { + const normalizedSlug = slugify(slug); + const db = await getMongoDb(); + + if (!db) { + throw new Error('Database is not configured. Set MONGODB_URI and MONGODB_DB.'); + } + + if (!normalizedSlug) { + throw new Error('Blog post slug is required.'); + } + + await db.collection(BLOG_COLLECTION).deleteOne({ slug: normalizedSlug }); +} + +export async function listSitemapEntries() { + const pages = await listPages(); + const posts = await listBlogPosts(false); + + const pageEntries = pages.map((page) => ({ + path: page.path, + updatedAt: null + })); + const blogEntries = posts.map((post) => ({ + path: post.path, + updatedAt: post.publishedAt || null + })); + + return [...pageEntries, { path: '/blog', updatedAt: null }, ...blogEntries]; +} diff --git a/src/lib/site.js b/src/lib/site.js new file mode 100644 index 0000000..fca3ca6 --- /dev/null +++ b/src/lib/site.js @@ -0,0 +1,16 @@ +import { env } from '$env/dynamic/public'; + +const fallbackUrl = 'http://localhost:8080'; + +export const siteUrl = (env.PUBLIC_SITE_URL || fallbackUrl).replace(/\/$/, ''); + +export const seo = { + title: 'Lean 101 | Continuous Improvement Consulting That Sticks', + description: + 'Lean 101 helps teams remove waste, improve flow, build internal capability, and turn process improvement into measurable operating results.', + image: `${siteUrl}/assets/lean101-logotipo.png` +}; + +export function absoluteUrl(path = '/') { + return `${siteUrl}${path.startsWith('/') ? path : `/${path}`}`; +} diff --git a/src/lib/v3/pages.js b/src/lib/v3/pages.js new file mode 100644 index 0000000..d4ee4bc --- /dev/null +++ b/src/lib/v3/pages.js @@ -0,0 +1,123 @@ +// @ts-nocheck +import homeSource from './source/home.html?raw'; +import consultingSource from './source/services/consulting.html?raw'; +import coachingSource from './source/services/coaching.html?raw'; +import digitalSolutionsSource from './source/services/digital-solutions.html?raw'; + +const defaultKeywords = [ + 'lean consulting', + 'continuous improvement', + 'process excellence', + 'lean coaching', + 'digital solutions', + 'operational improvement' +]; + +function extract(source, pattern, fallback = '') { + const match = source.match(pattern); + return match ? match[1].trim() : fallback; +} + +function decodeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('—', '—') + .replaceAll('–', '–') + .replaceAll('"', '"') + .replaceAll(''', "'"); +} + +function normalizePathing(html) { + return html + .replaceAll('href="../index.html"', 'href="/"') + .replaceAll('href="services/consulting.html"', 'href="/services/consulting"') + .replaceAll('href="services/coaching.html"', 'href="/services/coaching"') + .replaceAll('href="services/digital-solutions.html"', 'href="/services/digital-solutions"') + .replaceAll('href="consulting.html"', 'href="/services/consulting"') + .replaceAll('href="coaching.html"', 'href="/services/coaching"') + .replaceAll('href="digital-solutions.html"', 'href="/services/digital-solutions"') + .replaceAll('href="../index.html#why"', 'href="/#why"') + .replaceAll('href="../index.html#process"', 'href="/#process"') + .replaceAll('href="../index.html#contact"', 'href="/#contact"') + .replaceAll('src="../assets/', 'src="/assets/') + .replaceAll('src="assets/', 'src="/assets/'); +} + +function extractFirstImage(body) { + const match = body.match(/]+src="([^"]+)"/i); + return match ? match[1] : '/assets/lean101-logotipo.png'; +} + +function buildStructuredData(page) { + const base = { + '@context': 'https://schema.org', + '@type': 'ProfessionalService', + name: 'Lean 101', + url: page.path, + image: page.image, + logo: '/assets/lean101-isotipo.png', + description: page.description, + email: 'hello@lean-101.com', + areaServed: 'Australia', + sameAs: ['https://www.linkedin.com/company/lean101/'] + }; + + if (page.path === '/') { + return { + ...base, + serviceType: [ + 'Process Excellence & Transformation', + 'Coaching & Training', + 'Digital Solutions' + ] + }; + } + + return { + ...base, + '@type': 'Service', + provider: { + '@type': 'Organization', + name: 'Lean 101', + url: '/' + }, + serviceType: page.title.replace(/\s+—\s+Lean 101$/, '') + }; +} + +function parsePage(source, path) { + const title = decodeHtml(extract(source, /([\s\S]*?)<\/title>/i, 'Lean 101')); + const description = extract( + source, + /<meta\s+name="description"\s+content="([\s\S]*?)"\s*\/?>/i, + '' + ); + const styles = extract(source, /<style>([\s\S]*?)<\/style>/i); + const body = normalizePathing(extract(source, /<body[^>]*>([\s\S]*?)<\/body>/i)); + const scripts = [...source.matchAll(/<script>([\s\S]*?)<\/script>/gi)].map((match) => match[1].trim()); + const image = extractFirstImage(body); + const page = { + title, + description: decodeHtml(description), + socialDescription: decodeHtml(description), + path, + image, + keywords: defaultKeywords.join(', '), + styles, + body, + scripts + }; + + return { + ...page, + structuredData: buildStructuredData(page) + }; +} + +export const homePage = parsePage(homeSource, '/'); +export const consultingPage = parsePage(consultingSource, '/services/consulting'); +export const coachingPage = parsePage(coachingSource, '/services/coaching'); +export const digitalSolutionsPage = parsePage( + digitalSolutionsSource, + '/services/digital-solutions' +); diff --git a/src/lib/v3/source/home.html b/src/lib/v3/source/home.html new file mode 100644 index 0000000..dce3858 --- /dev/null +++ b/src/lib/v3/source/home.html @@ -0,0 +1,1997 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>Lean 101 — Smarter Improvement Solutions + + + + + + + + + + + + + + +
+ Smarter Improvement Solutions +

+ Continuous improvement,
+ made simple. +

+

+ Practical consulting, coaching and analytics to help businesses eliminate waste, + improve performance, and build a culture that keeps improving — long after we've gone. +

+ + + +
+ Team collaborating on a process improvement workshop +
+
+
+
Efficiency
+
+35%
+
+
+
Cost savings
+
−27%
+
+
+
Lead time
+
−44%
+
+
+
Engagement
+
+41%
+
+
+
+ + +
+
+
+
What we do
+

Three services. One outcome: improvement that lasts.

+

+ Frameworks aren't the goal — results are. Every engagement combines + consulting, capability building, and digital solutions — sized to what your team can sustain. +

+
+ +
+
+
+ Consulting + Consultant mapping out a process on a whiteboard +
+
+

Process Excellence & Transformation

+

+ We diagnose where work is breaking down, design optimised processes, and + run the change with you — not just for you. +

+ + Learn more + + +
+
+ +
+
+ Coaching & Training + Coach working with a small team in a workshop setting +
+
+

Building capability and culture

+

+ Tailored coaching and bespoke training that turns Lean theory into everyday + practice. Your team owns the improvement. +

+ + Learn more + + +
+
+ +
+
+ Digital Solutions + Custom software being developed on a laptop +
+
+

Custom apps, dashboards & automation

+

+ From Power BI dashboards to custom web apps that replace clunky spreadsheets — we build tailored software that streamlines your operations and makes data work for you. +

+ + Learn more + + +
+
+
+
+
+ + +
+
+
+
How it works
+

A clear path from waste to flow.

+

+ Every engagement runs four cycles — adapted to your scale, industry and team capability. The cycles repeat as the business evolves; what stays constant is the partnership underneath. +

+
+ +
+
+
+
Step 01
+

Diagnose

+

+ Process mapping, audits and a maturity assessment. We walk your operation alongside you, talk to the people doing the work, and quantify where the friction sits. +

+
+ By the end: a clear map of where time, quality and cost are leaking — and which fix moves the dial first. +
+
+ + + +
+
Step 02
+

Design & pilot

+

+ We co-design the future state with your team, then run a focused pilot in one area. Real data, real people, real results — before we touch the rest of the business. +

+
+ By the end: a proven future state, validated with hard data on a focused pilot. +
+
+ + + +
+
Step 03
+

Roll out

+

+ Once the pilot proves the gains, we scale the approach across other teams, sites or business units — adapted to each context, with the same disciplined method. +

+
+ By the end: the gains scaled across teams, sites or business units — same method, each context. +
+
+ + + +
+
Step 04
+

Sustain

+

+ Coaching, dashboards and train-the-trainer programs so the improvement becomes how your team operates — without us in the room. +

+
+ By the end: the improvement runs without us — your team owns it, the data proves it. +
+
+
+ + +
+
+ + +
+
+ The Partnership +

The four-step cycle happens inside an ongoing partnership. We stay alongside you across cycles — same team, same methods, learning your business as it evolves. Continuous improvement, made continuous.

+
+
+
+
+
+ + +
+
+
+
+
What good looks like
+

The five things continuous improvement moves.

+

+ Industry benchmarks for organisations that commit to a structured improvement program. Whatever your industry, these are the levers that shift. +

+
+ + Book a call + + +
+ + +
+
+ + +
+
+
+
+
Why Lean 101
+

One framework. Customised to your business.

+

+ Lean 101 is a one-on-one partnership. We bring globally recognised + frameworks — Lean, Six Sigma, continuous improvement best practice — + and adapt them to how your business actually runs. Every company, + every industry is different, so we work alongside you to solve the + problems that matter to you — not deliver a generic playbook. +

+
+ +
    +
  • +
    + +
    +
    +

    Globally recognised frameworks, practically applied

    +

    Lean, Six Sigma, and continuous improvement best practice — adapted to how your business actually runs, not lifted from a generic playbook.

    +
    +
  • +
  • +
    + +
    +
    +

    Customised to each business, not off the shelf

    +

    Every business is different. Focused coaching, a single process redesign, or full transformation — we shape the engagement around your priorities, capacity, and budget.

    +
    +
  • +
  • +
    + +
    +
    +

    We build capability, not dependency

    +

    Our success metric is the day you no longer need us. Train-the-trainer, embedded dashboards, and clear playbooks make the improvement stick.

    +
    +
  • +
  • +
    + +
    +
    +

    Data-led from day one

    +

    Power BI dashboards and automated reporting come standard. You see what's working, what isn't, and what to do next — in real time.

    +
    +
  • +
+
+
+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
1 30-min intro call
+ +
2 1-hour site visit
+ +
3 Tailored partnership
+
+ + +
+
+
+ + + + + + + + diff --git a/src/lib/v3/source/services/coaching.html b/src/lib/v3/source/services/coaching.html new file mode 100644 index 0000000..8661083 --- /dev/null +++ b/src/lib/v3/source/services/coaching.html @@ -0,0 +1,2082 @@ + + + + + +Coaching & Training — Lean 101 + + + + + + + + + + + + + + + +
+
+
+ Coaching & Training +

Building capability and culture.

+

+ Tailored coaching and bespoke training that turns Lean theory into everyday practice. Your team owns the improvement — and keeps building on it long after we've gone. +

+ +
+
+ Coaching conversation between two people in a working environment +
+
+
+ + +
+
+
+ Workshop with team members collaborating around laptops +
+
+
Who it's for
+

If any of these sound familiar, this is for you.

+

+ Most businesses we coach have smart people who want to improve. The problem isn't will — it's method. These are the things leaders most often say when they reach out. +

+
+ +
+
+
+

"Improvement only happens when I'm in the room. The moment I step out, it goes back to how it was."

+
+
+
+ +
+
+ The leader-bottleneck + Capability lives in one person — and only one person. +
+
+
+ +
+
+

"My team has experience. They've been here years. But when something goes wrong they just... work around it. They don't dig."

+
+
+
+ +
+
+ Smart people, no framework + Experience without a method for getting to root cause. +
+
+
+ +
+
+

"We had a consultant come in two years ago. The slides were great. The improvements lasted about a quarter. We're back where we started."

+
+
+
+ +
+
+ The slide-deck graveyard + Past help left process, not capability. The team never owned it. +
+
+
+ +
+
+

"I don't want to keep paying consultants. I want my team to be the consultants — for ourselves, and for whoever joins next."

+
+
+
+ +
+
+ Self-sustaining capability + The goal isn't a method, it's an internal improvement habit. +
+
+
+
+
+
+ + +
+
+
+
The framework
+

Learn it. Adapt it. Teach it.

+

+ Real capability develops in three stages — first you follow the method, then you adapt it to your context, then you teach the next team. We borrow this from the Toyota Way, where it's called Shu-Ha-Ri. +

+
+ +
+
+ +
+
+
01
+
+
+
Stage 1 · Learn the method Shu
+

Follow the form. Build the muscle.

+

The team learns the methods exactly as taught — A3 problem-solving, 5 Whys, value-stream mapping, structured root-cause analysis. The goal at this stage isn't creativity. It's fluency in the form, before improvising.

+
+ What it sounds like: "We're using the method, even when it feels slow. We're getting better at it." +
+
+
+ +
+
+
02
+
+
+
Stage 2 · Adapt to your context Ha
+

Understand why the method works. Then adjust it.

+

The team starts seeing why each step exists — and what to bend, skip, or extend for their own situation. Coaching shifts from instruction to discussion. Methods become tools they apply, not rules they follow.

+
+ What it sounds like: "We're solving real problems with the methods — and adjusting them to fit how we actually work." +
+
+
+ +
+
+
03
+
+
+
Stage 3 · Teach the next team Ri
+

The method becomes how you think. Then you teach it.

+

Your most capable people are now coaches themselves. They run workshops, mentor others, and propagate the method across the business. They create new approaches when the situation calls for it. The improvement engine has gone internal.

+
+ What it sounds like: "We're teaching the next team without us in the room. Improvement is just how we work now." +
+
+
+
+ +
+ Why this approach? Because the goal isn't a slide deck of methods — it's a team that can teach the next team. Most consultancies stop at Stage 1: a binder of tools, no real ownership. We're built around getting you to Stage 3 and stepping back. +
+
+
+ + +
+
+
+
What this looks like in practice
+

Where Coaching & Training delivers real value.

+

+ Two illustrative scenarios showing what kind of work this looks like and what kind of capability typically develops. Real programs are shaped to your team and the problems they're solving. +

+
+ +
+
+
+ Senior leader in a one-on-one coaching context +
+ + + Leadership development + +

Building Lean leaders, not just Lean teams.

+

Senior leaders sponsor improvement but don't lead it. Improvement happens to the team, not through it. We coach 3–6 senior leaders one-on-one, helping them shift from giving answers to asking the right structured questions — and run their own A3 cycles on real business problems.

+
+
+
3–6*
+
Leaders coached to fluency
+
+
+
2–4×*
+
More improvement cycles per quarter
+
+
+
+ +
+
+ Team in a structured discussion around a table +
+ + + Frontline team capability + +

Turning a frontline team into structured problem-solvers.

+

Smart, experienced team but no shared method. Same issues recur. A 12-week program: workshops on A3, 5 Whys, and value-stream mapping; weekly coaching cycles where the team brings real problems and we coach them through structured solving.

+
+
+
8–15*
+
Team members trained to fluency
+
+
+
5–12*
+
Real problems solved during program
+
+
+
+
+ +
+
+ + +
+
+
+
The engagement
+

A focused program. Often, the start of a partnership.

+

+ Most coaching engagements begin as a focused program — a workshop series, a capability build, or a train-the-trainer rollout. The capability work that follows tends to grow into an ongoing partnership over time. +

+
+ + +
+
+ + +
+

+ *About these ranges. These are illustrative scenarios drawn from typical capability-building work — not specific client results. Real programs are shaped to your team's starting capability and the business problems they're solving. +

+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
+
+ + + + + + + + + + diff --git a/src/lib/v3/source/services/consulting.html b/src/lib/v3/source/services/consulting.html new file mode 100644 index 0000000..e4fb667 --- /dev/null +++ b/src/lib/v3/source/services/consulting.html @@ -0,0 +1,1915 @@ + + + + + +Process Excellence & Transformation — Lean 101 + + + + + + + + + + + + + + + +
+
+
+ Consulting +

Process Excellence & Transformation.

+

+ We diagnose where work is breaking down, design optimised processes, and run the change with you — not just for you. Practical Lean and Six Sigma applied to how your business actually runs. +

+ +
+
+ Operations team at work in a real workplace +
+
+
+ + +
+
+
+ Team in a working session, problem-solving on a whiteboard +
+
+
Who it's for
+

Built for businesses where the process used to work — and now doesn't.

+

+ Most of our consulting clients aren't in crisis. They've grown, things have got more complex, and the way work flows has stopped keeping up. If any of these sound familiar, this is for you. +

+
+ +
+
+
+ +
+

Things take longer than they used to

+

Lead times have crept up. Customers wait longer. Your team is busy but throughput hasn't kept pace with demand.

+
+
+
+ +
+

Errors are repeating, not improving

+

The same defects, the same rework, the same complaints — month after month. Fixes are reactive, not structural.

+
+
+
+ +
+

You sense waste, but can't quantify it

+

You know there's slack in the system. Hand-offs, waiting, duplicate work. But you don't have data to prove where, or how much.

+
+
+
+ +
+

You've grown — and the system hasn't kept up

+

What worked at 20 people doesn't work at 80. The same roles do more jobs, the same processes carry more load, and the cracks are starting to show.

+
+
+
+
+ + +
+
+
+
The framework
+

Lean Six Sigma, applied with discipline.

+

+ Process Excellence engagements run on the DMAIC framework — five phases proven across decades of operational improvement work, adapted to your scale and context. +

+
+ +
+
+
D
+
+

Define

+

Scope the problem and the goal. Anchor the engagement to a measurable target before any work begins.

+
+
+
+
M
+
+

Measure

+

Capture the current state with hard data — cycle times, defects, hand-offs, costs. Evidence over opinion.

+
+
+
+
A
+
+

Analyse

+

Find the root causes. Separate symptoms from structural drivers using value-stream mapping and analysis.

+
+
+
+
I
+
+

Improve

+

Design and pilot the fix with your team. Prove it works on a focused area before scaling.

+
+
+
+
C
+
+

Control

+

Lock in the gains. Standard work and visual controls hold the improvement — and feed the next cycle.

+
+
+
+ +
+ Why DMAIC? It's the most widely-adopted process improvement framework in the world — used across manufacturing, healthcare, and service industries for over 30 years. We apply it well, adapted to small-to-mid businesses where most consultancies overcomplicate it. +
+
+
+ + +
+
+
+
What this looks like in practice
+

Where Process Excellence delivers real value.

+

+ Two illustrative scenarios showing what kind of work this looks like and what kind of results typically follow. Real engagements are scoped to your specific operation. +

+
+ +
+
+
+ Manufacturing line at work +
+ + + Manufacturing & operations + +

Reducing lead time on a manual production line.

+

Customers waiting, work piling up, the team busy but not productive. We value-stream map the production flow, identify hand-off bottlenecks, and redesign standard work to eliminate wait states between stations.

+
+
+
−30 to −50%*
+
Lead time reduction
+
+
+
+20 to +40%*
+
Throughput on the same headcount
+
+
+
+ +
+
+ Office team reviewing process workflows +
+ + + Services & admin + +

Cutting cycle time on quote-to-cash.

+

Deals taking weeks to quote, errors slipping through, sales chasing internal teams for status. We map the full quote-to-invoice flow across departments, eliminate duplicate hand-offs, and standardise the request-to-approval flow.

+
+
+
−40 to −70%*
+
Cycle time, quote to invoice
+
+
+
−50 to −80%*
+
Errors and rework
+
+
+
+
+ +
+
+ + +
+
+
+
The engagement
+

A focused project. Often, the start of a partnership.

+

+ Most engagements begin as a focused project — small, medium, or large depending on scope. The work that delivers tends to grow into an ongoing partnership over time. +

+
+ + +
+
+ + +
+

+ *About these ranges. These are illustrative scenarios drawn from typical Lean improvement work — not specific client results. Real engagements are tailored to your business; scope, sector, and starting capability all shape what's achievable. +

+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
+
+ + + + + + + + + + diff --git a/src/lib/v3/source/services/digital-solutions.html b/src/lib/v3/source/services/digital-solutions.html new file mode 100644 index 0000000..e1884f9 --- /dev/null +++ b/src/lib/v3/source/services/digital-solutions.html @@ -0,0 +1,2183 @@ + + + + + +Digital Solutions — Lean 101 + + + + + + + + + + + + + + + +
+
+
+ Digital Solutions +

Custom apps, dashboards & automation.

+

+ From Power BI dashboards to custom web apps that replace clunky spreadsheets — we build tailored software that streamlines your operations and makes data work for you. Built by people who understand the operations they're improving. +

+ +
+
+ Operational dashboard with charts and live data +
+
+
+ + +
+
+
+ Person reviewing data and analytics on a screen +
+
+
Who it's for
+

Built for businesses where the spreadsheets have outgrown themselves.

+

+ Most of our Digital Solutions work starts as: "we're running this on Excel and it's gotten out of hand." Custom software built for your operations replaces the duct tape — without ripping up what already works. +

+
+ +
+
+
+ +
+

Critical processes run on a single Excel file

+

One person knows the formulas. The file emails around as v17, v18, v18-final, v18-final-FINAL. When that person is on leave, work stops.

+
+
+
+ +
+

Reporting takes a day, not a click

+

End-of-month, someone manually pulls data from three systems, copies it into a deck, formats charts, sends it round. By the time it lands, it's already old.

+
+
+
+ +
+

You're paying for SaaS that doesn't fit

+

You bought an off-the-shelf tool that does 60% of what you need, and your team works around the missing 40% in spreadsheets. The cost compounds — money out the door, time wasted on workarounds.

+
+
+
+ +
+

The data exists — but no one can see it

+

Your CRM, accounting system, and project tools all hold useful data. But there's no single view that answers the simple operational questions you ask every week.

+
+
+
+
+ + + +
+
+
+
What we build
+

Three kinds of builds, one disciplined method.

+

+ Most Digital Solutions engagements land in one of three categories. Each is built the same way — understand the work, ship a focused MVP, iterate with users, hand it over fully owned. +

+
+ +
+
+ +
+ Dashboards +

Operational visibility, refreshed automatically.

+

Power BI dashboards that pull from your real systems and answer the questions you ask each week — production output, lead times, defect rates, capacity, financial performance. End-of-month reporting goes from a day to a click.

+
    +
  • Real-time data from your existing systems
  • +
  • Tailored views per role (operators, supervisors, leadership)
  • +
  • Automated email digests and alerts
  • +
+
+
+ +
+ +
+ Web apps +

Custom apps that replace clunky spreadsheets.

+

Tailored web applications for the workflows that have outgrown Excel — order tracking, scheduling, inventory, quality logs, audit checklists. Multi-user, role-aware, accessible from anywhere, no more "v18-final-FINAL.xlsx."

+
    +
  • One source of truth, no more conflicting copies
  • +
  • Mobile-friendly so frontline teams can use it on the floor
  • +
  • Full audit trail of who changed what and when
  • +
+
+
+ +
+ +
+ Automation +

Repetitive admin work, automated away.

+

The copy-paste between systems, the manual report generation, the daily reconciliation — all the work nobody wants to do but somebody has to. Automated using Power Automate, Python scripts, or whatever tool fits the job.

+
    +
  • Hours of admin work removed from your team's week
  • +
  • Fewer errors from manual data entry
  • +
  • Triggered notifications when things need human attention
  • +
+
+
+
+ + +
+
+ How we build +

Same four phases, every build.

+
+
+
+
+
+ + + + +
+
01
+
+
Understand the work
+

We watch the process before we touch code. Talk to the people who'll use it. Document what's actually happening today, not what the org chart says.

+
+
+
+
+ + + + + + +
+
02
+
+
Ship the MVP
+

A focused first version, working on real data within weeks. Not pretty yet, but real — running in your environment, doing one job well.

+
+
+
+
+ + + + +
+
03
+
+
Iterate with users
+

Your team uses it. We watch how. Friction surfaces fast. We fix what matters, drop what doesn't, add the features that earn their place.

+
+
+
+
+ + + + +
+
04
+
+
Hand it over
+

Documentation, admin training, source code, deployment guide. Your team can run, extend, and modify the tool — we're a call away, not a dependency.

+
+
+
+
+
+ + +
+
+
+
What this looks like in practice
+

Where Digital Solutions delivers real value.

+

+ Two illustrative scenarios showing what kind of build this looks like and what kind of impact typically follows. Real projects are scoped to your operations, systems, and team. +

+
+ +
+
+
+ Live operational dashboard with charts +
+ + + Operational dashboard + +

Replacing manual reporting with live Power BI dashboards.

+

End-of-month reporting takes a day, the data's old by the time it's seen, decisions are made on gut. We connect your existing systems to a Power BI workspace, design role-aware views for operators, supervisors, and leadership, and set up automated daily refresh.

+
+
+
−90 to −95%*
+
Time spent compiling reports
+
+
+
3–6 weeks*
+
Typical build time
+
+
+
+ +
+
+ Custom application interface on a clean screen +
+ + + Custom web application + +

Replacing a critical spreadsheet with a custom web app.

+

A single Excel file runs a critical process, one person knows the formulas, the file emails around as v17, v18, v18-final. We map the actual workflow, build a tailored multi-user web app with role-based access, and migrate live data — running the new tool alongside the spreadsheet during cutover.

+
+
+
−60 to −80%*
+
Errors from manual data entry
+
+
+
8–16 weeks*
+
Typical build time
+
+
+
+
+ +
+
+ + +
+
+
+
The engagement
+

A focused build. Often, the start of a partnership.

+

+ Most builds begin as a focused project — a dashboard, a custom app, or a multi-system integration. The work that delivers tends to grow into an ongoing technology partnership as your operations evolve and tooling needs grow. +

+
+ + +
+
+ + +
+

+ *About these ranges. These are illustrative scenarios drawn from typical custom-software builds for SMBs — not specific client results. Real projects are scoped to your existing systems, integrations needed, and the workflow the tool needs to support. +

+
+ + +
+
+
+

Let's find your first improvement.

+

Start with a free 30-minute intro call. If it's a fit, we'll schedule a 1-hour site visit to walk your operation together — and from there we'll scope a tailored partnership.

+ +
+
+
+ + + + + + + + + + diff --git a/src/routes/+layout.js b/src/routes/+layout.js new file mode 100644 index 0000000..d43d0cd --- /dev/null +++ b/src/routes/+layout.js @@ -0,0 +1 @@ +export const prerender = false; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..4039a26 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/+page.server.js b/src/routes/+page.server.js new file mode 100644 index 0000000..ead014d --- /dev/null +++ b/src/routes/+page.server.js @@ -0,0 +1,12 @@ +import { error } from '@sveltejs/kit'; +import { getPageBySlug } from '$lib/server/content'; + +export async function load() { + const page = await getPageBySlug('home'); + + if (!page) { + throw error(404, 'Homepage not found.'); + } + + return { page }; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..d2b22f9 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,6 @@ + + + diff --git a/src/routes/[slug]/+page.server.js b/src/routes/[slug]/+page.server.js new file mode 100644 index 0000000..0a1e290 --- /dev/null +++ b/src/routes/[slug]/+page.server.js @@ -0,0 +1,12 @@ +import { error } from '@sveltejs/kit'; +import { getPageBySlug } from '$lib/server/content'; + +export async function load({ params }) { + const page = await getPageBySlug(params.slug); + + if (!page || page.slug === 'home') { + throw error(404, 'Page not found.'); + } + + return { page }; +} diff --git a/src/routes/[slug]/+page.svelte b/src/routes/[slug]/+page.svelte new file mode 100644 index 0000000..bbf02ea --- /dev/null +++ b/src/routes/[slug]/+page.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/routes/admin/+page.server.js b/src/routes/admin/+page.server.js new file mode 100644 index 0000000..4035293 --- /dev/null +++ b/src/routes/admin/+page.server.js @@ -0,0 +1,198 @@ +import { fail, redirect } from '@sveltejs/kit'; +import { + ADMIN_COOKIE_NAME, + createAdminSessionToken, + getAdminConfigIssues, + getAdminCookieOptions, + isAdminConfigured, + isValidAdminPassword, + verifyAdminSessionToken +} from '$lib/server/auth'; +import { + createBlankBlogPost, + createBlankPage, + deleteBlogPost, + deletePage, + hasDatabaseConfig, + listBlogPosts, + listPages, + saveBlogPost, + savePage +} from '$lib/server/content'; + +/** @param {import('@sveltejs/kit').Cookies} cookies */ +function isAuthenticated(cookies) { + return verifyAdminSessionToken(cookies.get(ADMIN_COOKIE_NAME)); +} + +/** @param {FormData} formData @param {string} key */ +function getString(formData, key) { + return String(formData.get(key) || ''); +} + +/** @param {string} raw */ +function parseJson(raw) { + return JSON.parse(raw); +} + +export async function load({ cookies }) { + const authenticated = isAuthenticated(cookies); + const databaseConfigured = hasDatabaseConfig(); + const adminConfigIssues = getAdminConfigIssues(); + + if (!authenticated) { + return { + authenticated, + databaseConfigured, + adminConfigured: isAdminConfigured(), + adminConfigIssues, + pages: [], + posts: [], + newPageTemplate: createBlankPage(), + newPostTemplate: createBlankBlogPost() + }; + } + + const [pages, posts] = await Promise.all([listPages(), listBlogPosts(true)]); + + return { + authenticated, + databaseConfigured, + adminConfigured: isAdminConfigured(), + adminConfigIssues, + pages, + posts, + newPageTemplate: createBlankPage(), + newPostTemplate: createBlankBlogPost() + }; +} + +export const actions = { + login: async ({ request, cookies, url }) => { + if (!isAdminConfigured()) { + return fail(500, { + loginError: + 'Admin login is not configured. Set ADMIN_PASSWORD and ADMIN_SESSION_SECRET first.' + }); + } + + const formData = await request.formData(); + const password = getString(formData, 'password'); + + if (!isValidAdminPassword(password)) { + return fail(400, { + loginError: 'Incorrect password.' + }); + } + + cookies.set(ADMIN_COOKIE_NAME, createAdminSessionToken(), getAdminCookieOptions(url)); + throw redirect(303, '/admin'); + }, + + savePage: async ({ request, cookies }) => { + if (!isAuthenticated(cookies)) { + throw redirect(303, '/admin'); + } + + const formData = await request.formData(); + const payload = getString(formData, 'payload'); + const selectedPageSlug = getString(formData, 'selectedPageSlug'); + const selectedTab = getString(formData, 'selectedTab') || 'pages'; + + try { + const { page, updatedAt } = await savePage(parseJson(payload)); + return { + selectedTab, + selectedPageSlug: page.slug, + saveSuccess: `Saved page "${page.title}" at ${updatedAt.toISOString()}.` + }; + } catch (error) { + return fail(400, { + selectedTab, + selectedPageSlug, + saveError: error instanceof Error ? error.message : 'Failed to save page.' + }); + } + }, + + deletePage: async ({ request, cookies }) => { + if (!isAuthenticated(cookies)) { + throw redirect(303, '/admin'); + } + + const formData = await request.formData(); + const slug = getString(formData, 'slug'); + const selectedTab = getString(formData, 'selectedTab') || 'pages'; + + try { + await deletePage(slug); + return { + selectedTab, + selectedPageSlug: 'home', + saveSuccess: `Deleted page "${slug}".` + }; + } catch (error) { + return fail(400, { + selectedTab, + selectedPageSlug: slug, + saveError: error instanceof Error ? error.message : 'Failed to delete page.' + }); + } + }, + + savePost: async ({ request, cookies }) => { + if (!isAuthenticated(cookies)) { + throw redirect(303, '/admin'); + } + + const formData = await request.formData(); + const payload = getString(formData, 'payload'); + const selectedPostSlug = getString(formData, 'selectedPostSlug'); + const selectedTab = getString(formData, 'selectedTab') || 'posts'; + + try { + const { post, updatedAt } = await saveBlogPost(parseJson(payload)); + return { + selectedTab, + selectedPostSlug: post.slug, + saveSuccess: `Saved blog post "${post.title}" at ${updatedAt.toISOString()}.` + }; + } catch (error) { + return fail(400, { + selectedTab, + selectedPostSlug, + saveError: error instanceof Error ? error.message : 'Failed to save blog post.' + }); + } + }, + + deletePost: async ({ request, cookies }) => { + if (!isAuthenticated(cookies)) { + throw redirect(303, '/admin'); + } + + const formData = await request.formData(); + const slug = getString(formData, 'slug'); + const selectedTab = getString(formData, 'selectedTab') || 'posts'; + + try { + await deleteBlogPost(slug); + return { + selectedTab, + selectedPostSlug: '', + saveSuccess: `Deleted blog post "${slug}".` + }; + } catch (error) { + return fail(400, { + selectedTab, + selectedPostSlug: slug, + saveError: error instanceof Error ? error.message : 'Failed to delete blog post.' + }); + } + }, + + logout: async ({ cookies, url }) => { + cookies.delete(ADMIN_COOKIE_NAME, getAdminCookieOptions(url)); + throw redirect(303, '/admin'); + } +}; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..ff22e10 --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,646 @@ + + + + + Lean 101 CMS + + +{#if !data.authenticated} +
+ +
+{:else} +
+
+
+
+ +
Lean 101 CMS
+

Pages and publishing

+

+ Manage page-level content, move sections within each page, and write blog posts from a + single admin area. +

+
+ + +
+ +
+
+ Database + {data.databaseConfigured ? 'Configured' : 'Missing env vars'} +
+
+ Pages + {pages.length} +
+
+ Blog Posts + {posts.length} +
+
+ + {#if statusMessage} +
{statusMessage}
+ {/if} + + {#if errorMessage} +
{errorMessage}
+ {/if} + + {#if !data.databaseConfigured} +
+ The editor UI still works, but saving requires `MONGODB_URI` and `MONGODB_DB`. +
+ {/if} + +
+ + +
+ {#if activeTab === 'pages'} +
+
+
Page editor
+

{selectedPage.title}

+

+ Edit core page metadata and move sections using the controls on each card. +

+
+ +
+ + View page + + {#if selectedPage.slug !== 'home'} + + {/if} +
+
+ +
+
+

Page settings

+ +
+ + + + +
+ + + + + +
+ +
+
+

Sections

+
+ + +
+
+ +
+ {#each selectedPage.sections as section, index} +
+
+
+ {section.type} + +
+
+ + + +
+
+ + + +
+ {/each} +
+
+
+ + {#if currentPageSerialization.error} +
+ This page has invalid JSON somewhere: {currentPageSerialization.error} +
+ {/if} + +
+
+ + + + +
+ + {#if selectedPage.slug !== 'home'} +
+ + + +
+ {/if} +
+ {:else} +
+
+
Blog editor
+

{selectedPost.title}

+

+ Draft posts here, then mark them published to make them appear on `/blog`. +

+
+ +
+ {#if selectedPost.slug} + + View post + + {/if} + +
+
+ +
+
+ + + + + + +
+ + + +
+ + +
+ + +
+ +
+
+ + + + +
+ + {#if selectedPost.slug} +
+ + + +
+ {/if} +
+ {/if} +
+
+
+
+{/if} diff --git a/src/routes/blog/+page.server.js b/src/routes/blog/+page.server.js new file mode 100644 index 0000000..75c954f --- /dev/null +++ b/src/routes/blog/+page.server.js @@ -0,0 +1,6 @@ +import { listBlogPosts } from '$lib/server/content'; + +export async function load() { + const posts = await listBlogPosts(false); + return { posts }; +} diff --git a/src/routes/blog/+page.svelte b/src/routes/blog/+page.svelte new file mode 100644 index 0000000..995aefb --- /dev/null +++ b/src/routes/blog/+page.svelte @@ -0,0 +1,67 @@ + + + + Lean 101 Blog + + + + + + +
+
+
+
Lean 101 Journal
+

Improvement ideas, written plainly.

+

+ Articles, field notes, and practical guidance from Lean 101. Built for leaders and teams + who want useful thinking, not filler. +

+
+ + {#if data.posts.length} +
+ {#each data.posts as post} + + {/each} +
+ {:else} +
+

No published posts yet.

+

Create one in the admin area and publish it when it’s ready.

+
+ {/if} +
+
diff --git a/src/routes/blog/[slug]/+page.server.js b/src/routes/blog/[slug]/+page.server.js new file mode 100644 index 0000000..71e49e8 --- /dev/null +++ b/src/routes/blog/[slug]/+page.server.js @@ -0,0 +1,12 @@ +import { error } from '@sveltejs/kit'; +import { getBlogPostBySlug } from '$lib/server/content'; + +export async function load({ params }) { + const post = await getBlogPostBySlug(params.slug, false); + + if (!post) { + throw error(404, 'Blog post not found.'); + } + + return { post }; +} diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte new file mode 100644 index 0000000..2521f6c --- /dev/null +++ b/src/routes/blog/[slug]/+page.svelte @@ -0,0 +1,55 @@ + + + + {data.post.title} | Lean 101 Blog + + + + + + + + + +
+
+ ← Back to blog +
+
+ {new Date(data.post.publishedAt).toLocaleDateString()} + {data.post.author} +
+

{data.post.title}

+

{data.post.excerpt}

+
+ + {data.post.coverAlt} + +
+ {#each paragraphs as paragraph} +

{paragraph}

+ {/each} +
+
+
diff --git a/src/routes/robots.txt/+server.js b/src/routes/robots.txt/+server.js new file mode 100644 index 0000000..0d9a31a --- /dev/null +++ b/src/routes/robots.txt/+server.js @@ -0,0 +1,17 @@ +import { absoluteUrl } from '$lib/site'; + +export const prerender = false; + +export function GET() { + const body = `User-agent: * +Allow: / + +Sitemap: ${absoluteUrl('/sitemap.xml')} +`; + + return new Response(body, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8' + } + }); +} diff --git a/src/routes/services/coaching/+page.svelte b/src/routes/services/coaching/+page.svelte new file mode 100644 index 0000000..3c2dfbf --- /dev/null +++ b/src/routes/services/coaching/+page.svelte @@ -0,0 +1,6 @@ + + + diff --git a/src/routes/services/consulting/+page.svelte b/src/routes/services/consulting/+page.svelte new file mode 100644 index 0000000..219e482 --- /dev/null +++ b/src/routes/services/consulting/+page.svelte @@ -0,0 +1,6 @@ + + + diff --git a/src/routes/services/digital-solutions/+page.svelte b/src/routes/services/digital-solutions/+page.svelte new file mode 100644 index 0000000..de1755e --- /dev/null +++ b/src/routes/services/digital-solutions/+page.svelte @@ -0,0 +1,6 @@ + + + diff --git a/src/routes/sitemap.xml/+server.js b/src/routes/sitemap.xml/+server.js new file mode 100644 index 0000000..ecf8837 --- /dev/null +++ b/src/routes/sitemap.xml/+server.js @@ -0,0 +1,31 @@ +import { absoluteUrl } from '$lib/site'; +import { listSitemapEntries } from '$lib/server/content'; + +export const prerender = false; + +export async function GET() { + const now = new Date().toISOString(); + const entries = await listSitemapEntries(); + + const urls = entries + .map( + (entry) => ` + ${absoluteUrl(entry.path)} + ${entry.updatedAt || now} + monthly + ${entry.path === '/' ? '1.0' : '0.7'} + ` + ) + .join('\n'); + + const body = ` + +${urls} +`; + + return new Response(body, { + headers: { + 'Content-Type': 'application/xml; charset=utf-8' + } + }); +} diff --git a/static/assets/lean101-isotipo.png b/static/assets/lean101-isotipo.png new file mode 100644 index 0000000..f372b59 Binary files /dev/null and b/static/assets/lean101-isotipo.png differ diff --git a/static/assets/lean101-logotipo.png b/static/assets/lean101-logotipo.png new file mode 100644 index 0000000..fba82e9 Binary files /dev/null and b/static/assets/lean101-logotipo.png differ diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..79a7922 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,9 @@ +import adapter from '@sveltejs/adapter-node'; + +const config = { + kit: { + adapter: adapter() + } +}; + +export default config; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..3406f32 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +});