diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1d473b7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +buy_me_a_coffee: + displayName: "Buy Me a Coffee" + account: dockhand \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f32e31a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1e2e0c7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,183 @@ +# syntax=docker/dockerfile:1.4 +# ============================================================================= +# Dockhand Docker Image - Security-Hardened Build +# ============================================================================= +# This Dockerfile builds a custom Wolfi OS from scratch using apko, ensuring: +# - Full transparency (no dependency on pre-built Chainguard images) +# - Reproducible builds from open-source Wolfi packages +# - Minimal attack surface with only required packages +# +# Bun is copied from the official oven/bun image (app-builder stage). +# For CPUs without AVX support (Celeron, Atom, pre-Haswell), build with: +# docker build --build-arg BUN_VARIANT=baseline -t dockhand:baseline . +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 1: OS Generator (Alpine + apko tool) +# ----------------------------------------------------------------------------- +# We use Alpine because it has a shell. This lets us download and run apko +# to build our custom Wolfi OS from scratch using open-source packages. +FROM alpine:3.21 AS os-builder + +ARG TARGETARCH + +WORKDIR /work + +# Install apko tool (latest stable release) +# apko is the tool Chainguard uses to build their images - we use it directly +ARG APKO_VERSION=0.30.34 +RUN apk add --no-cache curl unzip \ + && ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "arm64" || echo "amd64") \ + && curl -sL "https://github.com/chainguard-dev/apko/releases/download/v${APKO_VERSION}/apko_${APKO_VERSION}_linux_${ARCH}.tar.gz" \ + | tar -xz --strip-components=1 -C /usr/local/bin \ + && chmod +x /usr/local/bin/apko + +# Generate apko.yaml for current target architecture only +# We build single-arch to avoid multi-arch layer confusion in extraction +# Note: Bun is NOT included here - it's copied from app-builder stage for CPU compatibility +RUN APKO_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") \ + && printf '%s\n' \ + "contents:" \ + " repositories:" \ + " - https://packages.wolfi.dev/os" \ + " keyring:" \ + " - https://packages.wolfi.dev/os/wolfi-signing.rsa.pub" \ + " packages:" \ + " - wolfi-base" \ + " - ca-certificates" \ + " - busybox" \ + " - tzdata" \ + " - docker-cli" \ + " - docker-compose" \ + " - sqlite" \ + " - git" \ + " - openssh-client" \ + " - curl" \ + " - tini" \ + " - su-exec" \ + "entrypoint:" \ + " command: /bin/sh -l" \ + "archs:" \ + " - ${APKO_ARCH}" \ + > apko.yaml + +# Build the OS tarball and extract rootfs +# apko creates an OCI tarball - we need to extract the actual filesystem layer +RUN apko build apko.yaml dockhand-base:latest output.tar \ + && mkdir -p rootfs \ + && tar -xf output.tar \ + && LAYER=$(tar -tf output.tar | grep '.tar.gz$' | head -1) \ + && tar -xzf "$LAYER" -C rootfs + +# ----------------------------------------------------------------------------- +# Stage 2: Application Builder +# ----------------------------------------------------------------------------- +# Using Debian to avoid Alpine musl thread creation issues +# Alpine's musl libc causes rayon/tokio thread pool panics during svelte-adapter-bun build +FROM oven/bun:1.3.5-debian AS app-builder + +# Build argument for Bun variant (regular or baseline) +# baseline is for CPUs without AVX support (Celeron, Atom, pre-Haswell) +ARG BUN_VARIANT=regular +ARG TARGETARCH + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends jq git curl unzip ca-certificates && rm -rf /var/lib/apt/lists/* + +# Copy package files and install ALL dependencies (needed for build) +COPY package.json bun.lock* bunfig.toml ./ +RUN bun install --frozen-lockfile + +# Copy source code and build +COPY . . + +# Build with parallelism - dedicated build VM has 16 CPUs and 32GB RAM +RUN NODE_OPTIONS="--max-old-space-size=8192 --max-semi-space-size=128" bun run build + +# Prepare production node_modules (do this in builder where we have compilers) +# This ensures native addons compile correctly before copying to hardened runtime +RUN rm -rf node_modules && bun install --production --frozen-lockfile \ + && rm -rf node_modules/@types node_modules/bun-types + +# Download baseline Bun binary if BUN_VARIANT=baseline (for CPUs without AVX) +# Only applies to amd64 - ARM64 doesn't have AVX concept +ARG BUN_VERSION=1.3.5 +RUN if [ "$BUN_VARIANT" = "baseline" ] && [ "$TARGETARCH" = "amd64" ]; then \ + echo "Downloading Bun baseline binary for CPUs without AVX support..." && \ + curl -fsSL "https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-linux-x64-baseline.zip" -o /tmp/bun.zip && \ + unzip -o /tmp/bun.zip -d /tmp && \ + cp /tmp/bun-linux-x64-baseline/bun /usr/local/bin/bun && \ + chmod +x /usr/local/bin/bun && \ + rm -rf /tmp/bun.zip /tmp/bun-linux-x64-baseline && \ + echo "Bun baseline binary installed successfully"; \ + fi + +# ----------------------------------------------------------------------------- +# Stage 3: Final Image (Scratch + Custom Wolfi OS) +# ----------------------------------------------------------------------------- +FROM scratch + +# Install our custom-built Wolfi OS (now we have /bin/sh!) +COPY --from=os-builder /work/rootfs/ / + +# Copy Bun from official image - ensures compatibility with all x86_64 CPUs (no AVX2 requirement) +# Wolfi's bun package requires AVX2 which breaks on Celeron/Atom CPUs +# For baseline builds (BUN_VARIANT=baseline), this contains the baseline binary (no AVX requirement) +# For regular builds, this contains the standard oven/bun binary +COPY --from=app-builder /usr/local/bin/bun /usr/bin/bun + +WORKDIR /app + +# Set up environment variables +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + NODE_ENV=production \ + PORT=3000 \ + HOST=0.0.0.0 \ + DATA_DIR=/app/data \ + HOME=/home/dockhand \ + PUID=1001 \ + PGID=1001 + +# Create docker compose plugin symlink (we use `docker compose` syntax, Wolfi has standalone binary) +RUN mkdir -p /usr/libexec/docker/cli-plugins \ + && ln -s /usr/bin/docker-compose /usr/libexec/docker/cli-plugins/docker-compose + +# Create dockhand user and group (using busybox commands) +RUN addgroup -g 1001 dockhand \ + && adduser -u 1001 -G dockhand -h /home/dockhand -D dockhand + +# Copy application files with correct ownership (avoids layer duplication from chown -R) +COPY --from=app-builder --chown=dockhand:dockhand /app/node_modules ./node_modules +COPY --from=app-builder --chown=dockhand:dockhand /app/package.json ./ +COPY --from=app-builder --chown=dockhand:dockhand /app/build ./build +COPY --from=app-builder --chown=dockhand:dockhand /app/build/subprocesses/ ./subprocesses/ + +# Copy database migrations +COPY --chown=dockhand:dockhand drizzle/ ./drizzle/ +COPY --chown=dockhand:dockhand drizzle-pg/ ./drizzle-pg/ + +# Copy legal documents +COPY --chown=dockhand:dockhand LICENSE.txt PRIVACY.txt ./ + +# Copy entrypoint script (root-owned, executable) +COPY docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Copy emergency scripts +COPY --chown=dockhand:dockhand scripts/emergency/ ./scripts/ +RUN chmod +x ./scripts/*.sh ./scripts/**/*.sh 2>/dev/null || true + +# Create data directories with correct ownership +RUN mkdir -p /home/dockhand/.dockhand/stacks /app/data \ + && chown dockhand:dockhand /app/data /home/dockhand /home/dockhand/.dockhand /home/dockhand/.dockhand/stacks + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/ || exit 1 + +ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"] +CMD ["bun", "run", "./build/index.js"] diff --git a/LICENSE.txt b/LICENSE.txt index 86472ee..148c985 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -123,6 +123,6 @@ under an Open Source License, as stated in this License. For licensing inquiries, commercial licensing, or enterprise features: - Website: https://dockhand.io + Website: https://dockhand.pro ----------------------------------------------------------------------------- diff --git a/PRIVACY.txt b/PRIVACY.txt new file mode 100644 index 0000000..fb5bc78 --- /dev/null +++ b/PRIVACY.txt @@ -0,0 +1,425 @@ +DOCKHAND PRIVACY POLICY + +Last Updated: December 14, 2025 +Effective Date: December 14, 2025 + +================================================================================ + +1. INTRODUCTION + +This Privacy Policy describes how Finsys Jaroslaw Krochmalski ("Finsys," "we," +"us," or "our") handles data in connection with the Dockhand software +application ("Software"). This Policy applies to all users of the Software. + +Finsys is committed to protecting your privacy and ensuring transparency +about our data practices. This Policy explains that the Software operates +entirely locally on your infrastructure with no data transmitted to Finsys. + + +2. DATA CONTROLLER INFORMATION + +Finsys Jaroslaw Krochmalski +ul. Borki 6 +05-119 Jozefow +Poland + +VAT ID: PL7121835977 +REGON: 061576391 + +Email: enterprise@dockhand.pro +Website: https://dockhand.pro + +For the purpose of the General Data Protection Regulation (GDPR) and other +applicable data protection laws, Finsys is NOT the data controller for any +personal data processed through your installation of the Software. You (the +user or your organization) are the data controller for all data stored in +your Software installation. + + +3. OUR FUNDAMENTAL PRINCIPLE: LOCAL-ONLY DATA + +The Software is designed with privacy as a core principle: + +- ALL DATA STAYS LOCAL: The Software stores all data exclusively on your + infrastructure (your servers, your databases, your storage). + +- NO DATA TRANSMISSION: The Software does not transmit any data to Finsys + servers, third-party servers, or any external services. + +- NO TELEMETRY: The Software contains no telemetry, analytics, usage + tracking, crash reporting, or any other data collection mechanisms. + +- FULLY SELF-CONTAINED: The Software operates entirely within your + infrastructure without requiring any connection to Finsys systems. + +- FINSYS HAS NO ACCESS: Finsys cannot access, view, retrieve, or process + any data stored in your Software installation. + + +4. DATA PROCESSED BY THE SOFTWARE + +When you use the Software, the following types of data may be stored +LOCALLY on your infrastructure: + +4.1 User Account Data +- Usernames and email addresses +- Password hashes (never stored in plain text) +- Multi-factor authentication (MFA) secrets (Enterprise Edition) +- User profile information and avatars +- Role assignments and permissions (Enterprise Edition) + +4.2 Authentication Data +- Session tokens and cookies +- OIDC/SSO tokens and provider configurations +- LDAP/Active Directory connection settings (Enterprise Edition) +- API tokens for remote access + +4.3 Docker Environment Data +- Docker host connection details (URLs, ports, socket paths) +- Docker container information (names, IDs, configurations) +- Container logs and metrics +- Image and volume data +- Network configurations +- Compose stack definitions + +4.4 Git Integration Data +- Git repository URLs and credentials +- SSH keys and access tokens +- Deployment webhooks + +4.5 Registry Data +- Docker registry URLs and credentials +- Image pull/push history + +4.6 Activity and Audit Data +- User activity logs +- Container events and operations +- Audit trails (Enterprise Edition) + +4.7 Application Settings +- General configuration preferences +- Notification channel settings (SMTP, webhooks) +- Scheduled task configurations + +All of the above data is stored exclusively in your local database +(SQLite or PostgreSQL) and on your local filesystem. None of this data +is transmitted to or accessible by Finsys. + + +5. HOW DATA IS STORED + +5.1 Database Storage + +The Software uses either SQLite or PostgreSQL as configured by you: +- SQLite: Data stored in a local file on your server +- PostgreSQL: Data stored in your PostgreSQL database instance + +5.2 File Storage + +Certain data is stored in the local filesystem: +- Compose stack files +- Uploaded files (e.g., user avatars) +- Temporary files during operations + +5.3 Encryption + +- Passwords are hashed using secure algorithms (Argon2id) +- Sensitive credentials may be encrypted at rest depending on your + database configuration +- You are responsible for implementing disk encryption, database + encryption, and network security for your infrastructure + + +6. YOUR RESPONSIBILITIES AS DATA CONTROLLER + +Since all data is stored locally on your infrastructure, YOU are the +data controller for purposes of GDPR and other data protection laws. +As data controller, you are responsible for: + +6.1 Legal Basis for Processing +Ensuring you have a valid legal basis for processing personal data of +your users (e.g., consent, legitimate interest, contractual necessity). + +6.2 Data Subject Rights +Responding to data subject requests including: +- Right of access (Article 15 GDPR) +- Right to rectification (Article 16 GDPR) +- Right to erasure (Article 17 GDPR) +- Right to restriction of processing (Article 18 GDPR) +- Right to data portability (Article 20 GDPR) +- Right to object (Article 21 GDPR) + +6.3 Security Measures +Implementing appropriate technical and organizational measures to +protect personal data, including: +- Access controls and authentication +- Encryption of data at rest and in transit +- Regular security updates and patches +- Backup and disaster recovery procedures +- Network security (firewalls, VPNs, etc.) + +6.4 Data Retention +Establishing and implementing appropriate data retention policies. + +6.5 Breach Notification +Notifying supervisory authorities and affected individuals in case +of a personal data breach, as required by applicable law. + +6.6 Privacy Notices +Providing appropriate privacy notices to your users regarding how +their data is processed within the Software. + + +7. DATA WE DO NOT COLLECT + +To be absolutely clear, Finsys does NOT collect, receive, access, or +process ANY of the following: + +- Your identity or contact information (unless you contact us directly) +- Your Docker infrastructure information +- Your container configurations or data +- Your user accounts or credentials +- Your activity logs or audit trails +- Your git repositories or deployment data +- Usage statistics or analytics +- Error reports or crash data +- Any telemetry or diagnostic data +- Any data whatsoever from your Software installation + + +8. WHEN FINSYS MAY RECEIVE DATA + +The only circumstances in which Finsys may receive data from you are: + +8.1 Direct Communication +When you voluntarily contact us via email (enterprise@dockhand.pro), +we receive and process the information you provide (name, email address, +message content). This data is processed for the purpose of responding +to your inquiry based on our legitimate interest in providing customer +support. + +8.2 License Purchase + +When you purchase an Enterprise Edition license, we collect and process: + +Data Collected: +- Name and/or company name +- Email address +- Billing address +- Payment information (processed by payment provider) +- Licensed hostname/identifier + +Legal Basis (GDPR Article 6): +- Contract performance (Art. 6(1)(b)) - to fulfill the license agreement +- Legal obligation (Art. 6(1)(c)) - for invoicing and tax records + +How We Use This Data: +- To issue and deliver your License Key +- To send license renewal reminders +- To provide support related to your license +- To comply with tax and accounting obligations + +Data Retention: +- License and invoice records: 7 years (Polish tax law requirement) +- Email correspondence: 3 years after last contact + +Data Sharing: +- Payment processor (for payment transactions only) +- No other third parties +- No marketing or advertising use + +8.3 Website Visits +If you visit our website (https://dockhand.pro), standard web server +logs may be collected. See our website privacy policy for details. + + +9. LICENSE KEY DATA + +Enterprise Edition License Keys contain: +- Customer name (as registered) +- Licensed hostname or identifier +- Expiration date +- Cryptographic signature + +This information is embedded in the License Key itself and stored +locally in your Software installation. Finsys retains a record of +issued licenses for license management purposes. + + +10. INTERNATIONAL DATA TRANSFERS + +Since all Software data is stored locally on your infrastructure, no +international data transfers occur through the Software itself. + +If your infrastructure is located outside the European Economic Area +(EEA), you are responsible for ensuring appropriate safeguards for +any personal data stored therein. + + +11. DATA RETENTION + +11.1 Software Data +You control the retention of all data in your Software installation. +The Software does not automatically delete data unless you configure +retention policies or manually delete data. + +11.2 Communication Data +If you contact us directly, we retain correspondence for as long as +necessary to respond to your inquiry and for our records, typically +not exceeding 3 years unless required for legal purposes. + +11.3 License Records +We retain license purchase and activation records for the duration +required by tax and accounting regulations (typically 5-7 years). + + +12. CHILDREN'S PRIVACY + +The Software is not intended for use by children under 16 years of age. +We do not knowingly collect personal data from children. If you are a +parent or guardian and believe your child has provided personal data +to us through direct communication, please contact us. + + +13. THIRD-PARTY SERVICES + +13.1 Software Integrations + +The Software may connect to third-party services as configured by you: +- Docker registries +- Git repositories (GitHub, GitLab, etc.) +- OIDC/SSO providers +- LDAP/Active Directory servers +- Notification services (SMTP, Discord, Slack, etc.) + +These connections are initiated by you, configured by you, and occur +between your infrastructure and these third-party services. Finsys is +not involved in these connections and has no access to the data +exchanged. The privacy policies of these third-party services apply +to your use of them. + +13.2 No Hidden Third-Party Data Sharing + +The Software does not share any data with third parties on our behalf. +There are no embedded analytics services, advertising networks, or +data brokers within the Software. + + +14. SECURITY + +14.1 Software Security + +We implement security measures in the Software design: +- Secure password hashing (Argon2id) +- Session management with secure tokens +- Input validation and sanitization +- Protection against common web vulnerabilities + +14.2 Your Security Responsibilities + +Since all data is stored on your infrastructure, you are responsible +for: +- Keeping the Software updated +- Securing your server and database +- Implementing network security measures +- Managing user access and authentication +- Creating and securing backups + + +15. CHANGES TO THIS PRIVACY POLICY + +We may update this Privacy Policy from time to time. Material changes +will be communicated through: +- Updated "Last Updated" date at the top of this Policy +- Notice on our website +- Notice within the Software (for significant changes) + +We encourage you to review this Privacy Policy periodically. + + +16. GDPR COMPLIANCE + +Finsys complies with the General Data Protection Regulation (EU) 2016/679. + +Summary of Our Data Processing: +- We only collect personal data (email, name) when you purchase a license +- Legal basis: Contract performance and legal obligation +- Data is stored securely in the EU (Poland) +- Retention: 7 years for tax records, 3 years for correspondence +- No automated decision-making or profiling +- No data sold or shared for marketing purposes + +Your GDPR Rights (Articles 15-22): +You have the right to access, rectify, erase, restrict processing, +data portability, and object to processing of your personal data. + +To exercise any of these rights, contact: enterprise@dockhand.pro +We will respond within 30 days as required by GDPR. + + +17. YOUR RIGHTS + +If you are located in the European Economic Area (EEA), United Kingdom, +or other jurisdiction with data protection laws, you have rights +regarding personal data we hold about you (from direct communications +or license purchases): + +- Access: Request access to personal data we hold about you +- Rectification: Request correction of inaccurate data +- Erasure: Request deletion of your data +- Restriction: Request restriction of processing +- Portability: Request a copy of your data in portable format +- Objection: Object to processing based on legitimate interests +- Complaint: Lodge a complaint with a supervisory authority + +To exercise these rights, contact us at enterprise@dockhand.pro. + +Note: These rights apply to data WE hold (from direct communication or +license purchases), not to data in YOUR Software installation. For data +in your installation, YOU are the data controller and responsible for +handling such requests from your users. + + +18. SUPERVISORY AUTHORITY + +If you are located in Poland, the relevant supervisory authority is: + +Urzad Ochrony Danych Osobowych (UODO) +ul. Stawki 2 +00-193 Warszawa +Poland +https://uodo.gov.pl + +If you are located in another EEA country, you may contact your local +data protection authority. + + +19. CONTACT US + +For any privacy-related questions, concerns, or requests: + +Finsys Jaroslaw Krochmalski +ul. Borki 6 +05-119 Jozefow +Poland + +Email: enterprise@dockhand.pro +Website: https://dockhand.pro + + +================================================================================ +SUMMARY + +Dockhand is a privacy-respecting application: +- All data stays on YOUR infrastructure +- NO data is sent to Finsys servers +- NO telemetry or analytics +- YOU are the data controller for your installation +- Finsys has NO access to your data + +We believe privacy is a fundamental right, and we have designed Dockhand +to respect that right by ensuring you maintain complete control over your +data at all times. +================================================================================ + +Copyright (c) 2025-2026 Finsys Jaroslaw Krochmalski. All rights reserved. diff --git a/README.md b/README.md index 4d77500..e687248 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Dockhand + Dockhand

@@ -7,8 +7,8 @@

- Website • - Documentation • + Website • + DocumentationLicense

@@ -16,7 +16,7 @@ ## About -Dockhand is a modern, efficient Docker management application providing real-time container management, Compose stack orchestration, and multi-environment support. +Dockhand is a modern, efficient Docker management application providing real-time container management, Compose stack orchestration, and multi-environment support. All in a lightweight, secure and privacy-focused package. ### Features @@ -30,10 +30,11 @@ Dockhand is a modern, efficient Docker management application providing real-tim ## Tech Stack +- **Base**: own OS layer built from scratch using Wolfi packages via apko. Every package is explicitly declared in the Dockerfile. - **Frontend**: SvelteKit 2, Svelte 5, shadcn-svelte, TailwindCSS - **Backend**: Bun runtime with SvelteKit API routes - **Database**: SQLite or PostgreSQL via Drizzle ORM -- **Docker**: Dockerode library +- **Docker**: direct docker API calls. ## License @@ -47,10 +48,18 @@ Dockhand is licensed under the [Business Source License 1.1](LICENSE.txt) (BSL 1 See [LICENSE.txt](LICENSE.txt) for full terms. + + + Buy Me A Coffee + + + ## Links -- **Website**: [https://dockhand.io](https://dockhand.io) -- **Documentation**: [https://dockhand.io/docs](https://dockhand.io/docs) +- **Website**: [https://dockhand.pro](https://dockhand.pro) +- **Documentation**: [https://dockhand.pro/manual](https://dockhand.pro/manual) --- diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..51bc0ff --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,9 @@ +# Bun configuration for Dockhand + +[install] +# Use exact versions for reproducible builds +exact = true + +[run] +# Enable source maps for better error messages +sourcemap = "external" diff --git a/components.json b/components.json new file mode 100644 index 0000000..c5d91b4 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/docker-compose-postgresql.yaml b/docker-compose-postgresql.yaml new file mode 100644 index 0000000..a1fa941 --- /dev/null +++ b/docker-compose-postgresql.yaml @@ -0,0 +1,25 @@ +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_USER: dockhand + POSTGRES_PASSWORD: changeme + POSTGRES_DB: dockhand + volumes: + - postgres_data:/var/lib/postgresql/data + + dockhand: + image: fnsys/dockhand:latest + ports: + - 3000:3000 + environment: + DATABASE_URL: postgres://dockhand:changeme@postgres:5432/dockhand + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - dockhand_data:/app/data + depends_on: + - postgres + +volumes: + postgres_data: + dockhand_data: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b83f7c4 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,13 @@ +services: + dockhand: + image: fnsys/dockhand:latest + container_name: dockhand + restart: unless-stopped + ports: + - 3000:3000 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - dockhand_data:/app/data + +volumes: + dockhand_data: \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..82ab463 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,191 @@ +#!/bin/sh +set -e + +# Dockhand Docker Entrypoint +# === Configuration === +PUID=${PUID:-1001} +PGID=${PGID:-1001} + +# === Detect if running as root === +RUNNING_AS_ROOT=false +if [ "$(id -u)" = "0" ]; then + RUNNING_AS_ROOT=true +fi + +# === Non-root mode (user: directive in compose) === +# If container started as non-root, skip all user management and run directly +if [ "$RUNNING_AS_ROOT" = "false" ]; then + echo "Running as user $(id -u):$(id -g) (set via container user directive)" + + # Ensure data directories exist (user must have write access to DATA_DIR via volume mount) + DATA_DIR="${DATA_DIR:-/app/data}" + if [ ! -d "$DATA_DIR/db" ]; then + echo "Creating database directory at $DATA_DIR/db" + mkdir -p "$DATA_DIR/db" 2>/dev/null || { + echo "ERROR: Cannot create $DATA_DIR/db directory" + echo "Ensure the data volume is mounted with correct permissions for user $(id -u):$(id -g)" + echo "" + echo "Example docker-compose.yml:" + echo " volumes:" + echo " - ./data:/app/data # This directory must be writable by user $(id -u)" + exit 1 + } + fi + if [ ! -d "$DATA_DIR/stacks" ]; then + mkdir -p "$DATA_DIR/stacks" 2>/dev/null || true + fi + + # Check Docker socket access if mounted + SOCKET_PATH="/var/run/docker.sock" + if [ -S "$SOCKET_PATH" ]; then + if test -r "$SOCKET_PATH" 2>/dev/null; then + echo "Docker socket accessible at $SOCKET_PATH" + # Detect hostname from Docker if not set + if [ -z "$DOCKHAND_HOSTNAME" ]; then + DETECTED_HOSTNAME=$(curl -s --unix-socket "$SOCKET_PATH" http://localhost/info 2>/dev/null | sed -n 's/.*"Name":"\([^"]*\)".*/\1/p') + if [ -n "$DETECTED_HOSTNAME" ]; then + export DOCKHAND_HOSTNAME="$DETECTED_HOSTNAME" + echo "Detected Docker host hostname: $DOCKHAND_HOSTNAME" + fi + fi + else + SOCKET_GID=$(stat -c '%g' "$SOCKET_PATH" 2>/dev/null || echo "unknown") + echo "WARNING: Docker socket not readable by user $(id -u)" + echo "Add --group-add $SOCKET_GID to your docker run command" + fi + else + echo "No Docker socket found at $SOCKET_PATH" + echo "Configure Docker environments via the web UI (Settings > Environments)" + fi + + # Run directly as current user (no su-exec needed) + if [ "$1" = "" ]; then + exec bun run ./build/index.js + else + exec "$@" + fi +fi + +# === User Setup === +# Root mode: PUID=0 requested OR already running as root with default PUID/PGID +if [ "$PUID" = "0" ]; then + echo "Running as root user (PUID=0)" + RUN_USER="root" +elif [ "$RUNNING_AS_ROOT" = "true" ] && [ "$PUID" = "1001" ] && [ "$PGID" = "1001" ]; then + echo "Running as root user" + RUN_USER="root" +else + RUN_USER="dockhand" + # Only modify if PUID/PGID differ from image defaults (1001:1001) + if [ "$PUID" != "1001" ] || [ "$PGID" != "1001" ]; then + echo "Configuring user with PUID=$PUID PGID=$PGID" + + # Remove existing dockhand user/group (using busybox commands) + deluser dockhand 2>/dev/null || true + delgroup dockhand 2>/dev/null || true + + # Check for UID conflicts - warn but don't delete other users + SKIP_USER_CREATE=false + if getent passwd "$PUID" >/dev/null 2>&1; then + EXISTING=$(getent passwd "$PUID" | cut -d: -f1) + if [ "$EXISTING" = "bun" ]; then + echo "Note: UID $PUID is used by the 'bun' runtime user - reusing it for dockhand" + echo "If upgrading from a previous version, you may need to fix data permissions:" + echo " chown -R $PUID:$PGID /path/to/your/data" + RUN_USER="bun" + SKIP_USER_CREATE=true + else + echo "WARNING: UID $PUID already in use by '$EXISTING'. Using default UID 1001." + PUID=1001 + fi + fi + + # Handle GID - reuse existing group or create new + if getent group "$PGID" >/dev/null 2>&1; then + TARGET_GROUP=$(getent group "$PGID" | cut -d: -f1) + else + addgroup -g "$PGID" dockhand + TARGET_GROUP="dockhand" + fi + + if [ "$SKIP_USER_CREATE" = "false" ]; then + adduser -u "$PUID" -G "$TARGET_GROUP" -h /home/dockhand -D dockhand + fi + fi + + # === Directory Ownership === + chown -R "$RUN_USER":"$RUN_USER" /app/data 2>/dev/null || true + if [ "$RUN_USER" = "dockhand" ]; then + chown -R dockhand:dockhand /home/dockhand 2>/dev/null || true + fi + + if [ -n "$DATA_DIR" ] && [ "$DATA_DIR" != "/app/data" ] && [ "$DATA_DIR" != "./data" ]; then + mkdir -p "$DATA_DIR" + chown -R "$RUN_USER":"$RUN_USER" "$DATA_DIR" 2>/dev/null || true + fi +fi + +# === Docker Socket Access (Optional) === +# Check if Docker socket is mounted and accessible +# Note: DOCKER_HOST with tcp:// requires configuring an environment via the web UI +SOCKET_PATH="/var/run/docker.sock" + +if [ -S "$SOCKET_PATH" ]; then + # Socket exists - check if readable + if [ "$RUN_USER" != "root" ]; then + if ! su-exec "$RUN_USER" test -r "$SOCKET_PATH" 2>/dev/null; then + SOCKET_GID=$(stat -c '%g' "$SOCKET_PATH" 2>/dev/null || echo "unknown") + echo "WARNING: Docker socket at $SOCKET_PATH is not readable by $RUN_USER user" + echo "" + echo "To use local Docker, fix with one of these options:" + echo "" + echo " 1. Add container to docker group (GID: $SOCKET_GID):" + echo " docker run --group-add $SOCKET_GID ..." + echo "" + echo " 2. Use a socket proxy:" + echo " Configure a 'direct' environment pointing to tcp://socket-proxy:2375" + echo "" + echo " 3. Make socket world-readable (less secure):" + echo " chmod 666 /var/run/docker.sock" + echo "" + echo "Continuing startup - configure environments via the web UI..." + else + echo "Docker socket accessible at $SOCKET_PATH" + fi + else + echo "Docker socket accessible at $SOCKET_PATH" + fi + + # === Detect Docker Host Hostname (for license validation) === + # Query Docker API to get the real host hostname (not container ID) + if [ -z "$DOCKHAND_HOSTNAME" ]; then + DETECTED_HOSTNAME=$(curl -s --unix-socket "$SOCKET_PATH" http://localhost/info 2>/dev/null | sed -n 's/.*"Name":"\([^"]*\)".*/\1/p') + if [ -n "$DETECTED_HOSTNAME" ]; then + export DOCKHAND_HOSTNAME="$DETECTED_HOSTNAME" + echo "Detected Docker host hostname: $DOCKHAND_HOSTNAME" + fi + else + echo "Using configured hostname: $DOCKHAND_HOSTNAME" + fi +else + echo "No local Docker socket mounted (this is normal when using socket-proxy or remote Docker)" + echo "Configure your Docker environment via the web UI: Settings > Environments" +fi + +# === Run Application === +if [ "$RUN_USER" = "root" ]; then + # Running as root - execute directly + if [ "$1" = "" ]; then + exec bun run ./build/index.js + else + exec "$@" + fi +else + # Running as non-root user + echo "Running as user: $RUN_USER" + if [ "$1" = "" ]; then + exec su-exec "$RUN_USER" bun run ./build/index.js + else + exec su-exec "$RUN_USER" "$@" + fi +fi diff --git a/drizzle-pg/0000_initial_schema.sql b/drizzle-pg/0000_initial_schema.sql new file mode 100644 index 0000000..15c71a7 --- /dev/null +++ b/drizzle-pg/0000_initial_schema.sql @@ -0,0 +1,401 @@ +CREATE TABLE "audit_logs" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer, + "username" text NOT NULL, + "action" text NOT NULL, + "entity_type" text NOT NULL, + "entity_id" text, + "entity_name" text, + "environment_id" integer, + "description" text, + "details" text, + "ip_address" text, + "user_agent" text, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "auth_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "auth_enabled" boolean DEFAULT false, + "default_provider" text DEFAULT 'local', + "session_timeout" integer DEFAULT 86400, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "auto_update_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "container_name" text NOT NULL, + "enabled" boolean DEFAULT false, + "schedule_type" text DEFAULT 'daily', + "cron_expression" text, + "vulnerability_criteria" text DEFAULT 'never', + "last_checked" timestamp, + "last_updated" timestamp, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "auto_update_settings_environment_id_container_name_unique" UNIQUE("environment_id","container_name") +); +--> statement-breakpoint +CREATE TABLE "config_sets" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "description" text, + "env_vars" text, + "labels" text, + "ports" text, + "volumes" text, + "network_mode" text DEFAULT 'bridge', + "restart_policy" text DEFAULT 'no', + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "config_sets_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "container_events" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "container_id" text NOT NULL, + "container_name" text, + "image" text, + "action" text NOT NULL, + "actor_attributes" text, + "timestamp" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "environment_notifications" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer NOT NULL, + "notification_id" integer NOT NULL, + "enabled" boolean DEFAULT true, + "event_types" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "environment_notifications_environment_id_notification_id_unique" UNIQUE("environment_id","notification_id") +); +--> statement-breakpoint +CREATE TABLE "environments" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "host" text, + "port" integer DEFAULT 2375, + "protocol" text DEFAULT 'http', + "tls_ca" text, + "tls_cert" text, + "tls_key" text, + "tls_skip_verify" boolean DEFAULT false, + "icon" text DEFAULT 'globe', + "collect_activity" boolean DEFAULT true, + "collect_metrics" boolean DEFAULT true, + "highlight_changes" boolean DEFAULT true, + "labels" text, + "connection_type" text DEFAULT 'socket', + "socket_path" text DEFAULT '/var/run/docker.sock', + "hawser_token" text, + "hawser_last_seen" timestamp, + "hawser_agent_id" text, + "hawser_agent_name" text, + "hawser_version" text, + "hawser_capabilities" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "environments_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "git_credentials" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "auth_type" text DEFAULT 'none' NOT NULL, + "username" text, + "password" text, + "ssh_private_key" text, + "ssh_passphrase" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "git_credentials_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "git_repositories" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "branch" text DEFAULT 'main', + "credential_id" integer, + "compose_path" text DEFAULT 'docker-compose.yml', + "environment_id" integer, + "auto_update" boolean DEFAULT false, + "auto_update_schedule" text DEFAULT 'daily', + "auto_update_cron" text DEFAULT '0 3 * * *', + "webhook_enabled" boolean DEFAULT false, + "webhook_secret" text, + "last_sync" timestamp, + "last_commit" text, + "sync_status" text DEFAULT 'pending', + "sync_error" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "git_repositories_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "git_stacks" ( + "id" serial PRIMARY KEY NOT NULL, + "stack_name" text NOT NULL, + "environment_id" integer, + "repository_id" integer NOT NULL, + "compose_path" text DEFAULT 'docker-compose.yml', + "auto_update" boolean DEFAULT false, + "auto_update_schedule" text DEFAULT 'daily', + "auto_update_cron" text DEFAULT '0 3 * * *', + "webhook_enabled" boolean DEFAULT false, + "webhook_secret" text, + "last_sync" timestamp, + "last_commit" text, + "sync_status" text DEFAULT 'pending', + "sync_error" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "git_stacks_stack_name_environment_id_unique" UNIQUE("stack_name","environment_id") +); +--> statement-breakpoint +CREATE TABLE "hawser_tokens" ( + "id" serial PRIMARY KEY NOT NULL, + "token" text NOT NULL, + "token_prefix" text NOT NULL, + "name" text NOT NULL, + "environment_id" integer, + "is_active" boolean DEFAULT true, + "last_used" timestamp, + "created_at" timestamp DEFAULT now(), + "expires_at" timestamp, + CONSTRAINT "hawser_tokens_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "host_metrics" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "cpu_percent" double precision NOT NULL, + "memory_percent" double precision NOT NULL, + "memory_used" bigint, + "memory_total" bigint, + "timestamp" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "ldap_config" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "enabled" boolean DEFAULT false, + "server_url" text NOT NULL, + "bind_dn" text, + "bind_password" text, + "base_dn" text NOT NULL, + "user_filter" text DEFAULT '(uid={{username}})', + "username_attribute" text DEFAULT 'uid', + "email_attribute" text DEFAULT 'mail', + "display_name_attribute" text DEFAULT 'cn', + "group_base_dn" text, + "group_filter" text, + "admin_group" text, + "role_mappings" text, + "tls_enabled" boolean DEFAULT false, + "tls_ca" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "notification_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "type" text NOT NULL, + "name" text NOT NULL, + "enabled" boolean DEFAULT true, + "config" text NOT NULL, + "event_types" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "oidc_config" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "enabled" boolean DEFAULT false, + "issuer_url" text NOT NULL, + "client_id" text NOT NULL, + "client_secret" text NOT NULL, + "redirect_uri" text NOT NULL, + "scopes" text DEFAULT 'openid profile email', + "username_claim" text DEFAULT 'preferred_username', + "email_claim" text DEFAULT 'email', + "display_name_claim" text DEFAULT 'name', + "admin_claim" text, + "admin_value" text, + "role_mappings_claim" text DEFAULT 'groups', + "role_mappings" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "registries" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "url" text NOT NULL, + "username" text, + "password" text, + "is_default" boolean DEFAULT false, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "registries_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "roles" ( + "id" serial PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "description" text, + "is_system" boolean DEFAULT false, + "permissions" text NOT NULL, + "environment_ids" text, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "roles_name_unique" UNIQUE("name") +); +--> statement-breakpoint +CREATE TABLE "schedule_executions" ( + "id" serial PRIMARY KEY NOT NULL, + "schedule_type" text NOT NULL, + "schedule_id" integer NOT NULL, + "environment_id" integer, + "entity_name" text NOT NULL, + "triggered_by" text NOT NULL, + "triggered_at" timestamp NOT NULL, + "started_at" timestamp, + "completed_at" timestamp, + "duration" integer, + "status" text NOT NULL, + "error_message" text, + "details" text, + "logs" text, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "provider" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "settings" ( + "key" text PRIMARY KEY NOT NULL, + "value" text NOT NULL, + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE "stack_events" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "stack_name" text NOT NULL, + "event_type" text NOT NULL, + "timestamp" timestamp DEFAULT now(), + "metadata" text +); +--> statement-breakpoint +CREATE TABLE "stack_sources" ( + "id" serial PRIMARY KEY NOT NULL, + "stack_name" text NOT NULL, + "environment_id" integer, + "source_type" text DEFAULT 'internal' NOT NULL, + "git_repository_id" integer, + "git_stack_id" integer, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "stack_sources_stack_name_environment_id_unique" UNIQUE("stack_name","environment_id") +); +--> statement-breakpoint +CREATE TABLE "user_preferences" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer, + "environment_id" integer, + "key" text NOT NULL, + "value" text NOT NULL, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "user_preferences_user_id_environment_id_key_unique" UNIQUE("user_id","environment_id","key") +); +--> statement-breakpoint +CREATE TABLE "user_roles" ( + "id" serial PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "role_id" integer NOT NULL, + "environment_id" integer, + "created_at" timestamp DEFAULT now(), + CONSTRAINT "user_roles_user_id_role_id_environment_id_unique" UNIQUE("user_id","role_id","environment_id") +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" serial PRIMARY KEY NOT NULL, + "username" text NOT NULL, + "email" text, + "password_hash" text NOT NULL, + "display_name" text, + "avatar" text, + "auth_provider" text DEFAULT 'local', + "mfa_enabled" boolean DEFAULT false, + "mfa_secret" text, + "is_active" boolean DEFAULT true, + "last_login" timestamp, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "users_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE "vulnerability_scans" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer, + "image_id" text NOT NULL, + "image_name" text NOT NULL, + "scanner" text NOT NULL, + "scanned_at" timestamp NOT NULL, + "scan_duration" integer, + "critical_count" integer DEFAULT 0, + "high_count" integer DEFAULT 0, + "medium_count" integer DEFAULT 0, + "low_count" integer DEFAULT 0, + "negligible_count" integer DEFAULT 0, + "unknown_count" integer DEFAULT 0, + "vulnerabilities" text, + "error" text, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "auto_update_settings" ADD CONSTRAINT "auto_update_settings_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "container_events" ADD CONSTRAINT "container_events_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "environment_notifications" ADD CONSTRAINT "environment_notifications_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "environment_notifications" ADD CONSTRAINT "environment_notifications_notification_id_notification_settings_id_fk" FOREIGN KEY ("notification_id") REFERENCES "public"."notification_settings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_repositories" ADD CONSTRAINT "git_repositories_credential_id_git_credentials_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."git_credentials"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_stacks" ADD CONSTRAINT "git_stacks_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "git_stacks" ADD CONSTRAINT "git_stacks_repository_id_git_repositories_id_fk" FOREIGN KEY ("repository_id") REFERENCES "public"."git_repositories"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "hawser_tokens" ADD CONSTRAINT "hawser_tokens_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "host_metrics" ADD CONSTRAINT "host_metrics_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "schedule_executions" ADD CONSTRAINT "schedule_executions_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_events" ADD CONSTRAINT "stack_events_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_sources" ADD CONSTRAINT "stack_sources_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_sources" ADD CONSTRAINT "stack_sources_git_repository_id_git_repositories_id_fk" FOREIGN KEY ("git_repository_id") REFERENCES "public"."git_repositories"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "stack_sources" ADD CONSTRAINT "stack_sources_git_stack_id_git_stacks_id_fk" FOREIGN KEY ("git_stack_id") REFERENCES "public"."git_stacks"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_preferences" ADD CONSTRAINT "user_preferences_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "vulnerability_scans" ADD CONSTRAINT "vulnerability_scans_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "audit_logs_user_id_idx" ON "audit_logs" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs" USING btree ("created_at");--> statement-breakpoint +CREATE INDEX "container_events_env_timestamp_idx" ON "container_events" USING btree ("environment_id","timestamp");--> statement-breakpoint +CREATE INDEX "host_metrics_env_timestamp_idx" ON "host_metrics" USING btree ("environment_id","timestamp");--> statement-breakpoint +CREATE INDEX "schedule_executions_type_id_idx" ON "schedule_executions" USING btree ("schedule_type","schedule_id");--> statement-breakpoint +CREATE INDEX "sessions_user_id_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "sessions_expires_at_idx" ON "sessions" USING btree ("expires_at");--> statement-breakpoint +CREATE INDEX "vulnerability_scans_env_image_idx" ON "vulnerability_scans" USING btree ("environment_id","image_id"); \ No newline at end of file diff --git a/drizzle-pg/0001_add_stack_env_vars.sql b/drizzle-pg/0001_add_stack_env_vars.sql new file mode 100644 index 0000000..8ee2010 --- /dev/null +++ b/drizzle-pg/0001_add_stack_env_vars.sql @@ -0,0 +1,14 @@ +CREATE TABLE "stack_environment_variables" ( + "id" serial PRIMARY KEY NOT NULL, + "stack_name" text NOT NULL, + "environment_id" integer, + "key" text NOT NULL, + "value" text NOT NULL, + "is_secret" boolean DEFAULT false, + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now(), + CONSTRAINT "stack_environment_variables_stack_name_environment_id_key_unique" UNIQUE("stack_name","environment_id","key") +); +--> statement-breakpoint +ALTER TABLE "git_stacks" ADD COLUMN "env_file_path" text;--> statement-breakpoint +ALTER TABLE "stack_environment_variables" ADD CONSTRAINT "stack_environment_variables_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle-pg/0002_add_pending_container_updates.sql b/drizzle-pg/0002_add_pending_container_updates.sql new file mode 100644 index 0000000..ac712d1 --- /dev/null +++ b/drizzle-pg/0002_add_pending_container_updates.sql @@ -0,0 +1,12 @@ +CREATE TABLE "pending_container_updates" ( + "id" serial PRIMARY KEY NOT NULL, + "environment_id" integer NOT NULL, + "container_id" text NOT NULL, + "container_name" text NOT NULL, + "current_image" text NOT NULL, + "checked_at" timestamp DEFAULT now(), + "created_at" timestamp DEFAULT now(), + CONSTRAINT "pending_container_updates_environment_id_container_id_unique" UNIQUE("environment_id","container_id") +); +--> statement-breakpoint +ALTER TABLE "pending_container_updates" ADD CONSTRAINT "pending_container_updates_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle-pg/0003_add_stack_paths.sql b/drizzle-pg/0003_add_stack_paths.sql new file mode 100644 index 0000000..8102648 --- /dev/null +++ b/drizzle-pg/0003_add_stack_paths.sql @@ -0,0 +1,2 @@ +ALTER TABLE "stack_sources" ADD COLUMN "compose_path" text;--> statement-breakpoint +ALTER TABLE "stack_sources" ADD COLUMN "env_path" text; \ No newline at end of file diff --git a/drizzle-pg/meta/0000_snapshot.json b/drizzle-pg/meta/0000_snapshot.json new file mode 100644 index 0000000..c2004b5 --- /dev/null +++ b/drizzle-pg/meta/0000_snapshot.json @@ -0,0 +1,2709 @@ +{ + "id": "50905243-3288-41de-8cef-87b4e546d7cd", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/0001_snapshot.json b/drizzle-pg/meta/0001_snapshot.json new file mode 100644 index 0000000..c972fe5 --- /dev/null +++ b/drizzle-pg/meta/0001_snapshot.json @@ -0,0 +1,2803 @@ +{ + "id": "31d336d0-689e-4403-b49e-308e13df0014", + "prevId": "50905243-3288-41de-8cef-87b4e546d7cd", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_environment_variables": { + "name": "stack_environment_variables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/0002_snapshot.json b/drizzle-pg/meta/0002_snapshot.json new file mode 100644 index 0000000..209f367 --- /dev/null +++ b/drizzle-pg/meta/0002_snapshot.json @@ -0,0 +1,2883 @@ +{ + "id": "eef8322a-0ccc-418c-b0f6-f51972a1850e", + "prevId": "31d336d0-689e-4403-b49e-308e13df0014", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_container_updates": { + "name": "pending_container_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_environment_variables": { + "name": "stack_environment_variables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/0003_snapshot.json b/drizzle-pg/meta/0003_snapshot.json new file mode 100644 index 0000000..565117e --- /dev/null +++ b/drizzle-pg/meta/0003_snapshot.json @@ -0,0 +1,2895 @@ +{ + "id": "b10cba96-4947-484f-84a2-efb65205381f", + "prevId": "eef8322a-0ccc-418c-b0f6-f51972a1850e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_settings": { + "name": "auth_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auto_update_settings": { + "name": "auto_update_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_updated": { + "name": "last_updated", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.config_sets": { + "name": "config_sets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.container_events": { + "name": "container_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_notifications": { + "name": "environment_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "notification_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environments_name_unique": { + "name": "environments_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_credentials": { + "name": "git_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_repositories": { + "name": "git_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_stacks": { + "name": "git_stacks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auto_update": { + "name": "auto_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync": { + "name": "last_sync", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hawser_tokens": { + "name": "hawser_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.host_metrics": { + "name": "host_metrics", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_percent": { + "name": "memory_percent", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "memory_used": { + "name": "memory_used", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "memory_total": { + "name": "memory_total", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "timestamp", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ldap_config": { + "name": "ldap_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification_settings": { + "name": "notification_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oidc_config": { + "name": "oidc_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_container_updates": { + "name": "pending_container_updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "container_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registries": { + "name": "registries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "registries_name_unique": { + "name": "registries_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "roles_name_unique": { + "name": "roles_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_executions": { + "name": "schedule_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + { + "expression": "schedule_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_environment_variables": { + "name": "stack_environment_variables", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_secret": { + "name": "is_secret", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_events": { + "name": "stack_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.stack_sources": { + "name": "stack_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "external_compose_path": { + "name": "external_compose_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_env_path": { + "name": "external_env_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stack_name", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_preferences": { + "name": "user_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_roles": { + "name": "user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "role_id", + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vulnerability_scans": { + "name": "vulnerability_scans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scanned_at": { + "name": "scanned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle-pg/meta/_journal.json b/drizzle-pg/meta/_journal.json new file mode 100644 index 0000000..590bb1f --- /dev/null +++ b/drizzle-pg/meta/_journal.json @@ -0,0 +1,34 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1765804022462, + "tag": "0000_initial_schema", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1766378770502, + "tag": "0001_add_stack_env_vars", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1766763867484, + "tag": "0002_add_pending_container_updates", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1767687362730, + "tag": "0003_add_stack_paths", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..44f2d39 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'drizzle-kit'; + +const databaseUrl = process.env.DATABASE_URL; +const isPostgres = databaseUrl && (databaseUrl.startsWith('postgres://') || databaseUrl.startsWith('postgresql://')); + +export default defineConfig({ + // Use different schema files for SQLite vs PostgreSQL + schema: isPostgres + ? './src/lib/server/db/schema/pg-schema.ts' + : './src/lib/server/db/schema/index.ts', + out: isPostgres ? './drizzle-pg' : './drizzle', + dialect: isPostgres ? 'postgresql' : 'sqlite', + dbCredentials: isPostgres + ? { url: databaseUrl! } + : { url: `file:${process.env.DATA_DIR || './data'}/dockhand.db` } +}); diff --git a/drizzle/0000_initial_schema.sql b/drizzle/0000_initial_schema.sql new file mode 100644 index 0000000..b04383a --- /dev/null +++ b/drizzle/0000_initial_schema.sql @@ -0,0 +1,401 @@ +CREATE TABLE `audit_logs` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer, + `username` text NOT NULL, + `action` text NOT NULL, + `entity_type` text NOT NULL, + `entity_id` text, + `entity_name` text, + `environment_id` integer, + `description` text, + `details` text, + `ip_address` text, + `user_agent` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE INDEX `audit_logs_user_id_idx` ON `audit_logs` (`user_id`);--> statement-breakpoint +CREATE INDEX `audit_logs_created_at_idx` ON `audit_logs` (`created_at`);--> statement-breakpoint +CREATE TABLE `auth_settings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `auth_enabled` integer DEFAULT false, + `default_provider` text DEFAULT 'local', + `session_timeout` integer DEFAULT 86400, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `auto_update_settings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `container_name` text NOT NULL, + `enabled` integer DEFAULT false, + `schedule_type` text DEFAULT 'daily', + `cron_expression` text, + `vulnerability_criteria` text DEFAULT 'never', + `last_checked` text, + `last_updated` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE UNIQUE INDEX `auto_update_settings_environment_id_container_name_unique` ON `auto_update_settings` (`environment_id`,`container_name`);--> statement-breakpoint +CREATE TABLE `config_sets` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `description` text, + `env_vars` text, + `labels` text, + `ports` text, + `volumes` text, + `network_mode` text DEFAULT 'bridge', + `restart_policy` text DEFAULT 'no', + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `config_sets_name_unique` ON `config_sets` (`name`);--> statement-breakpoint +CREATE TABLE `container_events` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `container_id` text NOT NULL, + `container_name` text, + `image` text, + `action` text NOT NULL, + `actor_attributes` text, + `timestamp` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `container_events_env_timestamp_idx` ON `container_events` (`environment_id`,`timestamp`);--> statement-breakpoint +CREATE TABLE `environment_notifications` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer NOT NULL, + `notification_id` integer NOT NULL, + `enabled` integer DEFAULT true, + `event_types` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`notification_id`) REFERENCES `notification_settings`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `environment_notifications_environment_id_notification_id_unique` ON `environment_notifications` (`environment_id`,`notification_id`);--> statement-breakpoint +CREATE TABLE `environments` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `host` text, + `port` integer DEFAULT 2375, + `protocol` text DEFAULT 'http', + `tls_ca` text, + `tls_cert` text, + `tls_key` text, + `tls_skip_verify` integer DEFAULT false, + `icon` text DEFAULT 'globe', + `collect_activity` integer DEFAULT true, + `collect_metrics` integer DEFAULT true, + `highlight_changes` integer DEFAULT true, + `labels` text, + `connection_type` text DEFAULT 'socket', + `socket_path` text DEFAULT '/var/run/docker.sock', + `hawser_token` text, + `hawser_last_seen` text, + `hawser_agent_id` text, + `hawser_agent_name` text, + `hawser_version` text, + `hawser_capabilities` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `environments_name_unique` ON `environments` (`name`);--> statement-breakpoint +CREATE TABLE `git_credentials` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `auth_type` text DEFAULT 'none' NOT NULL, + `username` text, + `password` text, + `ssh_private_key` text, + `ssh_passphrase` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `git_credentials_name_unique` ON `git_credentials` (`name`);--> statement-breakpoint +CREATE TABLE `git_repositories` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `url` text NOT NULL, + `branch` text DEFAULT 'main', + `credential_id` integer, + `compose_path` text DEFAULT 'docker-compose.yml', + `environment_id` integer, + `auto_update` integer DEFAULT false, + `auto_update_schedule` text DEFAULT 'daily', + `auto_update_cron` text DEFAULT '0 3 * * *', + `webhook_enabled` integer DEFAULT false, + `webhook_secret` text, + `last_sync` text, + `last_commit` text, + `sync_status` text DEFAULT 'pending', + `sync_error` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`credential_id`) REFERENCES `git_credentials`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `git_repositories_name_unique` ON `git_repositories` (`name`);--> statement-breakpoint +CREATE TABLE `git_stacks` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `stack_name` text NOT NULL, + `environment_id` integer, + `repository_id` integer NOT NULL, + `compose_path` text DEFAULT 'docker-compose.yml', + `auto_update` integer DEFAULT false, + `auto_update_schedule` text DEFAULT 'daily', + `auto_update_cron` text DEFAULT '0 3 * * *', + `webhook_enabled` integer DEFAULT false, + `webhook_secret` text, + `last_sync` text, + `last_commit` text, + `sync_status` text DEFAULT 'pending', + `sync_error` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`repository_id`) REFERENCES `git_repositories`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `git_stacks_stack_name_environment_id_unique` ON `git_stacks` (`stack_name`,`environment_id`);--> statement-breakpoint +CREATE TABLE `hawser_tokens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `token` text NOT NULL, + `token_prefix` text NOT NULL, + `name` text NOT NULL, + `environment_id` integer, + `is_active` integer DEFAULT true, + `last_used` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `expires_at` text, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `hawser_tokens_token_unique` ON `hawser_tokens` (`token`);--> statement-breakpoint +CREATE TABLE `host_metrics` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `cpu_percent` real NOT NULL, + `memory_percent` real NOT NULL, + `memory_used` integer, + `memory_total` integer, + `timestamp` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `host_metrics_env_timestamp_idx` ON `host_metrics` (`environment_id`,`timestamp`);--> statement-breakpoint +CREATE TABLE `ldap_config` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT false, + `server_url` text NOT NULL, + `bind_dn` text, + `bind_password` text, + `base_dn` text NOT NULL, + `user_filter` text DEFAULT '(uid={{username}})', + `username_attribute` text DEFAULT 'uid', + `email_attribute` text DEFAULT 'mail', + `display_name_attribute` text DEFAULT 'cn', + `group_base_dn` text, + `group_filter` text, + `admin_group` text, + `role_mappings` text, + `tls_enabled` integer DEFAULT false, + `tls_ca` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `notification_settings` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `type` text NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT true, + `config` text NOT NULL, + `event_types` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `oidc_config` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT false, + `issuer_url` text NOT NULL, + `client_id` text NOT NULL, + `client_secret` text NOT NULL, + `redirect_uri` text NOT NULL, + `scopes` text DEFAULT 'openid profile email', + `username_claim` text DEFAULT 'preferred_username', + `email_claim` text DEFAULT 'email', + `display_name_claim` text DEFAULT 'name', + `admin_claim` text, + `admin_value` text, + `role_mappings_claim` text DEFAULT 'groups', + `role_mappings` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `registries` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `url` text NOT NULL, + `username` text, + `password` text, + `is_default` integer DEFAULT false, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `registries_name_unique` ON `registries` (`name`);--> statement-breakpoint +CREATE TABLE `roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `description` text, + `is_system` integer DEFAULT false, + `permissions` text NOT NULL, + `environment_ids` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `roles_name_unique` ON `roles` (`name`);--> statement-breakpoint +CREATE TABLE `schedule_executions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `schedule_type` text NOT NULL, + `schedule_id` integer NOT NULL, + `environment_id` integer, + `entity_name` text NOT NULL, + `triggered_by` text NOT NULL, + `triggered_at` text NOT NULL, + `started_at` text, + `completed_at` text, + `duration` integer, + `status` text NOT NULL, + `error_message` text, + `details` text, + `logs` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `schedule_executions_type_id_idx` ON `schedule_executions` (`schedule_type`,`schedule_id`);--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` text PRIMARY KEY NOT NULL, + `user_id` integer NOT NULL, + `provider` text NOT NULL, + `expires_at` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `sessions_user_id_idx` ON `sessions` (`user_id`);--> statement-breakpoint +CREATE INDEX `sessions_expires_at_idx` ON `sessions` (`expires_at`);--> statement-breakpoint +CREATE TABLE `settings` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE TABLE `stack_events` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `stack_name` text NOT NULL, + `event_type` text NOT NULL, + `timestamp` text DEFAULT CURRENT_TIMESTAMP, + `metadata` text, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `stack_sources` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `stack_name` text NOT NULL, + `environment_id` integer, + `source_type` text DEFAULT 'internal' NOT NULL, + `git_repository_id` integer, + `git_stack_id` integer, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`git_repository_id`) REFERENCES `git_repositories`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`git_stack_id`) REFERENCES `git_stacks`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE UNIQUE INDEX `stack_sources_stack_name_environment_id_unique` ON `stack_sources` (`stack_name`,`environment_id`);--> statement-breakpoint +CREATE TABLE `user_preferences` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer, + `environment_id` integer, + `key` text NOT NULL, + `value` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_preferences_user_id_environment_id_key_unique` ON `user_preferences` (`user_id`,`environment_id`,`key`);--> statement-breakpoint +CREATE TABLE `user_roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `role_id` integer NOT NULL, + `environment_id` integer, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `user_roles_user_id_role_id_environment_id_unique` ON `user_roles` (`user_id`,`role_id`,`environment_id`);--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `username` text NOT NULL, + `email` text, + `password_hash` text NOT NULL, + `display_name` text, + `avatar` text, + `auth_provider` text DEFAULT 'local', + `mfa_enabled` integer DEFAULT false, + `mfa_secret` text, + `is_active` integer DEFAULT true, + `last_login` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);--> statement-breakpoint +CREATE TABLE `vulnerability_scans` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer, + `image_id` text NOT NULL, + `image_name` text NOT NULL, + `scanner` text NOT NULL, + `scanned_at` text NOT NULL, + `scan_duration` integer, + `critical_count` integer DEFAULT 0, + `high_count` integer DEFAULT 0, + `medium_count` integer DEFAULT 0, + `low_count` integer DEFAULT 0, + `negligible_count` integer DEFAULT 0, + `unknown_count` integer DEFAULT 0, + `vulnerabilities` text, + `error` text, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `vulnerability_scans_env_image_idx` ON `vulnerability_scans` (`environment_id`,`image_id`); \ No newline at end of file diff --git a/drizzle/0001_add_stack_env_vars.sql b/drizzle/0001_add_stack_env_vars.sql new file mode 100644 index 0000000..aa52b21 --- /dev/null +++ b/drizzle/0001_add_stack_env_vars.sql @@ -0,0 +1,14 @@ +CREATE TABLE `stack_environment_variables` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `stack_name` text NOT NULL, + `environment_id` integer, + `key` text NOT NULL, + `value` text NOT NULL, + `is_secret` integer DEFAULT false, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + `updated_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `stack_environment_variables_stack_name_environment_id_key_unique` ON `stack_environment_variables` (`stack_name`,`environment_id`,`key`);--> statement-breakpoint +ALTER TABLE `git_stacks` ADD `env_file_path` text; \ No newline at end of file diff --git a/drizzle/0002_add_pending_container_updates.sql b/drizzle/0002_add_pending_container_updates.sql new file mode 100644 index 0000000..f3c87a6 --- /dev/null +++ b/drizzle/0002_add_pending_container_updates.sql @@ -0,0 +1,12 @@ +CREATE TABLE `pending_container_updates` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `environment_id` integer NOT NULL, + `container_id` text NOT NULL, + `container_name` text NOT NULL, + `current_image` text NOT NULL, + `checked_at` text DEFAULT CURRENT_TIMESTAMP, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`environment_id`) REFERENCES `environments`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `pending_container_updates_environment_id_container_id_unique` ON `pending_container_updates` (`environment_id`,`container_id`); \ No newline at end of file diff --git a/drizzle/0003_add_stack_paths.sql b/drizzle/0003_add_stack_paths.sql new file mode 100644 index 0000000..a9447ea --- /dev/null +++ b/drizzle/0003_add_stack_paths.sql @@ -0,0 +1,2 @@ +ALTER TABLE `stack_sources` ADD `compose_path` text;--> statement-breakpoint +ALTER TABLE `stack_sources` ADD `env_path` text; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..0aaf6ba --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,2824 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d7d12244-ddb1-4246-844c-56f6c903ea29", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..4a5d606 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,2924 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "9dd11a39-a911-4c3f-9c2f-6920b14c2d96", + "prevId": "d7d12244-ddb1-4246-844c-56f6c903ea29", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_environment_variables": { + "name": "stack_environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_secret": { + "name": "is_secret", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "columns": [ + "stack_name", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..f99d580 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,3008 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "31bce98b-04c0-4e21-8cb0-49a67c345d87", + "prevId": "9dd11a39-a911-4c3f-9c2f-6920b14c2d96", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_container_updates": { + "name": "pending_container_updates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "columns": [ + "environment_id", + "container_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_environment_variables": { + "name": "stack_environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_secret": { + "name": "is_secret", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "columns": [ + "stack_name", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..5a24de1 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,3022 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6414712d-d1a8-437b-9d1c-e339b4829a85", + "prevId": "31bce98b-04c0-4e21-8cb0-49a67c345d87", + "tables": { + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_logs_environment_id_environments_id_fk": { + "name": "audit_logs_environment_id_environments_id_fk", + "tableFrom": "audit_logs", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auth_settings": { + "name": "auth_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "auth_enabled": { + "name": "auth_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "default_provider": { + "name": "default_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "session_timeout": { + "name": "session_timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 86400 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "auto_update_settings": { + "name": "auto_update_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "vulnerability_criteria": { + "name": "vulnerability_criteria", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'never'" + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_updated": { + "name": "last_updated", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "auto_update_settings_environment_id_container_name_unique": { + "name": "auto_update_settings_environment_id_container_name_unique", + "columns": [ + "environment_id", + "container_name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "auto_update_settings_environment_id_environments_id_fk": { + "name": "auto_update_settings_environment_id_environments_id_fk", + "tableFrom": "auto_update_settings", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "config_sets": { + "name": "config_sets", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_vars": { + "name": "env_vars", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ports": { + "name": "ports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volumes": { + "name": "volumes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_mode": { + "name": "network_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'bridge'" + }, + "restart_policy": { + "name": "restart_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'no'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "config_sets_name_unique": { + "name": "config_sets_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "container_events": { + "name": "container_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_attributes": { + "name": "actor_attributes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "container_events_env_timestamp_idx": { + "name": "container_events_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "container_events_environment_id_environments_id_fk": { + "name": "container_events_environment_id_environments_id_fk", + "tableFrom": "container_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environment_notifications": { + "name": "environment_notifications", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notification_id": { + "name": "notification_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environment_notifications_environment_id_notification_id_unique": { + "name": "environment_notifications_environment_id_notification_id_unique", + "columns": [ + "environment_id", + "notification_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "environment_notifications_environment_id_environments_id_fk": { + "name": "environment_notifications_environment_id_environments_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_notifications_notification_id_notification_settings_id_fk": { + "name": "environment_notifications_notification_id_notification_settings_id_fk", + "tableFrom": "environment_notifications", + "tableTo": "notification_settings", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "environments": { + "name": "environments", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 2375 + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'http'" + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_cert": { + "name": "tls_cert", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_key": { + "name": "tls_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_skip_verify": { + "name": "tls_skip_verify", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'globe'" + }, + "collect_activity": { + "name": "collect_activity", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "collect_metrics": { + "name": "collect_metrics", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "highlight_changes": { + "name": "highlight_changes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "connection_type": { + "name": "connection_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'socket'" + }, + "socket_path": { + "name": "socket_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/var/run/docker.sock'" + }, + "hawser_token": { + "name": "hawser_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_last_seen": { + "name": "hawser_last_seen", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_id": { + "name": "hawser_agent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_agent_name": { + "name": "hawser_agent_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_version": { + "name": "hawser_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hawser_capabilities": { + "name": "hawser_capabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "environments_name_unique": { + "name": "environments_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_credentials": { + "name": "git_credentials", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_private_key": { + "name": "ssh_private_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssh_passphrase": { + "name": "ssh_passphrase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_credentials_name_unique": { + "name": "git_credentials_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_repositories": { + "name": "git_repositories", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'main'" + }, + "credential_id": { + "name": "credential_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_repositories_name_unique": { + "name": "git_repositories_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_repositories_credential_id_git_credentials_id_fk": { + "name": "git_repositories_credential_id_git_credentials_id_fk", + "tableFrom": "git_repositories", + "tableTo": "git_credentials", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "git_stacks": { + "name": "git_stacks", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'docker-compose.yml'" + }, + "env_file_path": { + "name": "env_file_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_update": { + "name": "auto_update", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "auto_update_schedule": { + "name": "auto_update_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'daily'" + }, + "auto_update_cron": { + "name": "auto_update_cron", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'0 3 * * *'" + }, + "webhook_enabled": { + "name": "webhook_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "webhook_secret": { + "name": "webhook_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_sync": { + "name": "last_sync", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_commit": { + "name": "last_commit", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_status": { + "name": "sync_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "git_stacks_stack_name_environment_id_unique": { + "name": "git_stacks_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "git_stacks_environment_id_environments_id_fk": { + "name": "git_stacks_environment_id_environments_id_fk", + "tableFrom": "git_stacks", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "git_stacks_repository_id_git_repositories_id_fk": { + "name": "git_stacks_repository_id_git_repositories_id_fk", + "tableFrom": "git_stacks", + "tableTo": "git_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "hawser_tokens": { + "name": "hawser_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_prefix": { + "name": "token_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_used": { + "name": "last_used", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "hawser_tokens_token_unique": { + "name": "hawser_tokens_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hawser_tokens_environment_id_environments_id_fk": { + "name": "hawser_tokens_environment_id_environments_id_fk", + "tableFrom": "hawser_tokens", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "host_metrics": { + "name": "host_metrics", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_percent": { + "name": "cpu_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_percent": { + "name": "memory_percent", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "host_metrics_env_timestamp_idx": { + "name": "host_metrics_env_timestamp_idx", + "columns": [ + "environment_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "host_metrics_environment_id_environments_id_fk": { + "name": "host_metrics_environment_id_environments_id_fk", + "tableFrom": "host_metrics", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "server_url": { + "name": "server_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bind_password": { + "name": "bind_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'(uid={{username}})'" + }, + "username_attribute": { + "name": "username_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'uid'" + }, + "email_attribute": { + "name": "email_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'mail'" + }, + "display_name_attribute": { + "name": "display_name_attribute", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'cn'" + }, + "group_base_dn": { + "name": "group_base_dn", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "group_filter": { + "name": "group_filter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_group": { + "name": "admin_group", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tls_enabled": { + "name": "tls_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "tls_ca": { + "name": "tls_ca", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_types": { + "name": "event_types", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oidc_config": { + "name": "oidc_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "issuer_url": { + "name": "issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'openid profile email'" + }, + "username_claim": { + "name": "username_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'preferred_username'" + }, + "email_claim": { + "name": "email_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'email'" + }, + "display_name_claim": { + "name": "display_name_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'name'" + }, + "admin_claim": { + "name": "admin_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "admin_value": { + "name": "admin_value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role_mappings_claim": { + "name": "role_mappings_claim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'groups'" + }, + "role_mappings": { + "name": "role_mappings", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_container_updates": { + "name": "pending_container_updates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "container_name": { + "name": "container_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "current_image": { + "name": "current_image", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checked_at": { + "name": "checked_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "pending_container_updates_environment_id_container_id_unique": { + "name": "pending_container_updates_environment_id_container_id_unique", + "columns": [ + "environment_id", + "container_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "pending_container_updates_environment_id_environments_id_fk": { + "name": "pending_container_updates_environment_id_environments_id_fk", + "tableFrom": "pending_container_updates", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "registries": { + "name": "registries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "registries_name_unique": { + "name": "registries_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_system": { + "name": "is_system", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_ids": { + "name": "environment_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "schedule_executions": { + "name": "schedule_executions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_name": { + "name": "entity_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "triggered_at": { + "name": "triggered_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logs": { + "name": "logs", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "schedule_executions_type_id_idx": { + "name": "schedule_executions_type_id_idx", + "columns": [ + "schedule_type", + "schedule_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "schedule_executions_environment_id_environments_id_fk": { + "name": "schedule_executions_environment_id_environments_id_fk", + "tableFrom": "schedule_executions", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "sessions_expires_at_idx": { + "name": "sessions_expires_at_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_environment_variables": { + "name": "stack_environment_variables", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_secret": { + "name": "is_secret", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_environment_variables_stack_name_environment_id_key_unique": { + "name": "stack_environment_variables_stack_name_environment_id_key_unique", + "columns": [ + "stack_name", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_environment_variables_environment_id_environments_id_fk": { + "name": "stack_environment_variables_environment_id_environments_id_fk", + "tableFrom": "stack_environment_variables", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_events": { + "name": "stack_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "stack_events_environment_id_environments_id_fk": { + "name": "stack_events_environment_id_environments_id_fk", + "tableFrom": "stack_events", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stack_sources": { + "name": "stack_sources", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "stack_name": { + "name": "stack_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'internal'" + }, + "git_repository_id": { + "name": "git_repository_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "git_stack_id": { + "name": "git_stack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "compose_path": { + "name": "compose_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "env_path": { + "name": "env_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "stack_sources_stack_name_environment_id_unique": { + "name": "stack_sources_stack_name_environment_id_unique", + "columns": [ + "stack_name", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "stack_sources_environment_id_environments_id_fk": { + "name": "stack_sources_environment_id_environments_id_fk", + "tableFrom": "stack_sources", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "stack_sources_git_repository_id_git_repositories_id_fk": { + "name": "stack_sources_git_repository_id_git_repositories_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_repositories", + "columnsFrom": [ + "git_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "stack_sources_git_stack_id_git_stacks_id_fk": { + "name": "stack_sources_git_stack_id_git_stacks_id_fk", + "tableFrom": "stack_sources", + "tableTo": "git_stacks", + "columnsFrom": [ + "git_stack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_preferences": { + "name": "user_preferences", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_preferences_user_id_environment_id_key_unique": { + "name": "user_preferences_user_id_environment_id_key_unique", + "columns": [ + "user_id", + "environment_id", + "key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_preferences_user_id_users_id_fk": { + "name": "user_preferences_user_id_users_id_fk", + "tableFrom": "user_preferences", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_preferences_environment_id_environments_id_fk": { + "name": "user_preferences_environment_id_environments_id_fk", + "tableFrom": "user_preferences", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_roles": { + "name": "user_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "user_roles_user_id_role_id_environment_id_unique": { + "name": "user_roles_user_id_role_id_environment_id_unique", + "columns": [ + "user_id", + "role_id", + "environment_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_roles_user_id_users_id_fk": { + "name": "user_roles_user_id_users_id_fk", + "tableFrom": "user_roles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_role_id_roles_id_fk": { + "name": "user_roles_role_id_roles_id_fk", + "tableFrom": "user_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_roles_environment_id_environments_id_fk": { + "name": "user_roles_environment_id_environments_id_fk", + "tableFrom": "user_roles", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'local'" + }, + "mfa_enabled": { + "name": "mfa_enabled", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "mfa_secret": { + "name": "mfa_secret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "last_login": { + "name": "last_login", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "vulnerability_scans": { + "name": "vulnerability_scans", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "environment_id": { + "name": "environment_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_name": { + "name": "image_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanner": { + "name": "scanner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scanned_at": { + "name": "scanned_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scan_duration": { + "name": "scan_duration", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "critical_count": { + "name": "critical_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "high_count": { + "name": "high_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "medium_count": { + "name": "medium_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "low_count": { + "name": "low_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "negligible_count": { + "name": "negligible_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "unknown_count": { + "name": "unknown_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "vulnerabilities": { + "name": "vulnerabilities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "vulnerability_scans_env_image_idx": { + "name": "vulnerability_scans_env_image_idx", + "columns": [ + "environment_id", + "image_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "vulnerability_scans_environment_id_environments_id_fk": { + "name": "vulnerability_scans_environment_id_environments_id_fk", + "tableFrom": "vulnerability_scans", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..f4192f3 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,34 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1765804016391, + "tag": "0000_initial_schema", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1766378754939, + "tag": "0001_add_stack_env_vars", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1766763860091, + "tag": "0002_add_pending_container_updates", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1767689000000, + "tag": "0003_add_stack_paths", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/lib/.DS_Store b/lib/.DS_Store deleted file mode 100644 index cd8d495..0000000 Binary files a/lib/.DS_Store and /dev/null differ diff --git a/lib/components/StackEnvVarsPanel.svelte b/lib/components/StackEnvVarsPanel.svelte deleted file mode 100644 index 15446ec..0000000 --- a/lib/components/StackEnvVarsPanel.svelte +++ /dev/null @@ -1,236 +0,0 @@ - - -
- -
-
-
- Environment variables - {#if infoText} - - - - - -

{infoText}

-
-
- {/if} -
- {#if !readonly} -
- - - {#if hasVariables} - - - - {/if} -
- - {/if} -
- -
- ${`{VAR}`} required - ${`{VAR:-default}`} optional - ${`{VAR:?error}`} required w/ error -
- - {#if validation} -
- {#if validation.missing.length > 0} - - {validation.missing.length} missing - - {/if} - {#if validation.required.length > 0} - - {validation.required.length - validation.missing.length} required - - {/if} - {#if validation.optional.length > 0} - - {validation.optional.length} optional - - {/if} - {#if validation.unused.length > 0} - - {validation.unused.length} unused - - {/if} -
- {/if} - - {#if validation && validation.missing.length > 0 && !readonly} -
- Add missing: - {#each validation.missing as missing} - - {/each} -
- {/if} -
- -
- -
-
diff --git a/lib/server/.DS_Store b/lib/server/.DS_Store deleted file mode 100644 index 7d5961e..0000000 Binary files a/lib/server/.DS_Store and /dev/null differ diff --git a/lib/server/db/.DS_Store b/lib/server/db/.DS_Store deleted file mode 100644 index cff8ab3..0000000 Binary files a/lib/server/db/.DS_Store and /dev/null differ diff --git a/lib/server/metrics-collector.ts b/lib/server/metrics-collector.ts deleted file mode 100644 index dbccbff..0000000 --- a/lib/server/metrics-collector.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { saveHostMetric, getEnvironments, getEnvSetting } from './db'; -import { listContainers, getContainerStats, getDockerInfo, getDiskUsage } from './docker'; -import { sendEventNotification } from './notifications'; -import os from 'node:os'; - -const COLLECT_INTERVAL = 10000; // 10 seconds -const DISK_CHECK_INTERVAL = 300000; // 5 minutes -const DEFAULT_DISK_THRESHOLD = 80; // 80% threshold for disk warnings - -let collectorInterval: ReturnType | null = null; -let diskCheckInterval: ReturnType | null = null; - -// Track last disk warning sent per environment to avoid spamming -const lastDiskWarning: Map = new Map(); -const DISK_WARNING_COOLDOWN = 3600000; // 1 hour between warnings - -/** - * Collect metrics for a single environment - */ -async function collectEnvMetrics(env: { id: number; name: string; collectMetrics?: boolean }) { - try { - // Skip environments where metrics collection is disabled - if (env.collectMetrics === false) { - return; - } - - // Get running containers - const containers = await listContainers(false, env.id); // Only running - let totalCpuPercent = 0; - let totalMemUsed = 0; - - // Get stats for each running container - const statsPromises = containers.map(async (container) => { - try { - const stats = await getContainerStats(container.id, env.id) as any; - - // Calculate CPU percentage - const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; - const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; - const cpuCount = stats.cpu_stats.online_cpus || os.cpus().length; - - let cpuPercent = 0; - if (systemDelta > 0 && cpuDelta > 0) { - cpuPercent = (cpuDelta / systemDelta) * cpuCount * 100; - } - - // Get container memory usage - const memUsage = stats.memory_stats?.usage || 0; - const memCache = stats.memory_stats?.stats?.cache || 0; - // Subtract cache from usage to get actual memory used by the container - const actualMemUsed = memUsage - memCache; - - return { cpu: cpuPercent, mem: actualMemUsed > 0 ? actualMemUsed : memUsage }; - } catch { - return { cpu: 0, mem: 0 }; - } - }); - - const statsResults = await Promise.all(statsPromises); - totalCpuPercent = statsResults.reduce((sum, v) => sum + v.cpu, 0); - totalMemUsed = statsResults.reduce((sum, v) => sum + v.mem, 0); - - // Get host total memory from Docker info (this is the remote host's memory) - const info = await getDockerInfo(env.id) as any; - const memTotal = info.MemTotal || os.totalmem(); - - // Calculate memory percentage based on container usage vs host total - const memPercent = memTotal > 0 ? (totalMemUsed / memTotal) * 100 : 0; - - // Normalize CPU by number of cores from the remote host - const cpuCount = info.NCPU || os.cpus().length; - const normalizedCpu = totalCpuPercent / cpuCount; - - // Save to database - await saveHostMetric( - normalizedCpu, - memPercent, - totalMemUsed, - memTotal, - env.id - ); - } catch (error) { - // Skip this environment if it fails (might be offline) - console.error(`Failed to collect metrics for ${env.name}:`, error); - } -} - -async function collectMetrics() { - try { - const environments = await getEnvironments(); - - // Filter enabled environments and collect metrics in parallel - const enabledEnvs = environments.filter(env => env.collectMetrics !== false); - - // Process all environments in parallel for better performance - await Promise.all(enabledEnvs.map(env => collectEnvMetrics(env))); - } catch (error) { - console.error('Metrics collection error:', error); - } -} - -/** - * Check disk space for a single environment - */ -async function checkEnvDiskSpace(env: { id: number; name: string; collectMetrics?: boolean }) { - try { - // Skip environments where metrics collection is disabled - if (env.collectMetrics === false) { - return; - } - - // Check if we're in cooldown for this environment - const lastWarningTime = lastDiskWarning.get(env.id); - if (lastWarningTime && Date.now() - lastWarningTime < DISK_WARNING_COOLDOWN) { - return; // Skip this environment, still in cooldown - } - - // Get Docker disk usage data - const diskData = await getDiskUsage(env.id) as any; - if (!diskData) return; - - // Calculate total Docker disk usage using reduce for cleaner code - let totalUsed = 0; - if (diskData.Images) { - totalUsed += diskData.Images.reduce((sum: number, img: any) => sum + (img.Size || 0), 0); - } - if (diskData.Containers) { - totalUsed += diskData.Containers.reduce((sum: number, c: any) => sum + (c.SizeRw || 0), 0); - } - if (diskData.Volumes) { - totalUsed += diskData.Volumes.reduce((sum: number, v: any) => sum + (v.UsageData?.Size || 0), 0); - } - if (diskData.BuildCache) { - totalUsed += diskData.BuildCache.reduce((sum: number, bc: any) => sum + (bc.Size || 0), 0); - } - - // Get Docker root filesystem info from Docker info - const info = await getDockerInfo(env.id) as any; - const driverStatus = info?.DriverStatus; - - // Try to find "Data Space Total" from driver status - let dataSpaceTotal = 0; - let diskPercentUsed = 0; - - if (driverStatus) { - for (const [key, value] of driverStatus) { - if (key === 'Data Space Total' && typeof value === 'string') { - dataSpaceTotal = parseSize(value); - break; - } - } - } - - // If we found total disk space, calculate percentage - if (dataSpaceTotal > 0) { - diskPercentUsed = (totalUsed / dataSpaceTotal) * 100; - } else { - // Fallback: just report absolute usage if we can't determine percentage - const GB = 1024 * 1024 * 1024; - if (totalUsed > 50 * GB) { - await sendEventNotification('disk_space_warning', { - title: 'High Docker disk usage', - message: `Environment "${env.name}" is using ${formatSize(totalUsed)} of Docker disk space`, - type: 'warning' - }, env.id); - lastDiskWarning.set(env.id, Date.now()); - } - return; - } - - // Check against threshold - const threshold = await getEnvSetting('disk_warning_threshold', env.id) || DEFAULT_DISK_THRESHOLD; - if (diskPercentUsed >= threshold) { - console.log(`[Metrics] Docker disk usage for ${env.name}: ${diskPercentUsed.toFixed(1)}% (threshold: ${threshold}%)`); - - await sendEventNotification('disk_space_warning', { - title: 'Disk space warning', - message: `Environment "${env.name}" Docker disk usage is at ${diskPercentUsed.toFixed(1)}% (${formatSize(totalUsed)} used)`, - type: 'warning' - }, env.id); - - lastDiskWarning.set(env.id, Date.now()); - } - } catch (error) { - // Skip this environment if it fails - console.error(`Failed to check disk space for ${env.name}:`, error); - } -} - -/** - * Check Docker disk usage and send warnings if above threshold - */ -async function checkDiskSpace() { - try { - const environments = await getEnvironments(); - - // Filter enabled environments and check disk space in parallel - const enabledEnvs = environments.filter(env => env.collectMetrics !== false); - - // Process all environments in parallel for better performance - await Promise.all(enabledEnvs.map(env => checkEnvDiskSpace(env))); - } catch (error) { - console.error('Disk space check error:', error); - } -} - -/** - * Parse size string like "107.4GB" to bytes - */ -function parseSize(sizeStr: string): number { - const units: Record = { - 'B': 1, - 'KB': 1024, - 'MB': 1024 * 1024, - 'GB': 1024 * 1024 * 1024, - 'TB': 1024 * 1024 * 1024 * 1024 - }; - - const match = sizeStr.match(/^([\d.]+)\s*([KMGT]?B)$/i); - if (!match) return 0; - - const value = parseFloat(match[1]); - const unit = match[2].toUpperCase(); - return value * (units[unit] || 1); -} - -/** - * Format bytes to human readable string - */ -function formatSize(bytes: number): string { - const units = ['B', 'KB', 'MB', 'GB', 'TB']; - let unitIndex = 0; - let size = bytes; - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; - } - - return `${size.toFixed(1)} ${units[unitIndex]}`; -} - -export function startMetricsCollector() { - if (collectorInterval) return; // Already running - - console.log('Starting server-side metrics collector (every 10s)'); - - // Initial collection - collectMetrics(); - - // Schedule regular collection - collectorInterval = setInterval(collectMetrics, COLLECT_INTERVAL); - - // Start disk space checking (every 5 minutes) - console.log('Starting disk space monitoring (every 5 minutes)'); - checkDiskSpace(); // Initial check - diskCheckInterval = setInterval(checkDiskSpace, DISK_CHECK_INTERVAL); -} - -export function stopMetricsCollector() { - if (collectorInterval) { - clearInterval(collectorInterval); - collectorInterval = null; - } - if (diskCheckInterval) { - clearInterval(diskCheckInterval); - diskCheckInterval = null; - } - lastDiskWarning.clear(); - console.log('Metrics collector stopped'); -} diff --git a/lib/server/stacks.ts b/lib/server/stacks.ts deleted file mode 100644 index d4c99a1..0000000 --- a/lib/server/stacks.ts +++ /dev/null @@ -1,1109 +0,0 @@ -/** - * Stack Management Module - * - * Provides compose-first stack operations for internal, git, and external stacks. - * All lifecycle operations use docker compose commands. - */ - -import { existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs'; -import { join, resolve } from 'node:path'; -import { - getEnvironment, - getStackEnvVarsAsRecord, - getStackSource, - upsertStackSource, - deleteStackSource, - getGitStackByName, - deleteGitStack, - getStackSources, - deleteStackEnvVars -} from './db'; -import { deleteGitStackFiles } from './git'; - -// ============================================================================= -// TYPES -// ============================================================================= - -/** - * Stack source types - */ -export type StackSourceType = 'internal' | 'git' | 'external'; - -/** - * Stack operation result - */ -export interface StackOperationResult { - success: boolean; - output?: string; - error?: string; -} - -/** - * Container detail within a stack - */ -export interface ContainerDetail { - id: string; - name: string; - service: string; - state: string; - status: string; - health?: string; - image: string; - ports: Array<{ publicPort: number; privatePort: number; type: string; display: string }>; - networks: Array<{ name: string; ipAddress: string }>; - volumeCount: number; - restartCount: number; - created: number; -} - -/** - * Compose stack information - */ -export interface ComposeStackInfo { - name: string; - containers: string[]; - containerDetails: ContainerDetail[]; - status: 'running' | 'stopped' | 'partial' | 'created'; - sourceType?: StackSourceType; - hasComposeFile?: boolean; -} - -/** - * Stack deployment options - */ -export interface DeployStackOptions { - name: string; - compose: string; - envId?: number | null; - envFileVars?: Record; - forceRecreate?: boolean; -} - -// ============================================================================= -// ERRORS -// ============================================================================= - -/** - * Error for operations on external stacks without compose files - */ -export class ExternalStackError extends Error { - public readonly stackName: string; - - constructor(stackName: string) { - super( - `Stack "${stackName}" was created outside of Dockhand. ` + - `To manage this stack, first import it by clicking the Import button in the stack menu.` - ); - this.name = 'ExternalStackError'; - this.stackName = stackName; - } -} - -/** - * Error when compose file is missing for a managed stack - */ -export class ComposeFileNotFoundError extends Error { - public readonly stackName: string; - - constructor(stackName: string) { - super( - `Compose file not found for stack "${stackName}". ` + - `The stack may have been deleted or was created outside of Dockhand.` - ); - this.name = 'ComposeFileNotFoundError'; - this.stackName = stackName; - } -} - -// ============================================================================= -// INTERNAL STATE -// ============================================================================= - -// Cache stacks directory -let _stacksDir: string | null = null; - -// Per-stack locking mechanism to prevent race conditions during concurrent operations -const stackLocks = new Map>(); - -/** - * Execute a function with exclusive lock on a stack. - * Prevents race conditions when multiple operations target the same stack. - */ -async function withStackLock(stackName: string, fn: () => Promise): Promise { - const lockKey = stackName; - - // Wait for any existing lock to release - while (stackLocks.has(lockKey)) { - await stackLocks.get(lockKey); - } - - // Create new lock - let releaseLock: () => void; - const lockPromise = new Promise((resolve) => { - releaseLock = resolve; - }); - stackLocks.set(lockKey, lockPromise); - - try { - return await fn(); - } finally { - stackLocks.delete(lockKey); - releaseLock!(); - } -} - -// Timeout configuration for compose operations -const COMPOSE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes -const COMPOSE_KILL_GRACE_MS = 5000; // 5 seconds grace period before SIGKILL - -// ============================================================================= -// DEBUG UTILITIES -// ============================================================================= - -/** - * Mask sensitive values in environment variables for safe logging. - * Masks values for keys containing common secret patterns and truncates long values. - */ -function maskSecrets(vars: Record): Record { - const masked: Record = {}; - const secretPatterns = /password|secret|token|key|api_key|apikey|auth|credential|private/i; - for (const [key, value] of Object.entries(vars)) { - if (secretPatterns.test(key)) { - masked[key] = '***'; - } else if (value.length > 50) { - // Truncate long values that might be secrets - masked[key] = value.substring(0, 10) + '...(truncated)'; - } else { - masked[key] = value; - } - } - return masked; -} - -// ============================================================================= -// UTILITIES -// ============================================================================= - -/** - * Get the compose stacks directory (always returns absolute path) - */ -export function getStacksDir(): string { - if (_stacksDir) return _stacksDir; - const dataDir = process.env.DATA_DIR || './data'; - // Resolve to absolute path to avoid issues with relative paths in docker compose - _stacksDir = resolve(join(dataDir, 'stacks')); - if (!existsSync(_stacksDir)) { - mkdirSync(_stacksDir, { recursive: true }); - } - return _stacksDir; -} - -/** - * List stacks that have compose files stored locally - */ -export function listManagedStacks(): string[] { - const stacksDir = getStacksDir(); - if (!existsSync(stacksDir)) { - return []; - } - - return readdirSync(stacksDir, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .filter((dirent) => { - const composeYml = join(stacksDir, dirent.name, 'docker-compose.yml'); - const composeYaml = join(stacksDir, dirent.name, 'docker-compose.yaml'); - return existsSync(composeYml) || existsSync(composeYaml); - }) - .map((dirent) => dirent.name); -} - -// ============================================================================= -// COMPOSE FILE MANAGEMENT -// ============================================================================= - -/** - * Get compose file content for a stack - */ -export async function getStackComposeFile( - stackName: string -): Promise<{ success: boolean; content?: string; error?: string }> { - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); - const composeFile = join(stackDir, 'docker-compose.yml'); - - const ymlFile = Bun.file(composeFile); - if (await ymlFile.exists()) { - return { - success: true, - content: await ymlFile.text() - }; - } - - const yamlFile = Bun.file(join(stackDir, 'docker-compose.yaml')); - if (await yamlFile.exists()) { - return { - success: true, - content: await yamlFile.text() - }; - } - - return { - success: false, - error: `Compose file not found for stack "${stackName}". The stack may have been created outside of Dockhand.` - }; -} - -/** - * Save or create a stack compose file without deploying. - * @param name - Stack name - * @param content - Compose file content - * @param create - If true, creates a new stack (fails if exists). If false, updates existing (fails if not exists). - */ -export async function saveStackComposeFile( - name: string, - content: string, - create = false -): Promise<{ success: boolean; error?: string }> { - // Validate stack name - if (!/^[a-zA-Z0-9_-]+$/.test(name)) { - return { - success: false, - error: 'Stack name can only contain letters, numbers, hyphens, and underscores' - }; - } - - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, name); - const composeFile = join(stackDir, 'docker-compose.yml'); - const exists = existsSync(stackDir); - - if (create) { - // Creating new stack - if directory exists, it's orphaned (clean it up) - if (exists) { - try { - console.log(`Cleaning up orphaned stack directory: ${stackDir}`); - rmSync(stackDir, { recursive: true, force: true }); - } catch (err: any) { - return { success: false, error: `Stack directory exists and cleanup failed: ${err.message}` }; - } - } - try { - mkdirSync(stackDir, { recursive: true }); - } catch (err: any) { - return { success: false, error: `Failed to create stack directory: ${err.message}` }; - } - } else { - // Updating existing stack - must exist - if (!exists) { - return { success: false, error: `Stack "${name}" not found` }; - } - } - - try { - await Bun.write(composeFile, content); - return { success: true }; - } catch (err: any) { - return { success: false, error: `Failed to ${create ? 'create' : 'save'} compose file: ${err.message}` }; - } -} - -// ============================================================================= -// COMPOSE COMMAND EXECUTION -// ============================================================================= - -interface ComposeCommandOptions { - stackName: string; - envId?: number | null; - forceRecreate?: boolean; - removeVolumes?: boolean; -} - -/** - * Execute a docker compose command locally via Bun.spawn - */ -async function executeLocalCompose( - operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull', - stackName: string, - composeContent: string, - dockerHost?: string, - envVars?: Record, - forceRecreate?: boolean, - removeVolumes?: boolean -): Promise { - const logPrefix = `[Stack:${stackName}]`; - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); - mkdirSync(stackDir, { recursive: true }); - - const composeFile = join(stackDir, 'docker-compose.yml'); - await Bun.write(composeFile, composeContent); - - const spawnEnv: Record = { ...(process.env as Record) }; - if (dockerHost) { - spawnEnv.DOCKER_HOST = dockerHost; - } - // Add stack-specific environment variables - if (envVars) { - Object.assign(spawnEnv, envVars); - } - - // Build command based on operation - const args = ['docker', 'compose', '-p', stackName, '-f', composeFile]; - - switch (operation) { - case 'up': - args.push('up', '-d', '--remove-orphans'); - if (forceRecreate) args.push('--force-recreate'); - break; - case 'down': - args.push('down'); - if (removeVolumes) args.push('--volumes'); - break; - case 'stop': - args.push('stop'); - break; - case 'start': - args.push('start'); - break; - case 'restart': - args.push('restart'); - break; - case 'pull': - args.push('pull'); - break; - } - - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} EXECUTE LOCAL COMPOSE`); - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} Operation:`, operation); - console.log(`${logPrefix} Command:`, args.join(' ')); - console.log(`${logPrefix} Working directory:`, stackDir); - console.log(`${logPrefix} Compose file:`, composeFile); - console.log(`${logPrefix} DOCKER_HOST:`, dockerHost || '(local socket)'); - console.log(`${logPrefix} Force recreate:`, forceRecreate ?? false); - console.log(`${logPrefix} Remove volumes:`, removeVolumes ?? false); - console.log(`${logPrefix} Env vars count:`, envVars ? Object.keys(envVars).length : 0); - if (envVars && Object.keys(envVars).length > 0) { - console.log(`${logPrefix} Env vars being injected (masked):`, JSON.stringify(maskSecrets(envVars), null, 2)); - } - - try { - console.log(`${logPrefix} Spawning docker compose process...`); - const proc = Bun.spawn(args, { - cwd: stackDir, - env: spawnEnv, - stdout: 'pipe', - stderr: 'pipe' - }); - - // Set up timeout with SIGTERM -> SIGKILL escalation - let timedOut = false; - const timeoutId = setTimeout(() => { - timedOut = true; - console.log(`${logPrefix} TIMEOUT: Process exceeded ${COMPOSE_TIMEOUT_MS / 1000} seconds, sending SIGTERM`); - proc.kill('SIGTERM'); - // Give process grace period to terminate cleanly before SIGKILL - setTimeout(() => { - try { - proc.kill('SIGKILL'); - console.log(`${logPrefix} TIMEOUT: Sent SIGKILL after grace period`); - } catch { - // Process may already be dead - } - }, COMPOSE_KILL_GRACE_MS); - }, COMPOSE_TIMEOUT_MS); - - try { - const [stdout, stderr] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text() - ]); - - const code = await proc.exited; - - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} COMPOSE PROCESS COMPLETE`); - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} Exit code:`, code); - console.log(`${logPrefix} Timed out:`, timedOut); - if (stdout) { - console.log(`${logPrefix} STDOUT:`); - console.log(stdout); - } - if (stderr) { - console.log(`${logPrefix} STDERR:`); - console.log(stderr); - } - - if (timedOut) { - return { - success: false, - output: stdout, - error: `docker compose ${operation} timed out after ${COMPOSE_TIMEOUT_MS / 1000} seconds` - }; - } - - if (code === 0) { - return { - success: true, - output: stdout || stderr || `Stack "${stackName}" ${operation} completed successfully` - }; - } else { - return { - success: false, - output: stdout, - error: stderr || `docker compose ${operation} exited with code ${code}` - }; - } - } finally { - clearTimeout(timeoutId); - } - } catch (err: any) { - console.log(`${logPrefix} EXCEPTION in executeLocalCompose:`, err.message); - return { - success: false, - output: '', - error: `Failed to run docker compose ${operation}: ${err.message}` - }; - } -} - -/** - * Execute a docker compose command via Hawser agent - */ -async function executeComposeViaHawser( - operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull', - stackName: string, - composeContent: string, - envId: number, - envVars?: Record, - forceRecreate?: boolean, - removeVolumes?: boolean -): Promise { - const logPrefix = `[Stack:${stackName}]`; - // Import dockerFetch dynamically to avoid circular dependency - const { dockerFetch } = await import('./docker.js'); - - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} EXECUTE COMPOSE VIA HAWSER`); - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} Operation:`, operation); - console.log(`${logPrefix} Environment ID:`, envId); - console.log(`${logPrefix} Force recreate:`, forceRecreate ?? false); - console.log(`${logPrefix} Remove volumes:`, removeVolumes ?? false); - console.log(`${logPrefix} Env vars count:`, envVars ? Object.keys(envVars).length : 0); - if (envVars && Object.keys(envVars).length > 0) { - console.log(`${logPrefix} Env vars being sent (masked):`, JSON.stringify(maskSecrets(envVars), null, 2)); - } - console.log(`${logPrefix} Compose content length:`, composeContent.length, 'chars'); - - try { - const body = JSON.stringify({ - operation, - projectName: stackName, - composeFile: composeContent, - envVars: envVars || {}, - forceRecreate: forceRecreate || false, - removeVolumes: removeVolumes || false - }); - - console.log(`${logPrefix} Sending request to Hawser agent...`); - const response = await dockerFetch( - '/_hawser/compose', - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body - }, - envId - ); - - const result = (await response.json()) as { - success: boolean; - output?: string; - error?: string; - }; - - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} HAWSER RESPONSE`); - console.log(`${logPrefix} ----------------------------------------`); - console.log(`${logPrefix} Success:`, result.success); - if (result.output) { - console.log(`${logPrefix} Output:`, result.output); - } - if (result.error) { - console.log(`${logPrefix} Error:`, result.error); - } - - if (result.success) { - return { - success: true, - output: result.output || `Stack "${stackName}" ${operation} completed via Hawser` - }; - } else { - return { - success: false, - output: result.output || '', - error: result.error || `Compose ${operation} failed` - }; - } - } catch (err: any) { - console.log(`${logPrefix} EXCEPTION in executeComposeViaHawser:`, err.message); - return { - success: false, - output: '', - error: `Failed to ${operation} via Hawser: ${err.message}` - }; - } -} - -/** - * Route compose command to appropriate executor based on connection type - */ -async function executeComposeCommand( - operation: 'up' | 'down' | 'stop' | 'start' | 'restart' | 'pull', - options: ComposeCommandOptions, - composeContent: string, - envVars?: Record -): Promise { - const { stackName, envId, forceRecreate, removeVolumes } = options; - - // Get environment configuration - const env = envId ? await getEnvironment(envId) : null; - - if (!env) { - // Local socket connection (no environment specified) - return executeLocalCompose( - operation, - stackName, - composeContent, - undefined, - envVars, - forceRecreate, - removeVolumes - ); - } - - switch (env.connectionType) { - case 'hawser-standard': - case 'hawser-edge': - return executeComposeViaHawser( - operation, - stackName, - composeContent, - envId!, - envVars, - forceRecreate, - removeVolumes - ); - - case 'direct': { - const port = env.port || 2375; - const dockerHost = `tcp://${env.host}:${port}`; - return executeLocalCompose( - operation, - stackName, - composeContent, - dockerHost, - envVars, - forceRecreate, - removeVolumes - ); - } - - case 'socket': - default: - return executeLocalCompose( - operation, - stackName, - composeContent, - undefined, - envVars, - forceRecreate, - removeVolumes - ); - } -} - -// ============================================================================= -// STACK DISCOVERY -// ============================================================================= - -/** - * List all compose stacks from Docker containers - */ -export async function listComposeStacks(envId?: number | null): Promise { - // Import dynamically to avoid circular dependency - const { listContainers } = await import('./docker.js'); - - const containers = await listContainers(true, envId); - const stacks = new Map>(); - - containers.forEach((container) => { - const projectLabel = container.labels['com.docker.compose.project']; - if (projectLabel) { - if (!stacks.has(projectLabel)) { - stacks.set(projectLabel, new Set()); - } - stacks.get(projectLabel)?.add(container.id); - } - }); - - const result: ComposeStackInfo[] = Array.from(stacks.entries()).map(([name, containerIds]) => { - const stackContainers = containers.filter((c) => containerIds.has(c.id)); - const runningCount = stackContainers.filter((c) => c.state === 'running').length; - - const containerDetails: ContainerDetail[] = stackContainers - .map((c) => { - const service = c.labels['com.docker.compose.service'] || c.name; - - // Build ports with structured data for clickable links - const ports = (c.ports || []) - .filter((p) => p.PublicPort) - .map((p) => ({ - publicPort: p.PublicPort!, - privatePort: p.PrivatePort, - type: p.Type, - display: `${p.PublicPort}:${p.PrivatePort}/${p.Type}` - })); - - // Build networks with IP addresses - const networks = Object.entries(c.networks || {}).map(([name, data]) => ({ - name, - ipAddress: data?.ipAddress || '' - })); - - const volumeCount = c.mounts?.length || 0; - - return { - id: c.id, - name: c.name, - service, - state: c.state, - status: c.status, - health: c.health, - image: c.image, - ports, - networks, - volumeCount, - restartCount: c.restartCount || 0, - created: c.created - }; - }) - .sort((a, b) => a.service.localeCompare(b.service)); - - return { - name, - containers: Array.from(containerIds), - containerDetails, - status: - runningCount === stackContainers.length - ? 'running' - : runningCount === 0 - ? 'stopped' - : 'partial' - }; - }); - - return result; -} - -/** - * Get containers for a specific stack by label - */ -async function getStackContainers(stackName: string, envId?: number | null): Promise { - const { listContainers } = await import('./docker.js'); - const containers = await listContainers(true, envId); - return containers.filter((c) => c.labels['com.docker.compose.project'] === stackName); -} - -/** - * Helper to perform container-based operations for external stacks - * Used as fallback when no compose file exists. - * Uses Promise.allSettled for parallel execution. - */ -async function withContainerFallback( - stackName: string, - envId: number | null | undefined, - operation: 'start' | 'stop' | 'restart' | 'remove' -): Promise { - const { startContainer, stopContainer, restartContainer, removeContainer } = await import('./docker.js'); - - const containers = await getStackContainers(stackName, envId); - if (containers.length === 0) { - return { success: false, error: `No containers found for stack "${stackName}"` }; - } - - // Execute all container operations in parallel - // Note: listContainers returns containers with lowercase property names: id, name, labels - const operationResults = await Promise.allSettled( - containers.map(async (container) => { - const containerName = container.name || container.id; - switch (operation) { - case 'start': - await startContainer(container.id, envId); - break; - case 'stop': - await stopContainer(container.id, envId); - break; - case 'restart': - await restartContainer(container.id, envId); - break; - case 'remove': - await removeContainer(container.id, true, envId); - break; - } - return containerName; - }) - ); - - // Collect successes and failures - const successes: string[] = []; - const errors: string[] = []; - - operationResults.forEach((result, index) => { - const containerName = containers[index].name || containers[index].id; - if (result.status === 'fulfilled') { - successes.push(result.value); - } else { - errors.push(`${containerName}: ${result.reason?.message || 'Unknown error'}`); - } - }); - - if (errors.length > 0) { - return { - success: successes.length > 0, - error: errors.join('; '), - output: successes.length > 0 ? `Partial success: ${successes.join(', ')}` : undefined - }; - } - - return { - success: true, - output: `${operation} completed for ${successes.length} container(s): ${successes.join(', ')}` - }; -} - -// ============================================================================= -// STACK LIFECYCLE OPERATIONS -// ============================================================================= - -/** - * Ensure we have a compose file for operations, throw appropriate error if not - */ -async function requireComposeFile( - stackName: string, - envId?: number | null -): Promise<{ content: string; envVars: Record }> { - const composeResult = await getStackComposeFile(stackName); - - if (!composeResult.success) { - // Check if this is an external stack - const source = await getStackSource(stackName, envId); - if (!source || source.sourceType === 'external') { - throw new ExternalStackError(stackName); - } - throw new ComposeFileNotFoundError(stackName); - } - - // Get environment variables from database - const envVars = await getStackEnvVarsAsRecord(stackName, envId); - - return { content: composeResult.content!, envVars }; -} - -/** - * Start a stack using docker compose up - * Falls back to individual container start for external stacks - */ -export async function startStack( - stackName: string, - envId?: number | null -): Promise { - try { - const { content, envVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('up', { stackName, envId }, content, envVars); - } catch (err) { - if (err instanceof ExternalStackError) { - return withContainerFallback(stackName, envId, 'start'); - } - throw err; - } -} - -/** - * Stop a stack using docker compose stop - * Falls back to individual container stop for external stacks - */ -export async function stopStack( - stackName: string, - envId?: number | null -): Promise { - try { - const { content, envVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('stop', { stackName, envId }, content, envVars); - } catch (err) { - if (err instanceof ExternalStackError) { - return withContainerFallback(stackName, envId, 'stop'); - } - throw err; - } -} - -/** - * Restart a stack using docker compose restart - * Falls back to individual container restart for external stacks - */ -export async function restartStack( - stackName: string, - envId?: number | null -): Promise { - try { - const { content, envVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('restart', { stackName, envId }, content, envVars); - } catch (err) { - if (err instanceof ExternalStackError) { - return withContainerFallback(stackName, envId, 'restart'); - } - throw err; - } -} - -/** - * Down a stack using docker compose down (removes containers, keeps files) - * For external stacks, this is equivalent to stop (no compose file to "down") - */ -export async function downStack( - stackName: string, - envId?: number | null, - removeVolumes = false -): Promise { - try { - const { content, envVars } = await requireComposeFile(stackName, envId); - return executeComposeCommand('down', { stackName, envId, removeVolumes }, content, envVars); - } catch (err) { - if (err instanceof ExternalStackError) { - // For external stacks, down is the same as stop (no compose file to tear down) - return withContainerFallback(stackName, envId, 'stop'); - } - throw err; - } -} - -/** - * Remove a stack completely (compose down + delete files + cleanup database) - * Uses stack locking to prevent concurrent operations. - */ -export async function removeStack( - stackName: string, - envId?: number | null, - force = false -): Promise { - return withStackLock(stackName, async () => { - // Get compose file (may not exist for external stacks) - const composeResult = await getStackComposeFile(stackName); - - // If compose file exists, run docker compose down first - if (composeResult.success) { - const envVars = await getStackEnvVarsAsRecord(stackName, envId); - const downResult = await executeComposeCommand( - 'down', - { stackName, envId }, - composeResult.content!, - envVars - ); - if (!downResult.success && !force) { - return downResult; - } - } else { - // External stack - remove containers directly in parallel - const { removeContainer } = await import('./docker.js'); - const stackContainers = await getStackContainers(stackName, envId); - - const removalResults = await Promise.allSettled( - stackContainers.map((container) => - removeContainer(container.id, force, envId).then(() => container.name) - ) - ); - - const errors: string[] = []; - removalResults.forEach((result, index) => { - if (result.status === 'rejected') { - const containerName = stackContainers[index].name || stackContainers[index].id; - errors.push(`Failed to remove ${containerName}: ${result.reason?.message || 'Unknown error'}`); - } - }); - - if (errors.length > 0 && !force) { - return { - success: false, - error: errors.join('; ') - }; - } - } - - // Clean up database records - collect errors but don't stop - const cleanupErrors: string[] = []; - - // Delete compose file and directory - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, stackName); - if (existsSync(stackDir)) { - try { - rmSync(stackDir, { recursive: true, force: true }); - } catch (err: any) { - console.error(`Failed to delete stack directory: ${err.message}`); - cleanupErrors.push(`directory: ${err.message}`); - } - // Verify deletion succeeded (rmSync with force:true may not throw on some failures) - if (existsSync(stackDir)) { - const verifyErr = 'Directory still exists after deletion attempt'; - console.error(`Failed to delete stack directory: ${verifyErr}`); - cleanupErrors.push(`directory: ${verifyErr}`); - } - } - - try { - await deleteStackSource(stackName, envId); - } catch (err: any) { - cleanupErrors.push(`stack source: ${err.message}`); - } - - try { - await deleteStackEnvVars(stackName, envId); - } catch (err: any) { - cleanupErrors.push(`env vars: ${err.message}`); - } - - // If git stack, clean up git stack record - try { - const gitStack = await getGitStackByName(stackName, envId); - if (gitStack) { - await deleteGitStack(gitStack.id); - deleteGitStackFiles(gitStack.id); - } - // Also cleanup any orphaned git stacks with NULL environment_id for this stack name - if (envId !== undefined && envId !== null) { - const orphanedGitStack = await getGitStackByName(stackName, null); - if (orphanedGitStack) { - await deleteGitStack(orphanedGitStack.id); - deleteGitStackFiles(orphanedGitStack.id); - } - } - } catch (err: any) { - cleanupErrors.push(`git stack: ${err.message}`); - } - - // Check if directory deletion failed - this blocks stack recreation - const directoryError = cleanupErrors.find(e => e.startsWith('directory:')); - if (directoryError) { - return { - success: false, - error: `Stack containers stopped but directory cleanup failed (${directoryError}). Cannot recreate stack with same name until directory is manually removed.` - }; - } - - // Return success with optional cleanup warnings for non-critical errors - const output = cleanupErrors.length > 0 - ? `Stack "${stackName}" removed with cleanup warnings: ${cleanupErrors.join('; ')}` - : `Stack "${stackName}" removed successfully`; - - return { success: true, output }; - }); -} - -/** - * Deploy a stack (create or update) - * Uses stack locking to prevent concurrent deployments. - */ -export async function deployStack(options: DeployStackOptions): Promise { - const { name, compose, envId, envFileVars, forceRecreate } = options; - const logPrefix = `[Stack:${name}]`; - - console.log(`${logPrefix} ========================================`); - console.log(`${logPrefix} DEPLOY STACK START`); - console.log(`${logPrefix} ========================================`); - console.log(`${logPrefix} Environment ID:`, envId ?? '(none - local)'); - console.log(`${logPrefix} Force recreate:`, forceRecreate ?? false); - console.log(`${logPrefix} Env file vars provided:`, envFileVars ? Object.keys(envFileVars).length : 0); - if (envFileVars && Object.keys(envFileVars).length > 0) { - console.log(`${logPrefix} Env file var keys:`, Object.keys(envFileVars).join(', ')); - console.log(`${logPrefix} Env file vars (masked):`, JSON.stringify(maskSecrets(envFileVars), null, 2)); - } - - // Validate stack name - if (!/^[a-zA-Z0-9_-]+$/.test(name)) { - console.log(`${logPrefix} ERROR: Invalid stack name format`); - return { - success: false, - output: '', - error: 'Stack name can only contain letters, numbers, hyphens, and underscores' - }; - } - - return withStackLock(name, async () => { - // Ensure stack directory exists and write compose file (for local reference) - const stacksDir = getStacksDir(); - const stackDir = join(stacksDir, name); - mkdirSync(stackDir, { recursive: true }); - - const composeFile = join(stackDir, 'docker-compose.yml'); - await Bun.write(composeFile, compose); - console.log(`${logPrefix} Compose file written to:`, composeFile); - console.log(`${logPrefix} Compose content length:`, compose.length, 'chars'); - console.log(`${logPrefix} Compose content (full):`); - console.log(compose); - - // Fetch stack environment variables from database (these are user overrides) - const dbEnvVars = await getStackEnvVarsAsRecord(name, envId); - console.log(`${logPrefix} DB env vars count:`, Object.keys(dbEnvVars).length); - if (Object.keys(dbEnvVars).length > 0) { - console.log(`${logPrefix} DB env var keys:`, Object.keys(dbEnvVars).join(', ')); - console.log(`${logPrefix} DB env vars (masked):`, JSON.stringify(maskSecrets(dbEnvVars), null, 2)); - } - - // Merge: env file vars as base, database overrides take precedence - const envVars = { ...envFileVars, ...dbEnvVars }; - console.log(`${logPrefix} Merged env vars count:`, Object.keys(envVars).length); - if (Object.keys(envVars).length > 0) { - console.log(`${logPrefix} Merged env var keys:`, Object.keys(envVars).join(', ')); - console.log(`${logPrefix} Merged env vars (masked):`, JSON.stringify(maskSecrets(envVars), null, 2)); - } - - console.log(`${logPrefix} Calling executeComposeCommand...`); - const result = await executeComposeCommand('up', { stackName: name, envId, forceRecreate }, compose, envVars); - console.log(`${logPrefix} ========================================`); - console.log(`${logPrefix} DEPLOY STACK RESULT`); - console.log(`${logPrefix} ========================================`); - console.log(`${logPrefix} Success:`, result.success); - if (result.output) { - console.log(`${logPrefix} Output:`, result.output); - } - if (result.error) { - console.log(`${logPrefix} Error:`, result.error); - } - return result; - }); -} - -/** - * Pull images for a stack - */ -export async function pullStackImages( - stackName: string, - envId?: number | null -): Promise<{ success: boolean; output?: string; error?: string }> { - const { content, envVars } = await requireComposeFile(stackName, envId); - - return executeComposeCommand('pull', { stackName, envId }, content, envVars); -} - -// ============================================================================= -// RE-EXPORTS FOR BACKWARDS COMPATIBILITY -// ============================================================================= - -// These exports maintain API compatibility with code that imports from docker.ts -// They can be removed once all imports are updated - -export type { StackOperationResult as CreateStackResult }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..eea5398 --- /dev/null +++ b/package.json @@ -0,0 +1,118 @@ +{ + "name": "dockhand", + "private": true, + "version": "1.0.7", + "type": "module", + "scripts": { + "dev": "bunx --bun vite dev", + "prebuild": "bunx license-checker --json --production | jq 'to_entries | map({name: (.key | split(\"@\")[0:-1] | join(\"@\")), version: (.key | split(\"@\")[-1]), license: .value.licenses, repository: .value.repository}) | sort_by(.name)' > src/lib/data/dependencies.json.tmp && mv src/lib/data/dependencies.json.tmp src/lib/data/dependencies.json || true", + "build": "bunx --bun vite build && bun scripts/patch-build.ts && bun scripts/build-subprocesses.ts", + "start": "bun ./build/index.js", + "preview": "bun ./build/index.js", + "prepare": "bunx --bun svelte-kit sync || echo ''", + "check": "bunx --bun svelte-kit sync && bunx --bun svelte-check --tsconfig ./tsconfig.json", + "check:watch": "bunx --bun svelte-kit sync && bunx --bun svelte-check --tsconfig ./tsconfig.json --watch", + "test": "bun test", + "test:smoke": "bun test tests/api-smoke.test.ts", + "test:containers": "bun test tests/container-lifecycle.test.ts", + "test:notifications": "bun test tests/notifications.test.ts", + "test:hawser": "bun test tests/hawser-connection.test.ts", + "test:build": "SKIP_BUILD_TEST=1 bun test tests/build.test.ts", + "test:postgres": "bun test tests/database-postgres.test.ts", + "test:crud": "bun test tests/crud-operations.test.ts", + "test:scheduling": "bun test tests/scheduling.test.ts", + "test:images": "bun test tests/images.test.ts", + "test:volumes": "bun test tests/volumes-networks.test.ts", + "test:stacks": "bun test tests/stacks.test.ts", + "test:stacks:matrix": "bun test tests/stack-matrix.test.ts", + "test:stacks:git": "bun test tests/stack-git-flow.test.ts", + "test:stacks:env": "bun test tests/stack-env-vars.test.ts", + "test:stacks:all": "bun test tests/stack-*.test.ts tests/stacks.test.ts", + "test:files": "bun test tests/container-files.test.ts", + "test:license": "bun test tests/license.test.ts", + "test:activity": "bun test tests/activity-dashboard.test.ts", + "test:all": "bun test tests/", + "test:quick": "bun test tests/api-smoke.test.ts tests/notifications.test.ts", + "test:integration": "bun test tests/api-smoke.test.ts tests/crud-operations.test.ts tests/scheduling.test.ts tests/hawser-connection.test.ts", + "test:e2e": "bunx playwright test tests/e2e/", + "generate:legal": "bun scripts/generate-legal-pages.ts" + }, + "dependencies": { + "@codemirror/autocomplete": "6.20.0", + "@codemirror/commands": "6.10.1", + "@codemirror/lang-css": "6.3.1", + "@codemirror/lang-html": "6.4.11", + "@codemirror/lang-javascript": "6.2.4", + "@codemirror/lang-json": "6.0.2", + "@codemirror/lang-markdown": "6.5.0", + "@codemirror/lang-python": "6.2.1", + "@codemirror/lang-sql": "6.10.0", + "@codemirror/lang-xml": "6.1.0", + "@codemirror/lang-yaml": "6.1.2", + "@codemirror/language": "6.12.1", + "@codemirror/search": "6.5.11", + "@codemirror/state": "6.5.3", + "@codemirror/theme-one-dark": "6.1.3", + "@codemirror/view": "6.39.9", + "@lezer/highlight": "1.2.3", + "@lucide/lab": "^0.1.2", + "codemirror": "6.0.2", + "croner": "9.1.0", + "cronstrue": "3.9.0", + "drizzle-orm": "0.45.1", + "hash-wasm": "4.12.0", + "js-yaml": "^4.1.1", + "ldapts": "^8.1.3", + "nodemailer": "^7.0.12", + "otpauth": "^9.4.1", + "postgres": "3.4.8", + "qrcode": "^1.5.4", + "svelte-dnd-action": "0.9.69", + "svelte-sonner": "1.0.7" + }, + "devDependencies": { + "@internationalized/date": "^3.10.1", + "@layerstack/tailwind": "^1.0.1", + "@lucide/svelte": "^0.562.0", + "@playwright/test": "1.57.0", + "@sveltejs/kit": "^2.49.3", + "@sveltejs/vite-plugin-svelte": "^6.2.3", + "@tailwindcss/vite": "^4.1.18", + "@types/bun": "^1.3.5", + "@types/js-yaml": "^4.0.9", + "@types/nodemailer": "^7.0.4", + "@types/qrcode": "^1.5.6", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", + "autoprefixer": "^10.4.23", + "bits-ui": "^2.15.4", + "clsx": "^2.1.1", + "cytoscape": "^3.33.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "drizzle-kit": "0.31.8", + "layerchart": "^1.0.13", + "lucide-svelte": "^0.562.0", + "mode-watcher": "^1.1.0", + "postcss": "^8.5.6", + "svelte": "^5.46.1", + "svelte-adapter-bun": "1.0.1", + "svelte-check": "^4.3.5", + "svelte-easy-crop": "^5.0.0", + "svelte-virtual-scroll-list": "^1.3.0", + "tailwind-merge": "^3.4.0", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", + "typescript": "^5.9.3", + "vite": "^7.3.1" + }, + "overrides": { + "@codemirror/state": "6.5.3", + "@codemirror/view": "6.39.9", + "@codemirror/language": "6.12.1", + "@lezer/common": "1.5.0", + "@lezer/highlight": "1.2.3" + } +} diff --git a/routes/api/stacks/[name]/env/+server.ts b/routes/api/stacks/[name]/env/+server.ts deleted file mode 100644 index 23d7a4a..0000000 --- a/routes/api/stacks/[name]/env/+server.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { getStackEnvVars, setStackEnvVars } from '$lib/server/db'; -import { authorize } from '$lib/server/authorize'; -import type { RequestHandler } from './$types'; - -/** - * GET /api/stacks/[name]/env?env=X - * Get all environment variables for a stack. - * Secrets are masked with '***' in the response. - */ -export const GET: RequestHandler = async ({ params, url, cookies }) => { - const auth = await authorize(cookies); - const envId = url.searchParams.get('env'); - const envIdNum = envId ? parseInt(envId) : null; - - // Permission check with environment context - if (auth.authEnabled && !await auth.can('stacks', 'view', envIdNum ?? undefined)) { - return json({ error: 'Permission denied' }, { status: 403 }); - } - - // Environment access check (enterprise only) - if (envIdNum && auth.isEnterprise && !await auth.canAccessEnvironment(envIdNum)) { - return json({ error: 'Access denied to this environment' }, { status: 403 }); - } - - try { - const stackName = decodeURIComponent(params.name); - const variables = await getStackEnvVars(stackName, envIdNum, true); - - return json({ - variables: variables.map(v => ({ - key: v.key, - value: v.value, - isSecret: v.isSecret - })) - }); - } catch (error) { - console.error('Error getting stack env vars:', error); - return json({ error: 'Failed to get environment variables' }, { status: 500 }); - } -}; - -/** - * PUT /api/stacks/[name]/env?env=X - * Set/replace all environment variables for a stack. - * Body: { variables: [{ key, value, isSecret? }] } - * - * Note: For secrets, if the value is '***' (the masked placeholder), the original - * secret value from the database is preserved instead of overwriting with '***'. - */ -export const PUT: RequestHandler = async ({ params, url, cookies, request }) => { - const auth = await authorize(cookies); - const envId = url.searchParams.get('env'); - const envIdNum = envId ? parseInt(envId) : null; - - // Permission check with environment context - if (auth.authEnabled && !await auth.can('stacks', 'edit', envIdNum ?? undefined)) { - return json({ error: 'Permission denied' }, { status: 403 }); - } - - // Environment access check (enterprise only) - if (envIdNum && auth.isEnterprise && !await auth.canAccessEnvironment(envIdNum)) { - return json({ error: 'Access denied to this environment' }, { status: 403 }); - } - - try { - const stackName = decodeURIComponent(params.name); - const body = await request.json(); - - if (!body.variables || !Array.isArray(body.variables)) { - return json({ error: 'Invalid request body: variables array required' }, { status: 400 }); - } - - // Validate variables - for (const v of body.variables) { - if (!v.key || typeof v.key !== 'string') { - return json({ error: 'Invalid variable: key is required and must be a string' }, { status: 400 }); - } - if (typeof v.value !== 'string') { - return json({ error: `Invalid variable "${v.key}": value must be a string` }, { status: 400 }); - } - // Validate key format (env var naming convention) - if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(v.key)) { - return json({ error: `Invalid variable name "${v.key}": must start with a letter or underscore and contain only alphanumeric characters and underscores` }, { status: 400 }); - } - } - - // Check if any secrets have the masked placeholder '***' - // If so, we need to preserve their original values from the database - const secretsWithMaskedValue = body.variables.filter( - (v: { key: string; value: string; isSecret?: boolean }) => - v.isSecret && v.value === '***' - ); - - let variablesToSave = body.variables; - - if (secretsWithMaskedValue.length > 0) { - // Get existing variables (unmasked) to preserve secret values - const existingVars = await getStackEnvVars(stackName, envIdNum, false); - const existingByKey = new Map(existingVars.map(v => [v.key, v])); - - // Replace masked secrets with their original values - variablesToSave = body.variables.map((v: { key: string; value: string; isSecret?: boolean }) => { - if (v.isSecret && v.value === '***') { - const existing = existingByKey.get(v.key); - if (existing && existing.isSecret) { - // Preserve the original secret value - return { ...v, value: existing.value }; - } - } - return v; - }); - } - - await setStackEnvVars(stackName, envIdNum, variablesToSave); - - return json({ success: true, count: variablesToSave.length }); - } catch (error) { - console.error('Error setting stack env vars:', error); - return json({ error: 'Failed to set environment variables' }, { status: 500 }); - } -}; diff --git a/routes/containers/CreateContainerModal.svelte b/routes/containers/CreateContainerModal.svelte deleted file mode 100644 index 6796704..0000000 --- a/routes/containers/CreateContainerModal.svelte +++ /dev/null @@ -1,1657 +0,0 @@ - - - isOpen && focusFirstInput()}> - - - Create new container - - - - - {#if !skipPullTab} -
- - - {#if envHasScanning} - - - - {/if} - - - -
- {/if} - - - -
- image = newImage} - /> -
- - -
- {#if envHasScanning} - - {:else} - -
-
- -

Vulnerability scanning is disabled for this environment.

-

Enable it in Settings → Environments to scan images.

-
-
- {/if} -
- - -
- -
-
- -
-

Image: {image || 'Not set'}

- {#if isPulling || isScanning} -

- - {isScanning ? 'Scanning...' : 'Pulling...'} -

- {:else if imageReady} -

- - Image pulled and ready - {#if scanResults.length > 0} - • {totalVulnerabilities} vulnerabilities - {/if} -

- {:else if !image && !skipPullTab} -

- - Go to "Pull" tab to set the image -

- {/if} -
-
-
- - - {#if configSets.length > 0} -
-
- -

Config set

-
-
-
- - - {selectedConfigSetId ? configSets.find(c => c.id === parseInt(selectedConfigSetId))?.name : 'Select a config set to pre-fill values...'} - - - {#each configSets as configSet} - -
- {configSet.name} - {#if configSet.description} - {configSet.description} - {/if} -
-
- {/each} -
-
-
-
-
- {/if} - - -
-
-

Basic settings

-
- -
- - errors.name = undefined} - /> - {#if errors.name} -

{errors.name}

- {/if} -
- -
- - -
- -
-
- - - - - {#if restartPolicy === 'no'} - - {:else if restartPolicy === 'always'} - - {:else if restartPolicy === 'on-failure'} - - {:else} - - {/if} - {restartPolicy === 'no' ? 'No' : restartPolicy === 'always' ? 'Always' : restartPolicy === 'on-failure' ? 'On failure' : 'Unless stopped'} - - - - - {#snippet children()} - - No - {/snippet} - - - {#snippet children()} - - Always - {/snippet} - - - {#snippet children()} - - On failure - {/snippet} - - - {#snippet children()} - - Unless stopped - {/snippet} - - - -
- -
- - - - - {#if networkMode === 'bridge'} - - {:else if networkMode === 'host'} - - {:else} - - {/if} - {networkMode === 'bridge' ? 'Bridge' : networkMode === 'host' ? 'Host' : 'None'} - - - - - {#snippet children()} - - Bridge - {/snippet} - - - {#snippet children()} - - Host - {/snippet} - - - {#snippet children()} - - None - {/snippet} - - - -
-
- -
- - -
-
- - - {#if availableNetworks.length > 0} -
-
-
- -

Networks

-
-
- -
- - - Select network to add... - - - {#each availableNetworks.filter(n => !selectedNetworks.includes(n.name) && !['bridge', 'host', 'none'].includes(n.name)) as network} - - {#snippet children()} -
- {network.name} - {network.driver} -
- {/snippet} -
- {/each} -
-
- - {#if selectedNetworks.length > 0} -
- {#each selectedNetworks as networkName} - {@const network = availableNetworks.find(n => n.name === networkName)} - - {networkName} - {#if network} - {network.driver} - {/if} - - - {/each} -
- {/if} -
-
- {/if} - - -
-
-

Port mappings

- -
- -
- {#each portMappings as mapping, index} -
-
- Host - -
-
- Container - -
- { portMappings[index].protocol = v; }} - /> - -
- {/each} -
-
- - -
-
-

Volume mappings

- -
- -
- {#each volumeMappings as mapping, index} -
-
- Host path - -
-
- Container path - -
- { volumeMappings[index].mode = v; }} - /> - -
- {/each} -
-
- - -
-
-

Environment variables

- -
- -
- {#each envVars as envVar, index} -
-
- Key - -
-
- Value - -
- -
- {/each} -
-
- - -
-
-

Labels

- -
- -
- {#each labels as label, index} -
-
- Key - -
-
- Value - -
- -
- {/each} -
-
- - -
-

Advanced container options (click to expand)

-
- - -
- - {#if showResources} -
-

Configure memory and CPU limits for this container

-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- {/if} -
- - -
- - {#if showSecurity} -
-
-
- - -
-
-
- - -
-
-
- -
- - { addCapability('add', v); }}> - - Select capability to add... - - - {#each commonCapabilities.filter(c => !capAdd.includes(c)) as cap} - - {/each} - - - {#if capAdd.length > 0} -
- {#each capAdd as cap} - - +{cap} - - - {/each} -
- {/if} -
- -
- - { addCapability('drop', v); }}> - - Select capability to drop... - - - {#each commonCapabilities.filter(c => !capDrop.includes(c)) as cap} - - {/each} - - - {#if capDrop.length > 0} -
- {#each capDrop as cap} - - -{cap} - - - {/each} -
- {/if} -
-
- {/if} -
- - -
- - {#if showHealth} -
-
- - -
- {#if healthcheckEnabled} -
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- {/if} -
- {/if} -
- - -
- - {#if showDns} -
-
- -
- { if (e.key === 'Enter') { e.preventDefault(); addDnsServer(); } }} - /> - -
- {#if dnsServers.length > 0} -
- {#each dnsServers as server} - - {server} - - - {/each} -
- {/if} -
-
- {/if} -
- - -
- - {#if showDevices} -
-
- -
- {#each deviceMappings as mapping, index} -
- - - -
- {/each} -
- {/if} -
- - -
- - {#if showUlimits} -
-
- -
- {#each ulimits as ulimit, index} -
- - - {ulimit.name} - - - {#each commonUlimits as name} - - {/each} - - - - - -
- {/each} -
- {/if} -
- - -
-
- -

Auto-update

-
- -
-
- -
-
- {#if activeTab === 'container' && hasCriticalOrHigh} -
- - Critical/high vulnerabilities found in image -
- {/if} -
-
- - -
-
-
-
diff --git a/routes/containers/EditContainerModal.svelte b/routes/containers/EditContainerModal.svelte deleted file mode 100644 index f0f41f3..0000000 --- a/routes/containers/EditContainerModal.svelte +++ /dev/null @@ -1,1299 +0,0 @@ - - - isOpen && focusFirstInput()}> - - - - Edit container - {#if isEditingTitle} - - - { - if (e.key === 'Enter') saveEditingTitle(); - if (e.key === 'Escape') cancelEditingTitle(); - }} - /> - - - {:else if name} - - {name} - - {/if} - - - - {#if loadingData} -
- - Loading container data... -
- {:else} -
- - {#if configSets.length > 0} -
-
- -

Apply config set

-
-
-
- - - {selectedConfigSetId ? configSets.find(c => c.id === parseInt(selectedConfigSetId))?.name : 'Select a config set to merge values...'} - - - {#each configSets as configSet} - -
- {configSet.name} - {#if configSet.description} - {configSet.description} - {/if} -
-
- {/each} -
-
-
-
- {#if selectedConfigSetId} - {@const selectedSet = configSets.find(c => c.id === parseInt(selectedConfigSetId))} - {#if selectedSet?.description} -

{selectedSet.description}

- {/if} - {/if} -

Note: Values from the config set will be merged with existing settings. Existing keys won't be overwritten.

-
- {/if} - - - {#if showComposeConfigWarning} -
- -
-

- This container belongs to stack "{composeStackName}" -

-

- Modifying settings will remove this container from the stack. The container will be recreated and lose its stack association. To avoid this, edit the stack's compose file instead. -

-
-
- {:else if showComposeRenameWarning} -
- -
-

- Renaming container from stack "{composeStackName}" -

-

- The container will stay in the stack, but the compose file will be out of sync. Running docker compose up may recreate it with the original name. -

-
-
- {:else if isComposeContainer} -
- -
-

- Stack container: {composeStackName} -

-

- This container is managed by a Docker Compose stack. -

-
-
- {/if} - - -
-
-

Basic settings

-
- -
-
- - errors.name = undefined} - /> - {#if errors.name} -

{errors.name}

- {/if} -
-
- - errors.image = undefined} - /> - {#if errors.image} -

{errors.image}

- {/if} -
-
- -
- - -
- -
-
- - - - - {#if restartPolicy === 'no'} - - {:else if restartPolicy === 'always'} - - {:else if restartPolicy === 'on-failure'} - - {:else} - - {/if} - {restartPolicy === 'no' ? 'No' : restartPolicy === 'always' ? 'Always' : restartPolicy === 'on-failure' ? 'On failure' : 'Unless stopped'} - - - - - {#snippet children()} - - No - {/snippet} - - - {#snippet children()} - - Always - {/snippet} - - - {#snippet children()} - - On failure - {/snippet} - - - {#snippet children()} - - Unless stopped - {/snippet} - - - -
- -
- - - - - {#if networkMode === 'bridge'} - - {:else if networkMode === 'host'} - - {:else} - - {/if} - {networkMode === 'bridge' ? 'Bridge' : networkMode === 'host' ? 'Host' : 'None'} - - - - - {#snippet children()} - - Bridge - {/snippet} - - - {#snippet children()} - - Host - {/snippet} - - - {#snippet children()} - - None - {/snippet} - - - -
-
- -
- - -
-
- - - {#if availableNetworks.length > 0} -
-
-
- -

Networks

-
-
- -
- - - Select network to add... - - - {#each availableNetworks.filter(n => !selectedNetworks.includes(n.name) && !['bridge', 'host', 'none'].includes(n.name)) as network} - - {#snippet children()} -
- {network.name} - {network.driver} -
- {/snippet} -
- {/each} -
-
- - {#if selectedNetworks.length > 0} -
- {#each selectedNetworks as networkName} - {@const network = availableNetworks.find(n => n.name === networkName)} - - {networkName} - {#if network} - {network.driver} - {/if} - - - {/each} -
- {/if} -

Container will be connected to selected networks in addition to the network mode above

-
-
- {/if} - - -
-
-

Port mappings

- -
- -
- {#each portMappings as mapping, index} -
-
- Host - -
-
- Container - -
-
- Protocol - - - {mapping.protocol.toUpperCase()} - - - - - - -
- -
- {/each} -
-
- - -
-
-

Volume mappings

- -
- -
- {#each volumeMappings as mapping, index} -
-
- Host path - -
-
- Container path - -
-
- Mode - - - {mapping.mode.toUpperCase()} - - - - - - -
- -
- {/each} -
-
- - -
-
-

Environment variables

- -
- -
- {#each envVars as envVar, index} -
-
- Key - -
-
- Value - -
- -
- {/each} -
-
- - -
-
-

Labels

- -
- -
- {#each labels as label, index} -
-
- Key - -
-
- Value - -
- -
- {/each} -
-
- - -
-
- -

Auto-update

-
- - -
- - {#if statusMessage} -
- {statusMessage} -
- {/if} - - {#if error} -
- {error} -
- {/if} - -
-
- - -
- {/if} -
-
diff --git a/routes/profile/MfaSetupModal.svelte b/routes/profile/MfaSetupModal.svelte deleted file mode 100644 index c41370d..0000000 --- a/routes/profile/MfaSetupModal.svelte +++ /dev/null @@ -1,117 +0,0 @@ - - - { if (o) { resetForm(); focusFirstInput(); } else onClose(); }}> - - - - - Setup two-factor authentication - - -
- {#if error} - - - {error} - - {/if} - -

- Scan this QR code with your authenticator app (Google Authenticator, Authy, etc.) -

- - {#if qrCode} -
- MFA QR Code -
- {/if} - -
- - {secret} -
- -
- - -

- Enter the code from your authenticator app to verify setup -

-
-
- - - - -
-
diff --git a/routes/stacks/StackModal.svelte b/routes/stacks/StackModal.svelte deleted file mode 100644 index 8ed7b7c..0000000 --- a/routes/stacks/StackModal.svelte +++ /dev/null @@ -1,740 +0,0 @@ - - - { - if (isOpen) { - focusFirstInput(); - } else { - // Prevent closing if there are unsaved changes - show confirmation instead - if (hasChanges) { - // Re-open the dialog and show confirmation - open = true; - showConfirmClose = true; - } - // If no changes, let it close naturally - } - }} -> - - -
-
-
-
- -
-
- - {#if mode === 'create'} - Create compose stack - {:else} - {stackName} - {/if} - - - {#if mode === 'create'} - Create a new Docker Compose stack - {:else} - Edit compose file and view stack structure - {/if} - -
-
- - -
- - -
-
- -
- - {#if activeTab === 'editor'} - - {/if} - - - -
-
-
- -
- {#if error} - - - {error} - - {/if} - - {#if errors.compose} - - - {errors.compose} - - {/if} - - {#if mode === 'edit' && loading} -
-
- - Loading compose file... -
-
- {:else if mode === 'edit' && loadError} -
-
-
- -
-

Could not load compose file

-

{loadError}

-

- This stack may have been created outside of Dockhand or the compose file may have been moved. -

-
-
- {:else} - - {#if mode === 'create'} -
-
- - errors.stackName = undefined} - /> - {#if errors.stackName} -

{errors.stackName}

- {/if} -
-
- {/if} - - -
- {#if activeTab === 'editor'} - -
- {#if open} -
- -
- {/if} -
- -
-
- - Environment variables -
-
- validateEnvVars()} - /> -
-
- {:else if activeTab === 'graph'} - - - {/if} -
- {/if} -
- - -
-
- {#if hasChanges} - Unsaved changes - {:else} - No changes - {/if} -
- -
- - - {#if mode === 'create'} - - - - {:else} - - - - {/if} -
-
-
-
- - - - - - Unsaved changes - - You have unsaved changes. Are you sure you want to close without saving? - - -
- - -
-
-
diff --git a/routes/terminal/+page.svelte b/routes/terminal/+page.svelte deleted file mode 100644 index 4dd3fc2..0000000 --- a/routes/terminal/+page.svelte +++ /dev/null @@ -1,349 +0,0 @@ - - -{#if $environments.length === 0 || !$currentEnvironment} -
- - -
-{:else} -
- -
- -
- -
- - - -
- - - {#if dropdownOpen} -
- {#if filteredContainers().length === 0} -
- {containers.length === 0 ? 'No running containers' : 'No matches found'} -
- {:else} - {#each filteredContainers() as container} - - {/each} - {/if} -
- {/if} -
- - {#if selectedContainer} - - {/if} - - {#if !selectedContainer} -
- - - - - {shellOptions.find(o => o.value === selectedShell)?.label || 'Select'} - - - {#each shellOptions as option} - - - {option.label} - - {/each} - - -
-
- - - - - {userOptions.find(o => o.value === selectedUser)?.label || 'Select'} - - - {#each userOptions as option} - - - {option.label} - - {/each} - - -
- {/if} -
- - -
- {#if !selectedContainer} -
-
- -

Select a container to open shell

-
-
- {:else} - -
-
- {#if connected} - - - Connected - - {:else} - Disconnected - {/if} -
-
- changeFontSize(Number(v))}> - - {terminalFontSize}px - - - {#each fontSizeOptions as size} - {size}px - {/each} - - - - - -
-
-
- {#key selectedContainer.id} - - {/key} -
- {/if} -
-
-{/if} - - diff --git a/scripts/build-subprocesses.ts b/scripts/build-subprocesses.ts new file mode 100644 index 0000000..35958e5 --- /dev/null +++ b/scripts/build-subprocesses.ts @@ -0,0 +1,31 @@ +/** + * Build subprocess scripts as standalone bundles for production. + * + * Subprocesses run via Bun.spawn and need all dependencies bundled + * since they can't access the SvelteKit build output's chunked modules. + */ + +const subprocesses = ['metrics-subprocess', 'event-subprocess']; + +console.log('[build-subprocesses] Bundling subprocess scripts...'); + +for (const name of subprocesses) { + const result = await Bun.build({ + entrypoints: [`./src/lib/server/subprocesses/${name}.ts`], + outdir: './build/subprocesses', + target: 'bun', + minify: false + }); + + if (!result.success) { + console.error(`[build-subprocesses] Failed to bundle ${name}:`); + for (const log of result.logs) { + console.error(log); + } + process.exit(1); + } + + console.log(`[build-subprocesses] Bundled ${name}.js`); +} + +console.log('[build-subprocesses] Done'); diff --git a/scripts/emergency/backup-db.sh b/scripts/emergency/backup-db.sh new file mode 100755 index 0000000..bde5e45 --- /dev/null +++ b/scripts/emergency/backup-db.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Emergency script to backup the database +# Automatically detects database type (SQLite or PostgreSQL) +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/backup-db.sh [output_dir] +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/backup-db.sh /app/data/backups +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/backup-db.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/backup-db.sh" "$@" +fi diff --git a/scripts/emergency/clear-sessions.sh b/scripts/emergency/clear-sessions.sh new file mode 100755 index 0000000..efe7e21 --- /dev/null +++ b/scripts/emergency/clear-sessions.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# +# Emergency script to clear all user sessions +# Automatically detects database type (SQLite or PostgreSQL) +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/clear-sessions.sh +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/clear-sessions.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/clear-sessions.sh" "$@" +fi diff --git a/scripts/emergency/create-admin.sh b/scripts/emergency/create-admin.sh new file mode 100755 index 0000000..35b94fc --- /dev/null +++ b/scripts/emergency/create-admin.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Emergency script to create an admin user +# Automatically detects database type (SQLite or PostgreSQL) +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/create-admin.sh +# +# Default credentials: admin / admin123 +# CHANGE THE PASSWORD IMMEDIATELY after logging in! +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/create-admin.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/create-admin.sh" "$@" +fi diff --git a/scripts/emergency/disable-auth.sh b/scripts/emergency/disable-auth.sh new file mode 100755 index 0000000..c9d25a2 --- /dev/null +++ b/scripts/emergency/disable-auth.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# +# Emergency script to disable authentication +# Automatically detects database type (SQLite or PostgreSQL) +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/disable-auth.sh +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/disable-auth.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/disable-auth.sh" "$@" +fi diff --git a/scripts/emergency/export-stacks.sh b/scripts/emergency/export-stacks.sh new file mode 100755 index 0000000..04c3095 --- /dev/null +++ b/scripts/emergency/export-stacks.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# +# Emergency script to export all compose stacks +# Exports docker-compose.yml files from the stacks directory +# +# Usage: +# docker exec -it dockhand /app/scripts/export-stacks.sh [output_dir] +# +# Example: +# docker exec -it dockhand /app/scripts/export-stacks.sh /tmp/stacks-backup +# +# Default output: /app/data/stacks-export +# + +set -e + +echo "========================================" +echo " Dockhand - Export Compose Stacks" +echo "========================================" +echo "" + +# Default paths +STACKS_DIR="${DOCKHAND_STACKS:-/home/dockhand/.dockhand/stacks}" +OUTPUT_DIR="${1:-/app/data/stacks-export}" + +# Check if running locally (not in Docker) +if [ ! -d "$STACKS_DIR" ] && [ -d "$HOME/.dockhand/stacks" ]; then + STACKS_DIR="$HOME/.dockhand/stacks" +fi + +if [ ! -d "$STACKS_DIR" ]; then + echo "Error: Stacks directory not found at $STACKS_DIR" + exit 1 +fi + +# Count stacks +STACK_COUNT=$(find "$STACKS_DIR" -maxdepth 1 -type d ! -path "$STACKS_DIR" 2>/dev/null | wc -l | tr -d ' ') + +echo "This script will export all compose stacks." +echo "" +echo "Stacks directory: $STACKS_DIR" +echo "Output directory: $OUTPUT_DIR" +echo "Stacks found: $STACK_COUNT" +echo "" + +if [ "$STACK_COUNT" -eq "0" ]; then + echo "No stacks found to export." + exit 0 +fi + +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +echo "Exporting stacks..." +echo "" + +# Export each stack +find "$STACKS_DIR" -maxdepth 1 -type d ! -path "$STACKS_DIR" | while read stack_dir; do + STACK_NAME=$(basename "$stack_dir") + COMPOSE_FILE="$stack_dir/docker-compose.yml" + + if [ -f "$COMPOSE_FILE" ]; then + mkdir -p "$OUTPUT_DIR/$STACK_NAME" + cp "$COMPOSE_FILE" "$OUTPUT_DIR/$STACK_NAME/" + + # Also copy .env file if exists + if [ -f "$stack_dir/.env" ]; then + cp "$stack_dir/.env" "$OUTPUT_DIR/$STACK_NAME/" + fi + + echo " Exported: $STACK_NAME" + fi +done + +echo "" +echo "Export complete!" +echo "Stacks exported to: $OUTPUT_DIR" +echo "" +echo "To copy from Docker container to host:" +echo " docker cp dockhand:$OUTPUT_DIR ./stacks-backup" diff --git a/scripts/emergency/list-users.sh b/scripts/emergency/list-users.sh new file mode 100755 index 0000000..b68e901 --- /dev/null +++ b/scripts/emergency/list-users.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# +# Emergency script to list all users +# Automatically detects database type (SQLite or PostgreSQL) +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/list-users.sh +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/list-users.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/list-users.sh" "$@" +fi diff --git a/scripts/emergency/postgres/backup-db.sh b/scripts/emergency/postgres/backup-db.sh new file mode 100755 index 0000000..ccc490f --- /dev/null +++ b/scripts/emergency/postgres/backup-db.sh @@ -0,0 +1,101 @@ +#!/bin/sh +# +# PostgreSQL: Emergency script to backup the database +# Creates a timestamped dump of the database +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/postgres/backup-db.sh [output_dir] +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/postgres/backup-db.sh /app/data/backups +# +# Default output: /app/data +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - Backup Database (PostgreSQL)" +echo "========================================" +echo "" + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +OUTPUT_DIR="${1:-/app/data}" + +# Parse DATABASE_URL +# Format: postgres://user:password@host:port/database +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +# Extract credentials +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +# Generate backup filename with timestamp +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$OUTPUT_DIR/dockhand_backup_$TIMESTAMP.sql" + +echo "This script will create a backup of the database." +echo "" +echo "Host: $DB_HOST:$DB_PORT" +echo "Database: $DB_NAME" +echo "Backup: $BACKUP_FILE" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" + +# Create output directory if needed +mkdir -p "$OUTPUT_DIR" + +echo "Creating database backup..." + +# Use pg_dump to create backup +export PGPASSWORD="$DB_PASS" +if command -v pg_dump >/dev/null 2>&1; then + pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -F p -f "$BACKUP_FILE" +else + echo "Error: pg_dump not found" + echo "Install PostgreSQL client tools to use this script" + exit 1 +fi + +if [ $? -eq 0 ] && [ -f "$BACKUP_FILE" ]; then + SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}') + echo "" + echo "Backup created successfully!" + echo "Size: $SIZE" + echo "" + echo "To copy from Docker container to host:" + echo " docker cp dockhand:$BACKUP_FILE ./dockhand_backup_$TIMESTAMP.sql" +else + echo "Error: Failed to create backup" + exit 1 +fi diff --git a/scripts/emergency/postgres/clear-sessions.sh b/scripts/emergency/postgres/clear-sessions.sh new file mode 100755 index 0000000..3621bc4 --- /dev/null +++ b/scripts/emergency/postgres/clear-sessions.sh @@ -0,0 +1,75 @@ +#!/bin/sh +# +# PostgreSQL: Emergency script to clear all user sessions +# Use this to force all users to re-login +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/postgres/clear-sessions.sh +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - Clear All Sessions (PostgreSQL)" +echo "========================================" +echo "" +echo "This script will clear all user sessions," +echo "forcing all users to log in again." +echo "" + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +# Parse DATABASE_URL +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +export PGPASSWORD="$DB_PASS" + +COUNT=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM sessions;" 2>/dev/null | tr -d ' ') + +echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" +echo "Active sessions: $COUNT" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" +echo "Clearing all user sessions..." +psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "DELETE FROM sessions;" + +if [ $? -eq 0 ]; then + echo "" + echo "Cleared $COUNT session(s) successfully." + echo "All users will need to log in again." +else + echo "Error: Failed to clear sessions" + exit 1 +fi diff --git a/scripts/emergency/postgres/create-admin.sh b/scripts/emergency/postgres/create-admin.sh new file mode 100755 index 0000000..528a534 --- /dev/null +++ b/scripts/emergency/postgres/create-admin.sh @@ -0,0 +1,117 @@ +#!/bin/sh +# +# PostgreSQL: Emergency script to create an admin user +# Use this if you're locked out of Dockhand and need to create a new admin +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/postgres/create-admin.sh +# +# Default credentials: admin / admin123 +# CHANGE THE PASSWORD IMMEDIATELY after logging in! +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - Create Admin User (PostgreSQL)" +echo "========================================" +echo "" +echo "This script will create an admin user with:" +echo " Username: admin" +echo " Password: admin123" +echo "" +echo "If user 'admin' already exists, password will" +echo "be reset and admin privileges restored." +echo "" + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +# Parse DATABASE_URL +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +export PGPASSWORD="$DB_PASS" + +echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +# Username and password +USERNAME="admin" +# Password: admin123 +# This is an argon2id hash of "admin123" - generated with default argon2 settings +PASSWORD_HASH='$argon2id$v=19$m=65536,t=3,p=4$Jq4am2SfyYKmc0PAHe+yzg$cq/27vK/Qg2eZb/jMDy0ExLDhOG+58cKAximxpG5Dss' + +echo "" +echo "Creating admin user..." + +# Check if admin user already exists +EXISTING=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ') + +if [ "$EXISTING" -gt "0" ]; then + echo "User '$USERNAME' already exists." + echo "Resetting password and ensuring active status..." + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "UPDATE users SET password_hash='$PASSWORD_HASH', is_active=true WHERE username='$USERNAME';" + USER_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ') +else + echo "Creating new admin user..." + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "INSERT INTO users (username, password_hash, is_active, auth_provider, created_at, updated_at) VALUES ('$USERNAME', '$PASSWORD_HASH', true, 'local', NOW(), NOW());" + USER_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ') + echo "Admin user created successfully." +fi + +# Get the Admin role ID (it's a system role) +ADMIN_ROLE_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM roles WHERE name='Admin';" 2>/dev/null | tr -d ' ') + +if [ -z "$ADMIN_ROLE_ID" ]; then + echo "Warning: Admin role not found in database." + echo "The user was created but may not have admin privileges." + echo "Please check Settings > Auth > Roles after logging in." +else + # Check if user already has Admin role + HAS_ROLE=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM user_roles WHERE user_id=$USER_ID AND role_id=$ADMIN_ROLE_ID;" 2>/dev/null | tr -d ' ') + + if [ "$HAS_ROLE" -eq "0" ]; then + echo "Assigning Admin role..." + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "INSERT INTO user_roles (user_id, role_id, created_at) VALUES ($USER_ID, $ADMIN_ROLE_ID, NOW());" + echo "Admin role assigned." + else + echo "User already has Admin role." + fi +fi + +echo "" +echo "Credentials:" +echo " Username: admin" +echo " Password: admin123" +echo "" +echo "WARNING: Change the password immediately after logging in!" diff --git a/scripts/emergency/postgres/disable-auth.sh b/scripts/emergency/postgres/disable-auth.sh new file mode 100755 index 0000000..bf3f562 --- /dev/null +++ b/scripts/emergency/postgres/disable-auth.sh @@ -0,0 +1,74 @@ +#!/bin/sh +# +# PostgreSQL: Emergency script to disable authentication +# Use this if you're locked out of Dockhand +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/postgres/disable-auth.sh +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - Disable Authentication (PostgreSQL)" +echo "========================================" +echo "" +echo "This script will disable authentication," +echo "allowing access to Dockhand without login." +echo "" + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +# Parse DATABASE_URL +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +export PGPASSWORD="$DB_PASS" + +echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" +echo "Disabling authentication..." +psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "UPDATE auth_settings SET auth_enabled = false WHERE id = 1;" + +if [ $? -eq 0 ]; then + echo "" + echo "Authentication disabled successfully." + echo "You can now access Dockhand without logging in." + echo "" + echo "Remember to re-enable authentication in Settings after regaining access." +else + echo "Error: Failed to disable authentication" + exit 1 +fi diff --git a/scripts/emergency/postgres/list-users.sh b/scripts/emergency/postgres/list-users.sh new file mode 100755 index 0000000..9ec1d1f --- /dev/null +++ b/scripts/emergency/postgres/list-users.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# +# PostgreSQL: Emergency script to list all users +# Shows username, admin status, active status, and last login +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/postgres/list-users.sh +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - List Users (PostgreSQL)" +echo "========================================" +echo "" + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +# Parse DATABASE_URL +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +export PGPASSWORD="$DB_PASS" + +# Get user count +USER_COUNT=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM users;" 2>/dev/null | tr -d ' ') + +if [ "$USER_COUNT" -eq "0" ]; then + echo "No users found." + exit 0 +fi + +# Get Admin role ID for checking admin status +ADMIN_ROLE_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM roles WHERE name='Admin';" 2>/dev/null | tr -d ' ') + +# Print header +printf "%-4s %-20s %-8s %-8s %-6s %s\n" "ID" "Username" "Admin" "Active" "MFA" "Last Login" +printf "%-4s %-20s %-8s %-8s %-6s %s\n" "----" "--------------------" "--------" "--------" "------" "-------------------" + +# List users (check admin status via user_roles table) +psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -A -F '|' -c "SELECT id, username, is_active, mfa_enabled, COALESCE(last_login::text, 'Never') FROM users ORDER BY id;" 2>/dev/null | while IFS='|' read id username is_active mfa_enabled last_login; do + # Check if user has Admin role + if [ -n "$ADMIN_ROLE_ID" ]; then + HAS_ADMIN=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM user_roles WHERE user_id=$id AND role_id=$ADMIN_ROLE_ID;" 2>/dev/null | tr -d ' ') + if [ "$HAS_ADMIN" -gt "0" ]; then + admin_str="Yes" + else + admin_str="No" + fi + else + admin_str="N/A" + fi + + # Convert boolean values (PostgreSQL returns t/f) + if [ "$is_active" = "t" ]; then + active_str="Yes" + else + active_str="No" + fi + + if [ "$mfa_enabled" = "t" ]; then + mfa_str="Yes" + else + mfa_str="No" + fi + + printf "%-4s %-20s %-8s %-8s %-6s %s\n" "$id" "$username" "$admin_str" "$active_str" "$mfa_str" "$last_login" +done + +echo "" +echo "Total: $USER_COUNT user(s)" + +# Show session count +SESSION_COUNT=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM sessions;" 2>/dev/null | tr -d ' ') +echo "Active sessions: $SESSION_COUNT" diff --git a/scripts/emergency/postgres/reset-db.sh b/scripts/emergency/postgres/reset-db.sh new file mode 100755 index 0000000..7fd8d98 --- /dev/null +++ b/scripts/emergency/postgres/reset-db.sh @@ -0,0 +1,118 @@ +#!/bin/sh +# +# PostgreSQL: Emergency script to factory reset the database +# WARNING: This will DELETE ALL DATA including users, settings, and activity logs! +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/postgres/reset-db.sh +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - Factory Reset Database (PostgreSQL)" +echo "========================================" +echo "" +echo "WARNING: This will DELETE ALL DATA!" +echo "" +echo "This includes:" +echo " - All users and their settings" +echo " - All sessions" +echo " - Authentication settings" +echo " - Activity logs" +echo " - Environment configurations" +echo " - OIDC/SSO settings" +echo "" +echo "The database tables will be truncated." +echo "" + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +# Parse DATABASE_URL +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +export PGPASSWORD="$DB_PASS" + +echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" +echo "Creating backup before reset..." +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="/app/data/dockhand_backup_pre_reset_$TIMESTAMP.sql" +if command -v pg_dump >/dev/null 2>&1; then + pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -F p -f "$BACKUP_FILE" 2>/dev/null || true + if [ -f "$BACKUP_FILE" ]; then + echo "Backup saved to: $BACKUP_FILE" + fi +fi + +echo "" +echo "Truncating all tables..." + +# Truncate all tables in the correct order (respecting foreign keys) +psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" < +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/postgres/reset-password.sh admin MyNewPassword123 +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - Reset User Password (PostgreSQL)" +echo "========================================" +echo "" + +# Check arguments +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Usage: $0 " + echo "" + echo "Example:" + echo " $0 admin MyNewPassword123" + exit 1 +fi + +USERNAME="$1" +NEW_PASSWORD="$2" + +# Validate password length +if [ ${#NEW_PASSWORD} -lt 8 ]; then + echo "Error: Password must be at least 8 characters" + exit 1 +fi + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +# Parse DATABASE_URL +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +export PGPASSWORD="$DB_PASS" + +# Check if user exists +EXISTING=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ') + +if [ "$EXISTING" -eq "0" ]; then + echo "Error: User '$USERNAME' not found" + echo "" + echo "Available users:" + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT username FROM users;" 2>/dev/null | while read user; do + user=$(echo "$user" | tr -d ' ') + if [ -n "$user" ]; then + echo " - $user" + fi + done + exit 1 +fi + +echo "This script will reset the password for user '$USERNAME'." +echo "" +echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" +echo "Username: $USERNAME" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +# Generate password hash using node (argon2 is available in the app) +echo "" +echo "Generating password hash..." + +# Check if node and argon2 are available +if command -v node >/dev/null 2>&1; then + # Try to use argon2 from node_modules + PASSWORD_HASH=$(node -e " + try { + const argon2 = require('argon2'); + argon2.hash('$NEW_PASSWORD').then(h => console.log(h)).catch(e => process.exit(1)); + } catch(e) { + process.exit(1); + } + " 2>/dev/null) + + if [ -z "$PASSWORD_HASH" ]; then + echo "Error: Could not generate password hash (argon2 not available)" + echo "This script requires Node.js with argon2 module" + exit 1 + fi +else + echo "Error: Node.js is required to generate password hash" + exit 1 +fi + +echo "Resetting password for user '$USERNAME'..." +psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "UPDATE users SET password_hash='$PASSWORD_HASH', updated_at=NOW() WHERE username='$USERNAME';" + +if [ $? -eq 0 ]; then + echo "" + echo "Password reset successfully for user '$USERNAME'" + echo "" + # Invalidate sessions + USER_ID=$(psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT id FROM users WHERE username='$USERNAME';" 2>/dev/null | tr -d ' ') + psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "DELETE FROM sessions WHERE user_id=$USER_ID;" 2>/dev/null || true + echo "All existing sessions have been invalidated." + echo "The user can now log in with the new password." +else + echo "Error: Failed to reset password" + exit 1 +fi diff --git a/scripts/emergency/postgres/restore-db.sh b/scripts/emergency/postgres/restore-db.sh new file mode 100755 index 0000000..8676a8f --- /dev/null +++ b/scripts/emergency/postgres/restore-db.sh @@ -0,0 +1,117 @@ +#!/bin/sh +# +# PostgreSQL: Emergency script to restore the database from a backup +# WARNING: This will overwrite the current database! +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/postgres/restore-db.sh +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/postgres/restore-db.sh /app/data/dockhand_backup_20240115_120000.sql +# +# To copy backup into container first: +# docker cp ./dockhand_backup.sql dockhand:/app/data/ +# +# Requires: DATABASE_URL environment variable +# + +set -e + +echo "========================================" +echo " Dockhand - Restore Database (PostgreSQL)" +echo "========================================" +echo "" + +# Check argument +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "" + echo "Example:" + echo " $0 /app/data/dockhand_backup_20240115_120000.sql" + echo "" + echo "To copy backup into container first:" + echo " docker cp ./dockhand_backup.sql dockhand:/app/data/" + exit 1 +fi + +BACKUP_FILE="$1" + +# Check DATABASE_URL +if [ -z "$DATABASE_URL" ]; then + echo "Error: DATABASE_URL environment variable not set" + echo "" + echo "Example: DATABASE_URL=postgres://user:pass@host:5432/dockhand" + exit 1 +fi + +# Parse DATABASE_URL +DB_URL="$DATABASE_URL" +DB_URL="${DB_URL#postgres://}" +DB_URL="${DB_URL#postgresql://}" + +DB_USER="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PASS="${DB_URL%%@*}" +DB_URL="${DB_URL#*@}" +DB_HOST="${DB_URL%%:*}" +DB_URL="${DB_URL#*:}" +DB_PORT="${DB_URL%%/*}" +DB_NAME="${DB_URL#*/}" +DB_NAME="${DB_NAME%%\?*}" + +export PGPASSWORD="$DB_PASS" + +# Check if backup file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo "Error: Backup file not found: $BACKUP_FILE" + exit 1 +fi + +# Get backup file size +BACKUP_SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}') + +echo "WARNING: This will overwrite the current database!" +echo "" +echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" +echo "Backup to restore: $BACKUP_FILE ($BACKUP_SIZE)" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +# Create backup of current database before restoring +echo "" +echo "Creating backup of current database..." +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +PRE_RESTORE_BACKUP="/app/data/dockhand_pre_restore_$TIMESTAMP.sql" +if command -v pg_dump >/dev/null 2>&1; then + pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -F p -f "$PRE_RESTORE_BACKUP" 2>/dev/null || true + if [ -f "$PRE_RESTORE_BACKUP" ]; then + echo "Current database backed up to: $PRE_RESTORE_BACKUP" + fi +fi + +echo "" +echo "Restoring database..." + +# Drop and recreate all tables by running the backup +psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$BACKUP_FILE" + +if [ $? -eq 0 ]; then + echo "" + echo "Database restored successfully!" + echo "" + echo "Restart Dockhand to apply changes:" + echo " docker restart dockhand" +else + echo "Error: Failed to restore database" + exit 1 +fi diff --git a/scripts/emergency/reset-db.sh b/scripts/emergency/reset-db.sh new file mode 100755 index 0000000..cc88591 --- /dev/null +++ b/scripts/emergency/reset-db.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# +# Emergency script to factory reset the database +# Automatically detects database type (SQLite or PostgreSQL) +# WARNING: This will DELETE ALL DATA! +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/reset-db.sh +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/reset-db.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/reset-db.sh" "$@" +fi diff --git a/scripts/emergency/reset-password.sh b/scripts/emergency/reset-password.sh new file mode 100755 index 0000000..a41fad9 --- /dev/null +++ b/scripts/emergency/reset-password.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Emergency script to reset a user's password +# Automatically detects database type (SQLite or PostgreSQL) +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/reset-password.sh +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/reset-password.sh admin MyNewPassword123 +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/reset-password.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/reset-password.sh" "$@" +fi diff --git a/scripts/emergency/restore-db.sh b/scripts/emergency/restore-db.sh new file mode 100755 index 0000000..db0575e --- /dev/null +++ b/scripts/emergency/restore-db.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# +# Emergency script to restore the database from a backup +# Automatically detects database type (SQLite or PostgreSQL) +# WARNING: This will overwrite the current database! +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/restore-db.sh +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/restore-db.sh /app/data/dockhand_backup_20240115_120000.db +# + +SCRIPT_DIR="$(dirname "$0")" + +# Detect database type +if [ -n "$DATABASE_URL" ] && (echo "$DATABASE_URL" | grep -qE '^postgres(ql)?://'); then + exec "$SCRIPT_DIR/postgres/restore-db.sh" "$@" +else + exec "$SCRIPT_DIR/sqlite/restore-db.sh" "$@" +fi diff --git a/scripts/emergency/sqlite/backup-db.sh b/scripts/emergency/sqlite/backup-db.sh new file mode 100755 index 0000000..7242ef9 --- /dev/null +++ b/scripts/emergency/sqlite/backup-db.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# +# SQLite: Emergency script to backup the database +# Creates a timestamped copy of the database file +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/backup-db.sh [output_dir] +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/sqlite/backup-db.sh /app/data/backups +# +# Default output: /app/data (same directory as database) +# + +set -e + +echo "========================================" +echo " Dockhand - Backup Database (SQLite)" +echo "========================================" +echo "" + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" +OUTPUT_DIR="${1:-$(dirname "$DB_PATH")}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" + OUTPUT_DIR="${1:-./data/db}" +fi + +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database not found at $DB_PATH" + echo "Set DOCKHAND_DB environment variable to specify the database path" + exit 1 +fi + +# Generate backup filename with timestamp +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +BACKUP_FILE="$OUTPUT_DIR/dockhand_backup_$TIMESTAMP.db" + +# Get database size +DB_SIZE=$(ls -lh "$DB_PATH" | awk '{print $5}') + +echo "This script will create a backup of the database." +echo "" +echo "Source: $DB_PATH ($DB_SIZE)" +echo "Backup: $BACKUP_FILE" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" + +# Create output directory if needed +mkdir -p "$OUTPUT_DIR" + +echo "Creating database backup..." + +# Use sqlite3 backup command for safe backup (handles WAL mode) +if command -v sqlite3 >/dev/null 2>&1; then + sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'" +else + # Fallback to file copy if sqlite3 not available + cp "$DB_PATH" "$BACKUP_FILE" +fi + +if [ $? -eq 0 ] && [ -f "$BACKUP_FILE" ]; then + SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}') + echo "" + echo "Backup created successfully!" + echo "Size: $SIZE" + echo "" + echo "To copy from Docker container to host:" + echo " docker cp dockhand:$BACKUP_FILE ./dockhand_backup_$TIMESTAMP.db" +else + echo "Error: Failed to create backup" + exit 1 +fi diff --git a/scripts/emergency/sqlite/clear-sessions.sh b/scripts/emergency/sqlite/clear-sessions.sh new file mode 100755 index 0000000..914c8ed --- /dev/null +++ b/scripts/emergency/sqlite/clear-sessions.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# +# SQLite: Emergency script to clear all user sessions +# Use this to force all users to re-login +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/clear-sessions.sh +# + +set -e + +echo "========================================" +echo " Dockhand - Clear All Sessions (SQLite)" +echo "========================================" +echo "" +echo "This script will clear all user sessions," +echo "forcing all users to log in again." +echo "" + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" +fi + +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database not found at $DB_PATH" + echo "Set DOCKHAND_DB environment variable to specify the database path" + exit 1 +fi + +COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM sessions;") + +echo "Database: $DB_PATH" +echo "Active sessions: $COUNT" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" +echo "Clearing all user sessions..." +sqlite3 "$DB_PATH" "DELETE FROM sessions;" + +if [ $? -eq 0 ]; then + echo "" + echo "Cleared $COUNT session(s) successfully." + echo "All users will need to log in again." +else + echo "Error: Failed to clear sessions" + exit 1 +fi diff --git a/scripts/emergency/sqlite/create-admin.sh b/scripts/emergency/sqlite/create-admin.sh new file mode 100755 index 0000000..91ff9c7 --- /dev/null +++ b/scripts/emergency/sqlite/create-admin.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# +# SQLite: Emergency script to create an admin user +# Use this if you're locked out of Dockhand and need to create a new admin +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/create-admin.sh +# +# Default credentials: admin / admin123 +# CHANGE THE PASSWORD IMMEDIATELY after logging in! +# + +set -e + +echo "========================================" +echo " Dockhand - Create Admin User (SQLite)" +echo "========================================" +echo "" +echo "This script will create an admin user with:" +echo " Username: admin" +echo " Password: admin123" +echo "" +echo "If user 'admin' already exists, password will" +echo "be reset and admin privileges restored." +echo "" + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" +fi + +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database not found at $DB_PATH" + echo "Set DOCKHAND_DB environment variable to specify the database path" + exit 1 +fi + +echo "Database: $DB_PATH" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +# Username and password +USERNAME="admin" +# Password: admin123 +# This is an argon2id hash of "admin123" - generated with default argon2 settings +PASSWORD_HASH='$argon2id$v=19$m=65536,t=3,p=4$Jq4am2SfyYKmc0PAHe+yzg$cq/27vK/Qg2eZb/jMDy0ExLDhOG+58cKAximxpG5Dss' + +echo "" +echo "Creating admin user..." + +# Check if admin user already exists +EXISTING=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users WHERE username='$USERNAME';") + +if [ "$EXISTING" -gt "0" ]; then + echo "User '$USERNAME' already exists." + echo "Resetting password and ensuring active status..." + sqlite3 "$DB_PATH" "UPDATE users SET password_hash='$PASSWORD_HASH', is_active=1 WHERE username='$USERNAME';" + USER_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM users WHERE username='$USERNAME';") +else + echo "Creating new admin user..." + sqlite3 "$DB_PATH" "INSERT INTO users (username, password_hash, is_active, auth_provider, created_at, updated_at) VALUES ('$USERNAME', '$PASSWORD_HASH', 1, 'local', datetime('now'), datetime('now'));" + USER_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM users WHERE username='$USERNAME';") + echo "Admin user created successfully." +fi + +# Get the Admin role ID (it's a system role) +ADMIN_ROLE_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM roles WHERE name='Admin';") + +if [ -z "$ADMIN_ROLE_ID" ]; then + echo "Warning: Admin role not found in database." + echo "The user was created but may not have admin privileges." + echo "Please check Settings > Auth > Roles after logging in." +else + # Check if user already has Admin role + HAS_ROLE=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM user_roles WHERE user_id=$USER_ID AND role_id=$ADMIN_ROLE_ID;") + + if [ "$HAS_ROLE" -eq "0" ]; then + echo "Assigning Admin role..." + sqlite3 "$DB_PATH" "INSERT INTO user_roles (user_id, role_id, created_at) VALUES ($USER_ID, $ADMIN_ROLE_ID, datetime('now'));" + echo "Admin role assigned." + else + echo "User already has Admin role." + fi +fi + +echo "" +echo "Credentials:" +echo " Username: admin" +echo " Password: admin123" +echo "" +echo "WARNING: Change the password immediately after logging in!" diff --git a/scripts/emergency/sqlite/disable-auth.sh b/scripts/emergency/sqlite/disable-auth.sh new file mode 100755 index 0000000..1ebcc49 --- /dev/null +++ b/scripts/emergency/sqlite/disable-auth.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# +# SQLite: Emergency script to disable authentication +# Use this if you're locked out of Dockhand +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/disable-auth.sh +# + +set -e + +echo "========================================" +echo " Dockhand - Disable Authentication (SQLite)" +echo "========================================" +echo "" +echo "This script will disable authentication," +echo "allowing access to Dockhand without login." +echo "" + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" +fi + +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database not found at $DB_PATH" + echo "Set DOCKHAND_DB environment variable to specify the database path" + exit 1 +fi + +echo "Database: $DB_PATH" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" +echo "Disabling authentication..." +sqlite3 "$DB_PATH" "UPDATE auth_settings SET auth_enabled = 0 WHERE id = 1;" + +if [ $? -eq 0 ]; then + echo "" + echo "Authentication disabled successfully." + echo "You can now access Dockhand without logging in." + echo "" + echo "Remember to re-enable authentication in Settings after regaining access." +else + echo "Error: Failed to disable authentication" + exit 1 +fi diff --git a/scripts/emergency/sqlite/list-users.sh b/scripts/emergency/sqlite/list-users.sh new file mode 100755 index 0000000..9df5c25 --- /dev/null +++ b/scripts/emergency/sqlite/list-users.sh @@ -0,0 +1,80 @@ +#!/bin/sh +# +# SQLite: Emergency script to list all users +# Shows username, admin status, active status, and last login +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/list-users.sh +# + +set -e + +echo "========================================" +echo " Dockhand - List Users (SQLite)" +echo "========================================" +echo "" + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" +fi + +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database not found at $DB_PATH" + echo "Set DOCKHAND_DB environment variable to specify the database path" + exit 1 +fi + +# Get user count +USER_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users;") + +if [ "$USER_COUNT" -eq "0" ]; then + echo "No users found." + exit 0 +fi + +# Get Admin role ID for checking admin status +ADMIN_ROLE_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM roles WHERE name='Admin';" 2>/dev/null || echo "") + +# Print header +printf "%-4s %-20s %-8s %-8s %-6s %s\n" "ID" "Username" "Admin" "Active" "MFA" "Last Login" +printf "%-4s %-20s %-8s %-8s %-6s %s\n" "----" "--------------------" "--------" "--------" "------" "-------------------" + +# List users (check admin status via user_roles table) +sqlite3 -separator '|' "$DB_PATH" "SELECT id, username, is_active, mfa_enabled, COALESCE(last_login, 'Never') FROM users ORDER BY id;" | while IFS='|' read id username is_active mfa_enabled last_login; do + # Check if user has Admin role + if [ -n "$ADMIN_ROLE_ID" ]; then + HAS_ADMIN=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM user_roles WHERE user_id=$id AND role_id=$ADMIN_ROLE_ID;") + if [ "$HAS_ADMIN" -gt "0" ]; then + admin_str="Yes" + else + admin_str="No" + fi + else + admin_str="N/A" + fi + + if [ "$is_active" = "1" ]; then + active_str="Yes" + else + active_str="No" + fi + + if [ "$mfa_enabled" = "1" ]; then + mfa_str="Yes" + else + mfa_str="No" + fi + + printf "%-4s %-20s %-8s %-8s %-6s %s\n" "$id" "$username" "$admin_str" "$active_str" "$mfa_str" "$last_login" +done + +echo "" +echo "Total: $USER_COUNT user(s)" + +# Show session count +SESSION_COUNT=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM sessions;") +echo "Active sessions: $SESSION_COUNT" diff --git a/scripts/emergency/sqlite/reset-db.sh b/scripts/emergency/sqlite/reset-db.sh new file mode 100755 index 0000000..d53532b --- /dev/null +++ b/scripts/emergency/sqlite/reset-db.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# +# SQLite: Emergency script to factory reset the database +# WARNING: This will DELETE ALL DATA including users, settings, and activity logs! +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-db.sh +# + +set -e + +echo "========================================" +echo " Dockhand - Factory Reset Database (SQLite)" +echo "========================================" +echo "" +echo "WARNING: This will DELETE ALL DATA!" +echo "" +echo "This includes:" +echo " - All users and their settings" +echo " - All sessions" +echo " - Authentication settings" +echo " - Activity logs" +echo " - Environment configurations" +echo " - OIDC/SSO settings" +echo "" +echo "The database will be recreated on next startup." +echo "" + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" +fi + +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database not found at $DB_PATH" + echo "Nothing to reset." + exit 0 +fi + +echo "Database: $DB_PATH" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +echo "" +echo "Creating backup before reset..." +BACKUP_FILE="${DB_PATH}.backup.$(date +%Y%m%d_%H%M%S)" +cp "$DB_PATH" "$BACKUP_FILE" +echo "Backup saved to: $BACKUP_FILE" + +echo "" +echo "Deleting database..." +rm -f "$DB_PATH" +rm -f "${DB_PATH}-wal" +rm -f "${DB_PATH}-shm" + +echo "" +echo "Database deleted successfully." +echo "" +echo "Restart Dockhand to recreate a fresh database:" +echo " docker restart dockhand" diff --git a/scripts/emergency/sqlite/reset-password.sh b/scripts/emergency/sqlite/reset-password.sh new file mode 100755 index 0000000..9a38214 --- /dev/null +++ b/scripts/emergency/sqlite/reset-password.sh @@ -0,0 +1,123 @@ +#!/bin/sh +# +# SQLite: Emergency script to reset a user's password +# Use this if a user is locked out and needs a password reset +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-password.sh +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/sqlite/reset-password.sh admin MyNewPassword123 +# + +set -e + +echo "========================================" +echo " Dockhand - Reset User Password (SQLite)" +echo "========================================" +echo "" + +# Check arguments +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Usage: $0 " + echo "" + echo "Example:" + echo " $0 admin MyNewPassword123" + exit 1 +fi + +USERNAME="$1" +NEW_PASSWORD="$2" + +# Validate password length +if [ ${#NEW_PASSWORD} -lt 8 ]; then + echo "Error: Password must be at least 8 characters" + exit 1 +fi + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" +fi + +if [ ! -f "$DB_PATH" ]; then + echo "Error: Database not found at $DB_PATH" + echo "Set DOCKHAND_DB environment variable to specify the database path" + exit 1 +fi + +# Check if user exists +EXISTING=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM users WHERE username='$USERNAME';") + +if [ "$EXISTING" -eq "0" ]; then + echo "Error: User '$USERNAME' not found" + echo "" + echo "Available users:" + sqlite3 "$DB_PATH" "SELECT username FROM users;" | while read user; do + echo " - $user" + done + exit 1 +fi + +echo "This script will reset the password for user '$USERNAME'." +echo "" +echo "Database: $DB_PATH" +echo "Username: $USERNAME" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +# Generate password hash using node (argon2 is available in the app) +echo "" +echo "Generating password hash..." + +# Check if node and argon2 are available +if command -v node >/dev/null 2>&1; then + # Try to use argon2 from node_modules + PASSWORD_HASH=$(node -e " + try { + const argon2 = require('argon2'); + argon2.hash('$NEW_PASSWORD').then(h => console.log(h)).catch(e => process.exit(1)); + } catch(e) { + process.exit(1); + } + " 2>/dev/null) + + if [ -z "$PASSWORD_HASH" ]; then + echo "Error: Could not generate password hash (argon2 not available)" + echo "This script requires Node.js with argon2 module" + exit 1 + fi +else + echo "Error: Node.js is required to generate password hash" + exit 1 +fi + +echo "Resetting password for user '$USERNAME'..." +sqlite3 "$DB_PATH" "UPDATE users SET password_hash='$PASSWORD_HASH', updated_at=datetime('now') WHERE username='$USERNAME';" + +if [ $? -eq 0 ]; then + echo "" + echo "Password reset successfully for user '$USERNAME'" + echo "" + # Invalidate sessions + USER_ID=$(sqlite3 "$DB_PATH" "SELECT id FROM users WHERE username='$USERNAME';") + sqlite3 "$DB_PATH" "DELETE FROM sessions WHERE user_id=$USER_ID;" 2>/dev/null || true + echo "All existing sessions have been invalidated." + echo "The user can now log in with the new password." +else + echo "Error: Failed to reset password" + exit 1 +fi diff --git a/scripts/emergency/sqlite/restore-db.sh b/scripts/emergency/sqlite/restore-db.sh new file mode 100755 index 0000000..96aa9e6 --- /dev/null +++ b/scripts/emergency/sqlite/restore-db.sh @@ -0,0 +1,106 @@ +#!/bin/sh +# +# SQLite: Emergency script to restore the database from a backup +# WARNING: This will overwrite the current database! +# +# Usage: +# docker exec -it dockhand /app/scripts/emergency/sqlite/restore-db.sh +# +# Example: +# docker exec -it dockhand /app/scripts/emergency/sqlite/restore-db.sh /app/data/dockhand_backup_20240115_120000.db +# +# To copy backup into container first: +# docker cp ./dockhand_backup.db dockhand:/app/data/ +# + +set -e + +echo "========================================" +echo " Dockhand - Restore Database (SQLite)" +echo "========================================" +echo "" + +# Check argument +if [ -z "$1" ]; then + echo "Usage: $0 " + echo "" + echo "Example:" + echo " $0 /app/data/dockhand_backup_20240115_120000.db" + echo "" + echo "To copy backup into container first:" + echo " docker cp ./dockhand_backup.db dockhand:/app/data/" + exit 1 +fi + +BACKUP_FILE="$1" + +# Default database path +DB_PATH="${DOCKHAND_DB:-/app/data/db/dockhand.db}" + +# Check if running locally (not in Docker) +if [ ! -f "$DB_PATH" ] && [ -f "./data/db/dockhand.db" ]; then + DB_PATH="./data/db/dockhand.db" +fi + +# Check if backup file exists +if [ ! -f "$BACKUP_FILE" ]; then + echo "Error: Backup file not found: $BACKUP_FILE" + exit 1 +fi + +# Verify it's a valid SQLite database +if ! sqlite3 "$BACKUP_FILE" "SELECT 1;" >/dev/null 2>&1; then + echo "Error: File is not a valid SQLite database: $BACKUP_FILE" + exit 1 +fi + +# Get backup file size +BACKUP_SIZE=$(ls -lh "$BACKUP_FILE" | awk '{print $5}') + +echo "WARNING: This will overwrite the current database!" +echo "" +echo "Current database: $DB_PATH" +echo "Backup to restore: $BACKUP_FILE ($BACKUP_SIZE)" +echo "" +printf "Continue? [y/N]: " +read CONFIRM + +case "$CONFIRM" in + [yY]|[yY][eE][sS]) + ;; + *) + echo "Aborted." + exit 0 + ;; +esac + +# Create backup of current database before restoring +if [ -f "$DB_PATH" ]; then + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + PRE_RESTORE_BACKUP="${DB_PATH}.pre-restore.$TIMESTAMP" + echo "" + echo "Creating backup of current database..." + cp "$DB_PATH" "$PRE_RESTORE_BACKUP" + echo "Current database backed up to: $PRE_RESTORE_BACKUP" +fi + +echo "" +echo "Restoring database..." + +# Remove WAL files if they exist +rm -f "${DB_PATH}-wal" +rm -f "${DB_PATH}-shm" + +# Copy backup to database location +cp "$BACKUP_FILE" "$DB_PATH" + +if [ $? -eq 0 ]; then + echo "" + echo "Database restored successfully!" + echo "" + echo "Restart Dockhand to apply changes:" + echo " docker restart dockhand" +else + echo "Error: Failed to restore database" + exit 1 +fi diff --git a/scripts/generate-changelog-page.ts b/scripts/generate-changelog-page.ts new file mode 100644 index 0000000..6ab60d1 --- /dev/null +++ b/scripts/generate-changelog-page.ts @@ -0,0 +1,164 @@ +#!/usr/bin/env bun +/** + * Generate changelog section in webpage/index.html from src/lib/data/changelog.json + * This ensures a single source of truth for release information + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +const ROOT_DIR = join(import.meta.dir, '..'); +const CHANGELOG_PATH = join(ROOT_DIR, 'src/lib/data/changelog.json'); +const INDEX_PATH = join(ROOT_DIR, 'webpage/index.html'); + +interface ChangelogEntry { + version: string; + date: string; + changes: Array<{ type: 'feature' | 'fix'; text: string }>; + imageTag: string; +} + +// SVG icons for change types +const FEATURE_SVG = ``; + +const FIX_SVG = ``; + +const TOGGLE_SVG = ``; + +const COPY_SVG = ``; + +function formatDate(dateStr: string): string { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); +} + +function generateChangeItem(change: { type: 'feature' | 'fix'; text: string }): string { + const pillClass = change.type === 'feature' ? 'changelog-pill-feature' : 'changelog-pill-fix'; + const svg = change.type === 'feature' ? FEATURE_SVG : FIX_SVG; + const label = change.type === 'feature' ? 'New' : 'Fix'; + return `
  • ${svg}${label}${change.text}
  • `; +} + +function generateLatestEntry(entry: ChangelogEntry): string { + const changes = entry.changes.map(generateChangeItem).join('\n'); + const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`; + + return ` +
    +
    +
    +

    ${version}

    + Latest +
    + ${formatDate(entry.date)} +
    +
      +${changes} +
    +
    + Docker image: + ${entry.imageTag} + + or + fnsys/dockhand:latest + +
    +
    `; +} + +function generateCollapsibleEntry(entry: ChangelogEntry): string { + const changes = entry.changes.map(generateChangeItem).join('\n'); + const version = entry.version.startsWith('v') ? entry.version : `v${entry.version}`; + + return ` +
    +
    +
    +

    ${version}

    + ${TOGGLE_SVG} +
    + ${formatDate(entry.date)} +
    +
    +
      +${changes} +
    +
    + Docker image: + ${entry.imageTag} + +
    +
    +
    `; +} + +function generateChangelogSection(entries: ChangelogEntry[]): string { + if (entries.length === 0) { + return ''; + } + + const [latest, ...rest] = entries; + const latestHtml = generateLatestEntry(latest); + const restHtml = rest.map(generateCollapsibleEntry).join('\n'); + + return ` +
    +
    +
    + +

    Release history

    +

    Track our progress and see what's new in each version. Spoiler: it gets better every time.

    +
    +
    +${latestHtml} +${restHtml} +
    +
    +
    `; +} + +// Read changelog.json +console.log('Reading changelog from:', CHANGELOG_PATH); +const changelog: ChangelogEntry[] = JSON.parse(readFileSync(CHANGELOG_PATH, 'utf-8')); +console.log(`Found ${changelog.length} changelog entries`); + +// Read index.html +console.log('Reading index.html from:', INDEX_PATH); +let indexHtml = readFileSync(INDEX_PATH, 'utf-8'); + +// Generate new changelog section +const newChangelogSection = generateChangelogSection(changelog); + +// Replace changelog section using regex +// Match from "" to the closing "" before "" +const changelogRegex = / [\s\S]*?<\/section>(?=\s*\n\s*)/; + +if (!changelogRegex.test(indexHtml)) { + console.error('ERROR: Could not find changelog section in index.html'); + console.error('Looking for pattern: ... followed by '); + process.exit(1); +} + +indexHtml = indexHtml.replace(changelogRegex, newChangelogSection); + +// Also update softwareVersion in JSON-LD schema +if (changelog.length > 0) { + const latestVersion = changelog[0].version; + // Match "softwareVersion": "X.X" or "softwareVersion": "X.X.X" + const versionRegex = /"softwareVersion":\s*"[\d.]+"/; + if (versionRegex.test(indexHtml)) { + indexHtml = indexHtml.replace(versionRegex, `"softwareVersion": "${latestVersion}"`); + console.log(`Updated softwareVersion to: ${latestVersion}`); + } +} + +// Write back to index.html +writeFileSync(INDEX_PATH, indexHtml); +console.log(''); +console.log('Generated changelog in webpage/index.html'); +console.log(` - Latest version: v${changelog[0]?.version || 'unknown'}`); +console.log(` - Total entries: ${changelog.length}`); diff --git a/scripts/generate-legal-pages.ts b/scripts/generate-legal-pages.ts new file mode 100644 index 0000000..5056190 --- /dev/null +++ b/scripts/generate-legal-pages.ts @@ -0,0 +1,137 @@ +#!/usr/bin/env bun +/** + * Generate static HTML pages for License and Privacy from .txt files + * This ensures a single source of truth for legal documents + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +const ROOT_DIR = join(import.meta.dir, '..'); +const WEBPAGE_DIR = join(ROOT_DIR, 'webpage'); + +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function generateHtmlPage(title: string, content: string): string { + return ` + + + + + ${title} - Dockhand + + + + +
    +
    + + Dockhand + + ← Back to home +
    + +

    ${title}

    + +
    +
    ${escapeHtml(content)}
    +
    + + +
    + +`; +} + +// Read the source files +const licenseContent = readFileSync(join(ROOT_DIR, 'LICENSE.txt'), 'utf-8'); +const privacyContent = readFileSync(join(ROOT_DIR, 'PRIVACY.txt'), 'utf-8'); + +// Generate HTML pages +const licenseHtml = generateHtmlPage('License Terms and Conditions', licenseContent); +const privacyHtml = generateHtmlPage('Privacy Policy', privacyContent); + +// Write to webpage directory +writeFileSync(join(WEBPAGE_DIR, 'license.html'), licenseHtml); +writeFileSync(join(WEBPAGE_DIR, 'privacy.html'), privacyHtml); + +console.log('Generated legal pages:'); +console.log(' - webpage/license.html'); +console.log(' - webpage/privacy.html'); diff --git a/scripts/patch-build.ts b/scripts/patch-build.ts new file mode 100644 index 0000000..df1b85a --- /dev/null +++ b/scripts/patch-build.ts @@ -0,0 +1,604 @@ +/** + * Post-build script to fix svelte-adapter-bun WebSocket issue + * The adapter calls server.websocket() which doesn't exist in SvelteKit. + * + * IMPORTANT: Terminal WebSocket logic is shared with vite.config.ts + * Core functions like resolveDockerTarget are defined in: + * src/lib/server/ws-terminal-shared.ts + * + * When updating WebSocket terminal handling, update the shared module + * and this file will use the same logic at build time. + */ + +import { join } from 'node:path'; + +const BUILD_DIR = join(import.meta.dir, '../build'); + +async function patchHandler() { + const handlerPath = join(BUILD_DIR, 'handler.js'); + const handlerFile = Bun.file(handlerPath); + + if (!await handlerFile.exists()) { + console.error('handler.js not found'); + process.exit(1); + } + + let content = await handlerFile.text(); + + // Replace broken server.websocket() call + content = content.replace( + 'const websocket = server.websocket();', + 'const websocket = null;' + ); + + // Add WebSocket upgrade detection before ssr handler + const ssrIndex = content.indexOf('var ssr = async (request, bunServer) => {'); + if (ssrIndex > -1) { + const upgradeCode = ` +var handleUpgrade = (request, bunServer) => { + const url = new URL(request.url); + const isUpgrade = request.headers.get('connection')?.toLowerCase().includes('upgrade') && + request.headers.get('upgrade')?.toLowerCase() === 'websocket'; + if (!isUpgrade) return null; + + // Handle terminal exec WebSocket + if (url.pathname.includes('/api/containers/') && url.pathname.includes('/exec')) { + const pathParts = url.pathname.split('/'); + const containerIdIndex = pathParts.indexOf('containers') + 1; + const containerId = pathParts[containerIdIndex]; + const shell = url.searchParams.get('shell') || '/bin/sh'; + const user = url.searchParams.get('user') || 'root'; + const envId = url.searchParams.get('envId') ? parseInt(url.searchParams.get('envId'), 10) : undefined; + if (bunServer.upgrade(request, { data: { type: 'terminal', containerId, shell, user, envId } })) { + return new Response(null, { status: 101 }); + } + } + + // Handle terminal attach WebSocket + if (url.pathname.includes('/api/containers/') && url.pathname.includes('/attach')) { + const pathParts = url.pathname.split('/'); + const containerIdIndex = pathParts.indexOf('containers') + 1; + const containerId = pathParts[containerIdIndex]; + const envId = url.searchParams.get('envId') ? parseInt(url.searchParams.get('envId'), 10) : undefined; + if (bunServer.upgrade(request, { data: { type: 'terminal', mode: 'attach', containerId, envId } })) { + return new Response(null, { status: 101 }); + } + } + + // Handle Hawser Edge WebSocket + if (url.pathname === '/api/hawser/connect') { + if (bunServer.upgrade(request, { data: { type: 'hawser' } })) { + return new Response(null, { status: 101 }); + } + } + + return null; +}; +`; + content = content.slice(0, ssrIndex) + upgradeCode + content.slice(ssrIndex); + } + + // Modify handler to check for upgrade first + content = content.replace( + 'return ssr(request, server2);', + 'const upgradeResponse = handleUpgrade(request, server2); if (upgradeResponse) return upgradeResponse; return ssr(request, server2);' + ); + + await Bun.write(handlerPath, content); + console.log('✓ Patched handler.js'); +} + +async function patchIndex() { + const indexPath = join(BUILD_DIR, 'index.js'); + const indexFile = Bun.file(indexPath); + + if (!await indexFile.exists()) { + console.error('index.js not found'); + process.exit(1); + } + + let content = await indexFile.text(); + + const wsHandler = ` +import { existsSync as _existsSync } from 'fs'; +import { homedir as _homedir } from 'os'; +import { Database as _Database } from 'bun:sqlite'; +import { SQL as _SQL } from 'bun'; +import { join as _join } from 'path'; + +// Database connection (supports both SQLite and PostgreSQL) +let _db = null; +let _isPostgres = false; +function _getDb() { + if (!_db) { + const dbUrl = process.env.DATABASE_URL; + if (dbUrl && (dbUrl.startsWith('postgres://') || dbUrl.startsWith('postgresql://'))) { + _db = new _SQL(dbUrl); + _isPostgres = true; + } else { + const _dbPath = _join(process.cwd(), 'data', 'db', 'dockhand.db'); + if (_existsSync(_dbPath)) { + _db = new _Database(_dbPath); + } + } + } + return _db; +} + +async function _getEnvironment(id) { + const db = _getDb(); + if (!db) return null; + let row; + if (_isPostgres) { + const result = await db.unsafe('SELECT * FROM environments WHERE id = $1', [id]); + row = result[0]; + } else { + row = db.prepare('SELECT * FROM environments WHERE id = ?').get(id); + } + return row ? { ...row, is_local: Boolean(row.is_local), connection_type: row.connection_type, hawser_token: row.hawser_token } : null; +} + +function detectDockerSocket() { + if (process.env.DOCKER_SOCKET && _existsSync(process.env.DOCKER_SOCKET)) return process.env.DOCKER_SOCKET; + if (process.env.DOCKER_HOST?.startsWith('unix://')) { + const p = process.env.DOCKER_HOST.replace('unix://', ''); + if (_existsSync(p)) return p; + } + for (const s of ['/var/run/docker.sock', _homedir() + '/.docker/run/docker.sock', _homedir() + '/.orbstack/run/docker.sock', '/run/docker.sock']) { + if (_existsSync(s)) return s; + } + return '/var/run/docker.sock'; +} +const dockerSocketPath = detectDockerSocket(); +console.log('Detected Docker socket at:', dockerSocketPath); + +const dockerStreams = new Map(); +let _wsConnCounter = 0; + +async function _getDockerTarget(envId) { + if (!envId) return { type: 'unix', socket: dockerSocketPath }; + const env = await _getEnvironment(envId); + if (!env) return { type: 'unix', socket: dockerSocketPath }; + // Check for socket connection type (local Unix socket) + if (env.is_local || env.connection_type === 'socket' || !env.connection_type) { + return { type: 'unix', socket: env.socket_path || dockerSocketPath }; + } + if (env.connection_type === 'hawser-edge') return { type: 'hawser-edge', environmentId: envId }; + return { type: 'tcp', host: env.host, port: env.port || 2375, hawserToken: env.connection_type === 'hawser-standard' ? env.hawser_token : undefined }; +} + +async function createExec(containerId, cmd, user, target) { + const headers = { 'Content-Type': 'application/json' }; + const fetchOpts = { + method: 'POST', + headers, + body: JSON.stringify({ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: cmd, User: user }) + }; + let url; + if (target.type === 'unix') { + url = 'http://localhost/containers/' + containerId + '/exec'; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/containers/' + containerId + '/exec'; + if (target.hawserToken) headers['X-Hawser-Token'] = target.hawserToken; + } + const res = await fetch(url, fetchOpts); + if (!res.ok) throw new Error('Failed to create exec: ' + (await res.text())); + return res.json(); +} + +async function resizeExec(execId, cols, rows, target) { + try { + const fetchOpts = { method: 'POST' }; + let url; + if (target.type === 'unix') { + url = 'http://localhost/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + if (target.hawserToken) fetchOpts.headers = { 'X-Hawser-Token': target.hawserToken }; + } + await fetch(url, fetchOpts); + } catch {} +} + +// ============ Hawser Edge Support ============ +// Global edge connections map (shared with hawser.ts via globalThis) +if (!globalThis.__hawserEdgeConnections) globalThis.__hawserEdgeConnections = new Map(); +const _edgeConnections = globalThis.__hawserEdgeConnections; + +// Map WebSocket to environmentId for quick lookup +const _wsToEnvId = new Map(); + +// Edge exec sessions (execId -> frontend WebSocket) +const _edgeExecSessions = new Map(); + +// Validate Hawser token against database +async function _validateHawserToken(token) { + const db = _getDb(); + if (!db) return { valid: false }; + let tokens; + if (_isPostgres) { + tokens = await db.unsafe('SELECT * FROM hawser_tokens WHERE is_active = true'); + } else { + tokens = db.prepare('SELECT * FROM hawser_tokens WHERE is_active = 1').all(); + } + for (const t of tokens) { + try { + const isValid = await Bun.password.verify(token, t.token); + if (isValid) { + if (_isPostgres) { + await db.unsafe('UPDATE hawser_tokens SET last_used = NOW() WHERE id = $1', [t.id]); + } else { + db.prepare('UPDATE hawser_tokens SET last_used = datetime(\\'now\\') WHERE id = ?').run(t.id); + } + return { valid: true, environmentId: t.environment_id, tokenId: t.id }; + } + } catch {} + } + return { valid: false }; +} + +// Update environment status in database +async function _updateEnvStatus(envId, conn) { + const db = _getDb(); + if (!db) return; + try { + if (conn) { + if (_isPostgres) { + await db.unsafe('UPDATE environments SET hawser_last_seen = NOW(), hawser_agent_id = $1, hawser_agent_name = $2, hawser_version = $3, hawser_capabilities = $4 WHERE id = $5', + [conn.agentId, conn.agentName, conn.agentVersion, JSON.stringify(conn.capabilities || []), envId]); + } else { + db.prepare('UPDATE environments SET hawser_last_seen = datetime(\\'now\\'), hawser_agent_id = ?, hawser_agent_name = ?, hawser_version = ?, hawser_capabilities = ? WHERE id = ?') + .run(conn.agentId, conn.agentName, conn.agentVersion, JSON.stringify(conn.capabilities || []), envId); + } + } else { + if (_isPostgres) { + await db.unsafe('UPDATE environments SET hawser_last_seen = NOW() WHERE id = $1', [envId]); + } else { + db.prepare('UPDATE environments SET hawser_last_seen = datetime(\\'now\\') WHERE id = ?').run(envId); + } + } + } catch {} +} + +// Handle Hawser Edge protocol messages +async function _handleHawserMessage(ws, msg) { + if (msg.type === 'hello') { + console.log('[Hawser] Hello from agent:', msg.agentName, '(' + msg.agentId + ')'); + const validation = await _validateHawserToken(msg.token); + if (!validation.valid) { + console.log('[Hawser] Invalid token'); + ws.send(JSON.stringify({ type: 'error', error: 'Invalid token' })); + ws.close(); + return; + } + const envId = validation.environmentId; + const existing = _edgeConnections.get(envId); + if (existing) { + const pendingCount = existing.pendingRequests.size; + const streamCount = existing.pendingStreamRequests.size; + console.log('[Hawser] Replacing existing connection for env', envId, '- rejecting', pendingCount, 'pending requests and', streamCount, 'stream requests'); + // Reject all pending requests before closing + for (const [requestId, pending] of existing.pendingRequests) { + clearTimeout(pending.timeout); + pending.reject(new Error('Connection replaced by new agent')); + } + for (const [requestId, pending] of existing.pendingStreamRequests) { + pending.onEnd?.('Connection replaced by new agent'); + } + existing.pendingRequests.clear(); + existing.pendingStreamRequests.clear(); + existing.ws.close(1000, 'Replaced'); + _wsToEnvId.delete(existing.ws); + } + const conn = { + ws, environmentId: envId, agentId: msg.agentId, agentName: msg.agentName, + agentVersion: msg.version || 'unknown', dockerVersion: msg.dockerVersion || 'unknown', + hostname: msg.hostname || 'unknown', capabilities: msg.capabilities || [], + connectedAt: new Date(), lastHeartbeat: new Date(), + pendingRequests: new Map(), pendingStreamRequests: new Map(), + pingInterval: null + }; + _edgeConnections.set(envId, conn); + _wsToEnvId.set(ws, envId); + await _updateEnvStatus(envId, conn); + ws.send(JSON.stringify({ type: 'welcome', environmentId: envId, message: 'Connected to Dockhand' })); + // Start server-side ping interval to keep connection alive through Traefik/proxies (5s) + conn.pingInterval = setInterval(() => { + try { ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); } + catch { if (conn.pingInterval) { clearInterval(conn.pingInterval); conn.pingInterval = null; } } + }, 5000); + console.log('[Hawser] Agent', msg.agentName, 'connected for env', envId); + } else if (msg.type === 'ping') { + const envId = _wsToEnvId.get(ws); + if (envId) { const c = _edgeConnections.get(envId); if (c) c.lastHeartbeat = new Date(); } + ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); + } else if (msg.type === 'pong') { + const envId = _wsToEnvId.get(ws); + if (envId) { const c = _edgeConnections.get(envId); if (c) c.lastHeartbeat = new Date(); } + } else if (msg.type === 'response') { + const envId = _wsToEnvId.get(ws); + if (!envId) { + console.warn('[Hawser] Response from unknown WebSocket, requestId=' + msg.requestId); + return; + } + const conn = _edgeConnections.get(envId); + if (conn) { + const pending = conn.pendingRequests.get(msg.requestId); + if (pending) { + clearTimeout(pending.timeout); + conn.pendingRequests.delete(msg.requestId); + pending.resolve({ statusCode: msg.statusCode, headers: msg.headers || {}, body: msg.body || '', isBinary: msg.isBinary || false }); + } else { + console.warn('[Hawser] Response for unknown request ' + msg.requestId + ' on env ' + envId); + } + } + } else if (msg.type === 'stream') { + const envId = _wsToEnvId.get(ws); + if (!envId) { + console.warn('[Hawser] Stream data from unknown WebSocket, requestId=' + msg.requestId); + return; + } + const conn = _edgeConnections.get(envId); + if (conn?.pendingStreamRequests) { + const pending = conn.pendingStreamRequests.get(msg.requestId); + if (pending) { + pending.onData(msg.data, msg.stream); + } else { + console.warn('[Hawser] Stream data for unknown request ' + msg.requestId + ' on env ' + envId); + } + } + } else if (msg.type === 'stream_end') { + const envId = _wsToEnvId.get(ws); + if (!envId) { + console.warn('[Hawser] Stream end from unknown WebSocket, requestId=' + msg.requestId); + return; + } + const conn = _edgeConnections.get(envId); + if (conn?.pendingStreamRequests) { + const pending = conn.pendingStreamRequests.get(msg.requestId); + if (pending) { + conn.pendingStreamRequests.delete(msg.requestId); + pending.onEnd(msg.reason); + } else { + console.warn('[Hawser] Stream end for unknown request ' + msg.requestId + ' on env ' + envId); + } + } + } else if (msg.type === 'exec_ready') { + const session = _edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) console.log('[Hawser] Exec ready:', msg.execId); + } else if (msg.type === 'exec_output') { + const session = _edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) { + const data = Buffer.from(msg.data, 'base64').toString('utf-8'); + session.ws.send(JSON.stringify({ type: 'output', data })); + } + } else if (msg.type === 'exec_end') { + const session = _edgeExecSessions.get(msg.execId); + if (session) { + console.log('[Hawser] Exec ended:', msg.execId); + if (session.ws?.readyState === 1) { session.ws.send(JSON.stringify({ type: 'exit' })); session.ws.close(); } + _edgeExecSessions.delete(msg.execId); + } + } else if (msg.type === 'container_event') { + const envId = _wsToEnvId.get(ws); + if (envId && msg.event) { + // Call the global handler registered by hawser.ts + if (globalThis.__hawserHandleContainerEvent) { + globalThis.__hawserHandleContainerEvent(envId, msg.event).catch((err) => { + console.error('[Hawser] Error handling container event:', err); + }); + } + } + } else if (msg.type === 'metrics') { + // Metrics from agent - save to database for dashboard graphs + const envId = _wsToEnvId.get(ws); + if (envId && msg.metrics) { + if (globalThis.__hawserHandleMetrics) { + globalThis.__hawserHandleMetrics(envId, msg.metrics).catch((err) => { + console.error('[Hawser] Error saving metrics:', err); + }); + } + } + } +} + +// Expose send function for hawser.ts module +globalThis.__hawserSendMessage = (envId, message) => { + const conn = _edgeConnections.get(envId); + if (!conn?.ws) return false; + try { conn.ws.send(message); return true; } catch { return false; } +}; + +// ============ Combined WebSocket Handler ============ +const combinedWebsocket = { + async open(ws) { + const connType = ws.data?.type; + + // Hawser Edge connection - wait for hello message + if (connType === 'hawser') { + console.log('[Hawser] New connection pending authentication'); + return; + } + + // Terminal connection + const connId = 'ws-' + (++_wsConnCounter); + ws.data = ws.data || {}; + ws.data.connId = connId; + const { containerId, shell, user, envId, mode } = ws.data; + if (!containerId) { ws.send(JSON.stringify({ type: 'error', message: 'No container ID' })); ws.close(); return; } + const target = await _getDockerTarget(envId); + const isAttach = mode === 'attach'; + console.log('[WS] Open:', connId, containerId, 'mode:', mode || 'exec', 'target:', target.type); + + // Handle Hawser Edge terminal + if (target.type === 'hawser-edge') { + const conn = _edgeConnections.get(target.environmentId); + if (!conn) { ws.send(JSON.stringify({ type: 'error', message: 'Edge agent not connected' })); ws.close(); return; } + const execId = crypto.randomUUID(); + _edgeExecSessions.set(execId, { ws, execId, environmentId: target.environmentId }); + ws.data.edgeExecId = execId; + if (isAttach) { + conn.ws.send(JSON.stringify({ type: 'attach', containerId, attachId: execId })); + } else { + conn.ws.send(JSON.stringify({ type: 'exec_start', execId, containerId, cmd: shell || '/bin/sh', user: user || 'root', cols: 120, rows: 30 })); + } + return; + } + + try { + let execId; + let httpRequest; + + if (isAttach) { + // Attach directly to container streams + execId = crypto.randomUUID(); + const tokenHeader = target.type === 'tcp' && target.hawserToken ? 'X-Hawser-Token: ' + target.hawserToken + '\\r\\n' : ''; + httpRequest = 'POST /containers/' + containerId + '/attach?stream=1&stdout=1&stderr=1&stdin=1 HTTP/1.1\\r\\nHost: localhost\\r\\nContent-Type: application/json\\r\\n' + tokenHeader + 'Connection: Upgrade\\r\\nUpgrade: tcp\\r\\nContent-Length: 0\\r\\n\\r\\n'; + } else { + // Create exec instance for shell + const exec = await createExec(containerId, [shell || '/bin/sh'], user || 'root', target); + execId = exec.Id; + const body = JSON.stringify({ Detach: false, Tty: true }); + const tokenHeader = target.type === 'tcp' && target.hawserToken ? 'X-Hawser-Token: ' + target.hawserToken + '\\r\\n' : ''; + httpRequest = 'POST /exec/' + execId + '/start HTTP/1.1\\r\\nHost: localhost\\r\\nContent-Type: application/json\\r\\n' + tokenHeader + 'Connection: Upgrade\\r\\nUpgrade: tcp\\r\\nContent-Length: ' + body.length + '\\r\\n\\r\\n' + body; + } + + let dockerStream; + let headersStripped = false; + let isChunked = false; + const socketHandler = { + data(socket, data) { + if (ws.readyState === 1) { + let text = new TextDecoder().decode(data); + if (!headersStripped) { + if (text.toLowerCase().includes('transfer-encoding: chunked')) isChunked = true; + const i = text.indexOf('\\r\\n\\r\\n'); + if (i > -1) { text = text.slice(i + 4); headersStripped = true; } + else if (text.startsWith('HTTP/')) return; + } + if (isChunked && text) text = text.replace(/^[0-9a-fA-F]+\\r\\n/gm, '').replace(/\\r\\n$/g, ''); + if (text) ws.send(JSON.stringify({ type: 'output', data: text })); + } + }, + close() { if (ws.readyState === 1) { ws.send(JSON.stringify({ type: 'exit' })); ws.close(); } }, + error() {}, + open(socket) { + socket.write(httpRequest); + } + }; + if (target.type === 'unix') { + dockerStream = await Bun.connect({ unix: target.socket, socket: socketHandler }); + } else { + dockerStream = await Bun.connect({ hostname: target.host, port: target.port, socket: socketHandler }); + } + dockerStreams.set(connId, { stream: dockerStream, execId, target }); + } catch (e) { ws.send(JSON.stringify({ type: 'error', message: e.message })); ws.close(); } + }, + async message(ws, message) { + const connType = ws.data?.type; + + // Hawser Edge message + if (connType === 'hawser') { + try { + let msgStr = typeof message === 'string' ? message : message instanceof ArrayBuffer ? new TextDecoder().decode(message) : Buffer.isBuffer(message) ? message.toString('utf-8') : new TextDecoder().decode(new Uint8Array(message)); + const msg = JSON.parse(msgStr); + await _handleHawserMessage(ws, msg); + } catch (e) { + console.error('[Hawser] Error:', e.message); + ws.send(JSON.stringify({ type: 'error', error: e.message })); + } + return; + } + + // Edge exec session input + const edgeExecId = ws.data?.edgeExecId; + if (edgeExecId) { + const session = _edgeExecSessions.get(edgeExecId); + if (session) { + const conn = _edgeConnections.get(session.environmentId); + if (conn) { + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input') conn.ws.send(JSON.stringify({ type: 'exec_input', execId: edgeExecId, data: Buffer.from(msg.data).toString('base64') })); + else if (msg.type === 'resize') conn.ws.send(JSON.stringify({ type: 'exec_resize', execId: edgeExecId, cols: msg.cols, rows: msg.rows })); + } catch {} + } + } + return; + } + + // Terminal message + const connId = ws.data?.connId; + if (!connId) return; + const d = dockerStreams.get(connId); + if (!d) return; + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input' && d.stream) d.stream.write(msg.data); + else if (msg.type === 'resize' && d.execId) resizeExec(d.execId, msg.cols, msg.rows, d.target); + } catch { if (d.stream) d.stream.write(message); } + }, + close(ws) { + const connType = ws.data?.type; + + // Hawser Edge disconnection + if (connType === 'hawser') { + const envId = _wsToEnvId.get(ws); + if (envId) { + const conn = _edgeConnections.get(envId); + if (conn) { + console.log('[Hawser] Agent disconnected:', conn.agentId); + // Clear server-side ping interval + if (conn.pingInterval) { clearInterval(conn.pingInterval); conn.pingInterval = null; } + for (const [, p] of conn.pendingRequests) { clearTimeout(p.timeout); p.reject(new Error('Connection closed')); } + for (const [, p] of conn.pendingStreamRequests) { p.onEnd('Connection closed'); } + _edgeConnections.delete(envId); + _updateEnvStatus(envId, null); + } + _wsToEnvId.delete(ws); + } + return; + } + + // Edge exec session close + const edgeExecId = ws.data?.edgeExecId; + if (edgeExecId) { + const session = _edgeExecSessions.get(edgeExecId); + if (session) { + const conn = _edgeConnections.get(session.environmentId); + if (conn) conn.ws.send(JSON.stringify({ type: 'exec_end', execId: edgeExecId, reason: 'user_closed' })); + _edgeExecSessions.delete(edgeExecId); + } + return; + } + + // Terminal close + const connId = ws.data?.connId; + if (!connId) return; + const d = dockerStreams.get(connId); + if (d?.stream) d.stream.end(); + dockerStreams.delete(connId); + } +}; +`; + + const insertPoint = content.indexOf('var path = env('); + if (insertPoint > -1) { + content = content.slice(0, insertPoint) + wsHandler + content.slice(insertPoint); + } + + content = content.replace( + 'var { fetch: handlerFetch, websocket } = getHandler();', + 'var { fetch: handlerFetch, websocket: _ } = getHandler(); var websocket = combinedWebsocket;' + ); + + await Bun.write(indexPath, content); + console.log('✓ Patched index.js'); +} + +console.log('Patching build...'); +await patchHandler(); +await patchIndex(); +console.log('✓ Done'); diff --git a/src/LICENSE.txt b/src/LICENSE.txt new file mode 100644 index 0000000..86472ee --- /dev/null +++ b/src/LICENSE.txt @@ -0,0 +1,128 @@ +Business Source License 1.1 + +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +Licensor: Finsys / Jarek Krochmalski + +Licensed Work: Dockhand + The Licensed Work is (c) 2025-2026 Finsys / Jarek Krochmalski. + +Additional Use Grant: You may use the Licensed Work for any purpose, including + production use, provided that you do not offer the Licensed + Work, or any derivative work of the Licensed Work, to third + parties as a commercial hosted service, managed service, or + software-as-a-service (SaaS) offering where the primary value + proposition to users is Docker container management + functionality substantially similar to the Licensed Work. + + For clarity, the following uses are explicitly permitted + without any restriction: + + (a) Personal use, including home labs and hobby projects + (b) Internal business use within your organization, regardless + of the number of Docker environments managed + (c) Use by non-profit organizations and charitable entities + (d) Educational, academic, and research purposes + (e) Evaluation, testing, development, and demonstration purposes + (f) Embedding or integrating the Licensed Work into internal + tools or platforms that are not offered commercially to + third parties + (g) Use by managed service providers (MSPs) to manage Docker + infrastructure on behalf of their clients, provided the + MSP does not offer Dockhand itself as the service + +Change Date: January 1, 2029 + +Change License: Apache License, Version 2.0 + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License's text to license +your works, and to refer to it using the trademark "Business Source License", +as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License's text and the "Business +Source License" name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where "compatible" means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text "None". + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the "License") is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +----------------------------------------------------------------------------- + +For licensing inquiries, commercial licensing, or enterprise features: + + Website: https://dockhand.io + +----------------------------------------------------------------------------- diff --git a/app.css b/src/app.css similarity index 100% rename from app.css rename to src/app.css diff --git a/app.d.ts b/src/app.d.ts similarity index 100% rename from app.d.ts rename to src/app.d.ts diff --git a/app.html b/src/app.html similarity index 99% rename from app.html rename to src/app.html index 5f6ec02..d96ece1 100644 --- a/app.html +++ b/src/app.html @@ -13,5 +13,3 @@
    %sveltekit.body%
    - - diff --git a/hooks.server.ts b/src/hooks.server.ts similarity index 92% rename from hooks.server.ts rename to src/hooks.server.ts index 0e07423..c5bb24f 100644 --- a/hooks.server.ts +++ b/src/hooks.server.ts @@ -4,6 +4,7 @@ import { startScheduler } from '$lib/server/scheduler'; import { isAuthEnabled, validateSession } from '$lib/server/auth'; import { setServerStartTime } from '$lib/server/uptime'; import { checkLicenseExpiry, getHostname } from '$lib/server/license'; +import { initCryptoFallback } from '$lib/server/crypto-fallback'; import type { HandleServerError, Handle } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; @@ -20,6 +21,9 @@ let initialized = false; if (!initialized) { try { + // Initialize crypto fallback first (detects old kernels and logs status) + initCryptoFallback(); + setServerStartTime(); // Track when server started initDatabase(); // Log hostname for license validation (set by entrypoint in Docker, or os.hostname() outside) @@ -68,11 +72,16 @@ const PUBLIC_PATHS = [ '/api/auth/oidc', '/api/license', '/api/changelog', - '/api/dependencies' + '/api/dependencies', + '/api/health' ]; // Check if path is public function isPublicPath(pathname: string): boolean { + // Webhook endpoints have their own auth (signature/secret verification) + if (pathname.match(/^\/api\/git\/stacks\/\d+\/webhook$/)) return true; + if (pathname.match(/^\/api\/git\/webhook\/\d+$/)) return true; + return PUBLIC_PATHS.some(path => pathname === path || pathname.startsWith(path + '/')); } diff --git a/images/logo.webp b/src/images/logo.webp similarity index 100% rename from images/logo.webp rename to src/images/logo.webp diff --git a/lib/actions/column-resize.ts b/src/lib/actions/column-resize.ts similarity index 100% rename from lib/actions/column-resize.ts rename to src/lib/actions/column-resize.ts diff --git a/lib/assets/favicon.svg b/src/lib/assets/favicon.svg similarity index 100% rename from lib/assets/favicon.svg rename to src/lib/assets/favicon.svg diff --git a/lib/components/AvatarCropper.svelte b/src/lib/components/AvatarCropper.svelte similarity index 100% rename from lib/components/AvatarCropper.svelte rename to src/lib/components/AvatarCropper.svelte diff --git a/lib/components/BatchOperationModal.svelte b/src/lib/components/BatchOperationModal.svelte similarity index 100% rename from lib/components/BatchOperationModal.svelte rename to src/lib/components/BatchOperationModal.svelte diff --git a/lib/components/CodeEditor.svelte b/src/lib/components/CodeEditor.svelte similarity index 84% rename from lib/components/CodeEditor.svelte rename to src/lib/components/CodeEditor.svelte index f49406f..0e76cb9 100644 --- a/lib/components/CodeEditor.svelte +++ b/src/lib/components/CodeEditor.svelte @@ -2,12 +2,54 @@ import { onMount, onDestroy } from 'svelte'; import { EditorState, StateField, StateEffect, RangeSet } from '@codemirror/state'; import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, gutter, GutterMarker, Decoration, WidgetType, type DecorationSet } from '@codemirror/view'; + // Note: Secret masking was removed - secrets are now excluded from the raw editor entirely + // and are only stored in the database (never written to .env file) import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; - import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching } from '@codemirror/language'; + import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching, StreamLanguage, type StreamParser } from '@codemirror/language'; import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete'; import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark'; + // Simple dotenv/env file language parser + const dotenvParser: StreamParser<{ inValue: boolean }> = { + startState() { + return { inValue: false }; + }, + token(stream, state) { + // Start of line + if (stream.sol()) { + state.inValue = false; + // Skip leading whitespace + stream.eatSpace(); + // Comment line + if (stream.peek() === '#') { + stream.skipToEnd(); + return 'comment'; + } + } + // If in value part, consume the rest + if (state.inValue) { + stream.skipToEnd(); + return 'string'; + } + // Variable name before = + if (stream.match(/^[a-zA-Z_][a-zA-Z0-9_]*/)) { + if (stream.peek() === '=') { + return 'variableName.definition'; + } + return 'variableName'; + } + // Equals sign - switch to value mode + if (stream.eat('=')) { + state.inValue = true; + return 'operator'; + } + // Skip anything else + stream.next(); + return null; + } + }; + // Docker Compose keywords for autocomplete const COMPOSE_TOP_LEVEL = ['services', 'networks', 'volumes', 'configs', 'secrets', 'name', 'version']; @@ -172,7 +214,10 @@ variableMarkers?: VariableMarker[]; } - let { value = '', language = 'yaml', readonly = false, theme = 'dark', onchange, class: className = '', variableMarkers = [] }: Props = $props(); + let { value = '', language = 'yaml', readonly = false, theme = 'dark', onchange, class: className = '', variableMarkers: variableMarkersProp = [] }: Props = $props(); + + // Keep markers reactive - destructured props with defaults lose reactivity + const variableMarkers = $derived(variableMarkersProp); let container: HTMLDivElement; let view: EditorView | null = null; @@ -180,6 +225,9 @@ // Mutable ref for callback - allows updating without recreating editor let onchangeRef: ((value: string) => void) | undefined = onchange; + // Flag to suppress onchange during programmatic value sync + let isSyncingExternalValue = false; + // Keep callback ref updated when prop changes $effect(() => { onchangeRef = onchange; @@ -453,6 +501,9 @@ case 'sh': // No dedicated shell/dockerfile support, use basic highlighting return []; + case 'dotenv': + case 'env': + return StreamLanguage.define(dotenvParser); default: return []; } @@ -542,6 +593,13 @@ // Track if we're initialized (prevents multiple createEditor calls) let initialized = false; + // Debounce timer for marker updates (prevents flicker during fast typing) + let markerUpdateTimer: ReturnType | null = null; + const MARKER_UPDATE_DEBOUNCE_MS = 300; + + // Track last applied markers to avoid redundant updates + let lastAppliedMarkersJson = ''; + function createEditor() { if (!container || view || initialized) return; initialized = true; @@ -551,12 +609,14 @@ : [dockhandLight, syntaxHighlighting(defaultHighlightStyle)]; // Build autocompletion config - add Docker Compose completions for YAML + // Note: activateOnTyping can interfere with key repeat, so we disable it + // Users can still trigger autocomplete manually with Ctrl+Space const autocompletionConfig = language === 'yaml' ? autocompletion({ override: [composeCompletions, composeValueCompletions], - activateOnTyping: true + activateOnTyping: false }) - : autocompletion(); + : autocompletion({ activateOnTyping: false }); const extensions = [ lineNumbers(), @@ -594,18 +654,26 @@ extensions }); - // Custom transaction handler - this is SYNCHRONOUS and more reliable than updateListener + // Custom transaction handler - applies transactions synchronously but defers callback // Based on the Svelte Playground pattern: https://svelte.dev/playground/91649ba3e0ce4122b3b34f3a95a00104 const dispatchTransactions = (trs: readonly import('@codemirror/state').Transaction[]) => { if (!view) return; - // Apply all transactions + // Apply all transactions synchronously (required by CodeMirror) view.update(trs); // Check if any transaction changed the document + // Skip onchange during programmatic value sync (only fire for user edits) const lastChangingTr = trs.findLast(tr => tr.docChanged); - if (lastChangingTr && onchangeRef) { - onchangeRef(lastChangingTr.newDoc.toString()); + if (lastChangingTr && onchangeRef && !isSyncingExternalValue) { + // Defer callback to next microtask to avoid blocking input handling + // This allows key repeat to work properly + const newContent = lastChangingTr.newDoc.toString(); + queueMicrotask(() => { + if (onchangeRef) { + onchangeRef(newContent); + } + }); } }; @@ -615,7 +683,6 @@ dispatchTransactions }); - // Push initial markers if provided if (variableMarkers.length > 0) { view.dispatch({ @@ -625,11 +692,16 @@ } function destroyEditor() { + if (markerUpdateTimer) { + clearTimeout(markerUpdateTimer); + markerUpdateTimer = null; + } if (view) { view.destroy(); view = null; } initialized = false; + lastAppliedMarkersJson = ''; } // Get current editor content @@ -656,11 +728,35 @@ } // Update variable markers - this is the key method for parent to call - export function updateVariableMarkers(markers: VariableMarker[]) { - if (view) { - view.dispatch({ - effects: updateMarkersEffect.of(markers) - }); + // Debounced to prevent flicker during fast typing + export function updateVariableMarkers(markers: VariableMarker[], immediate = false) { + if (!view) return; + + // Check if markers actually changed (compare by content, not reference) + const newJson = JSON.stringify(markers); + if (newJson === lastAppliedMarkersJson) { + return; // No change, skip update + } + + // Clear any pending update + if (markerUpdateTimer) { + clearTimeout(markerUpdateTimer); + markerUpdateTimer = null; + } + + const applyUpdate = () => { + if (view) { + lastAppliedMarkersJson = newJson; + view.dispatch({ + effects: updateMarkersEffect.of(markers) + }); + } + }; + + if (immediate) { + applyUpdate(); + } else { + markerUpdateTimer = setTimeout(applyUpdate, MARKER_UPDATE_DEBOUNCE_MS); } } @@ -693,12 +789,29 @@ }); // Update markers when prop changes (backup mechanism, parent should also call updateVariableMarkers) + // Uses the debounced update to prevent flicker during fast typing $effect(() => { const markers = variableMarkers; if (view && markers) { - view.dispatch({ - effects: updateMarkersEffect.of(markers) - }); + updateVariableMarkers(markers); + } + }); + + // Sync external value changes to the editor (e.g., when parent clears the content) + $effect(() => { + const externalValue = value; + if (view) { + const currentContent = view.state.doc.toString(); + // Only update if the external value differs from editor content + // This prevents feedback loops from editor changes + if (externalValue !== currentContent) { + // Suppress onchange during programmatic sync - only user edits should trigger it + isSyncingExternalValue = true; + view.dispatch({ + changes: { from: 0, to: currentContent.length, insert: externalValue } + }); + isSyncingExternalValue = false; + } } }); @@ -706,7 +819,6 @@
    e.stopPropagation()} >
    diff --git a/src/routes/attach/AttachTerminal.svelte b/src/routes/attach/AttachTerminal.svelte new file mode 100644 index 0000000..03519da --- /dev/null +++ b/src/routes/attach/AttachTerminal.svelte @@ -0,0 +1,287 @@ + + +
    + + diff --git a/src/routes/attach/[id]/+page.svelte b/src/routes/attach/[id]/+page.svelte new file mode 100644 index 0000000..b10badb --- /dev/null +++ b/src/routes/attach/[id]/+page.svelte @@ -0,0 +1,340 @@ + + + + Attach - {containerName || 'Loading...'} + + +
    + +
    +
    + + {containerName || containerId} + {#if containerValid} + {#if connected} + + + Attached + + {:else} + Connecting... + {/if} + {:else if validationError} + {validationError} + {:else} + Validating... + {/if} +
    +
    + Attach Mode +
    +
    + + +
    + {#if validationError} +
    +
    + +

    {validationError}

    +

    + Go back to attach page +

    +
    +
    + {:else if xtermLoaded && containerValid} +
    + {:else} +
    + Loading... +
    + {/if} +
    +
    + + diff --git a/routes/audit/+page.svelte b/src/routes/audit/+page.svelte similarity index 100% rename from routes/audit/+page.svelte rename to src/routes/audit/+page.svelte diff --git a/routes/audit/+server.ts b/src/routes/audit/+server.ts similarity index 100% rename from routes/audit/+server.ts rename to src/routes/audit/+server.ts diff --git a/routes/audit/users/+server.ts b/src/routes/audit/users/+server.ts similarity index 100% rename from routes/audit/users/+server.ts rename to src/routes/audit/users/+server.ts diff --git a/routes/containers/+page.svelte b/src/routes/containers/+page.svelte similarity index 86% rename from routes/containers/+page.svelte rename to src/routes/containers/+page.svelte index 8fa394a..8018400 100644 --- a/routes/containers/+page.svelte +++ b/src/routes/containers/+page.svelte @@ -1,5 +1,7 @@ - + @@ -450,7 +501,7 @@ -
    +
    {#if loading}
    @@ -461,7 +512,7 @@
    {:else if containerData} - + showLogs = false}>Overview showLogs = true}>Logs showLogs = false}>Layers @@ -470,6 +521,7 @@ showLogs = false}>Mounts showLogs = false}>Files showLogs = false}>Environment + showLogs = false}>Labels showLogs = false}>Security showLogs = false}>Resources showLogs = false}>Health @@ -642,23 +694,6 @@
    {/if} - - {#if containerData.Config?.Labels && Object.keys(containerData.Config.Labels).length > 0} -
    - - Labels ({Object.keys(containerData.Config.Labels).length}) - -
    - {#each Object.entries(containerData.Config.Labels) as [key, value]} -
    - {key} - = - {value} -
    - {/each} -
    -
    - {/if} @@ -845,8 +880,22 @@ {#each Object.entries(containerData.NetworkSettings.Ports) as [containerPort, hostBindings]} {#if hostBindings && hostBindings.length > 0} {#each hostBindings as binding} + {@const url = getPortUrl(parseInt(binding.HostPort))}
    - {binding.HostIp || '0.0.0.0'}:{binding.HostPort} + {#if url} + + {binding.HostIp || '0.0.0.0'}:{binding.HostPort} + + + {:else} + {binding.HostIp || '0.0.0.0'}:{binding.HostPort} + {/if} {containerPort}
    @@ -944,6 +993,37 @@ {/if} + + + {#if containerData.Config?.Labels && Object.keys(containerData.Config.Labels).length > 0} +
    + {#each Object.entries(containerData.Config.Labels).sort((a, b) => a[0].localeCompare(b[0])) as [key, value]} +
    +
    + {key} + = + {value} +
    + +
    + {/each} +
    + {:else} +

    No labels

    + {/if} +
    + @@ -1187,7 +1267,7 @@ - + diff --git a/src/routes/containers/ContainerSettingsTab.svelte b/src/routes/containers/ContainerSettingsTab.svelte new file mode 100644 index 0000000..90ba559 --- /dev/null +++ b/src/routes/containers/ContainerSettingsTab.svelte @@ -0,0 +1,1268 @@ + + +
    + + {#if mode === 'create' && imageSummary} +
    +
    + +
    +

    Image: {image || 'Not set'}

    + {#if imageSummary.isPulling || imageSummary.isScanning} +

    + + {imageSummary.isScanning ? 'Scanning...' : 'Pulling...'} +

    + {:else if imageSummary.imageReady} +

    + + Image pulled and ready + {#if imageSummary.scanResults && imageSummary.scanResults.length > 0} + • {imageSummary.totalVulnerabilities ?? 0} vulnerabilities + {/if} +

    + {:else if !image} +

    + + Go to "Pull" tab to set the image +

    + {/if} +
    +
    +
    + {/if} + + + {#if configSets.length > 0} +
    +
    + +

    {mode === 'edit' ? 'Apply config set' : 'Config set'}

    +
    +
    +
    + + + {selectedConfigSetId ? configSets.find(c => c.id === parseInt(selectedConfigSetId))?.name : (mode === 'edit' ? 'Select a config set to merge values...' : 'Select a config set to pre-fill values...')} + + + {#each configSets as configSet} + +
    + {configSet.name} + {#if configSet.description} + {configSet.description} + {/if} +
    +
    + {/each} +
    +
    +
    +
    + {#if mode === 'edit'} +

    Note: Values from the config set will be merged with existing settings. Existing keys won't be overwritten.

    + {/if} +
    + {/if} + + +
    +
    +

    Basic settings

    +
    + +
    +
    + + errors.name = undefined} + /> + {#if errors.name} +

    {errors.name}

    + {/if} +
    + {#if mode === 'edit'} +
    + + errors.image = undefined} + /> + {#if errors.image} +

    {errors.image}

    + {/if} +
    + {/if} +
    + +
    + + +
    + +
    +
    + + + + + {#if restartPolicy === 'no'} + + {:else if restartPolicy === 'always'} + + {:else if restartPolicy === 'on-failure'} + + {:else} + + {/if} + {restartPolicy === 'no' ? 'No' : restartPolicy === 'always' ? 'Always' : restartPolicy === 'on-failure' ? 'On failure' : 'Unless stopped'} + + + + + {#snippet children()} + + No + {/snippet} + + + {#snippet children()} + + Always + {/snippet} + + + {#snippet children()} + + On failure + {/snippet} + + + {#snippet children()} + + Unless stopped + {/snippet} + + + + {#if restartPolicy === 'on-failure'} +
    + + +

    Leave empty for unlimited retries

    +
    + {/if} +
    + +
    + + + + + {#if networkMode === 'bridge'} + + {:else if networkMode === 'host'} + + {:else} + + {/if} + {networkMode === 'bridge' ? 'Bridge' : networkMode === 'host' ? 'Host' : 'None'} + + + + + {#snippet children()} + + Bridge + {/snippet} + + + {#snippet children()} + + Host + {/snippet} + + + {#snippet children()} + + None + {/snippet} + + + +
    +
    + +
    + + +
    +
    + + + {#if availableNetworks.length > 0} +
    +
    +
    + +

    Networks

    +
    +
    + +
    + + + Select network to add... + + + {#each availableNetworks.filter(n => !selectedNetworks.includes(n.name) && !['bridge', 'host', 'none'].includes(n.name)) as network} + + {#snippet children()} +
    + {network.name} + {network.driver} +
    + {/snippet} +
    + {/each} +
    +
    + + {#if selectedNetworks.length > 0} +
    + {#each selectedNetworks as networkName} + {@const network = availableNetworks.find(n => n.name === networkName)} + + {networkName} + {#if network} + {network.driver} + {/if} + + + {/each} +
    + {/if} + {#if mode === 'edit'} +

    Container will be connected to selected networks in addition to the network mode above

    + {/if} +
    +
    + {/if} + + +
    +
    +

    Port mappings

    + +
    + +
    + {#each portMappings as mapping, index} +
    +
    + Host + +
    +
    + Container + +
    + { portMappings[index].protocol = v; }} + /> + +
    + {/each} +
    +
    + + +
    +
    +

    Volume mappings

    + +
    + +
    + {#each volumeMappings as mapping, index} +
    +
    + Host path + +
    +
    + Container path + +
    + { volumeMappings[index].mode = v; }} + /> + +
    + {/each} +
    +
    + + +
    +
    +

    Environment variables

    + +
    + +
    + {#each envVars as envVar, index} +
    +
    + Key + +
    +
    + Value + +
    + +
    + {/each} +
    +
    + + +
    +
    +

    Labels

    + +
    + +
    + {#each labels as label, index} +
    +
    + Key + +
    +
    + Value + +
    + +
    + {/each} +
    +
    + + +
    +

    Advanced container options (click to expand)

    +
    + + +
    + + {#if showResources} +
    +

    Configure memory and CPU limits for this container

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

    Microseconds per period

    +
    +
    + + +

    Period in microseconds

    +
    +
    +
    + {/if} +
    + + +
    + + {#if showSecurity} +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + +
    + + { addCapability('add', v); }}> + + Select capability to add... + + + {#each commonCapabilities.filter(c => !capAdd.includes(c)) as cap} + + {/each} + + + {#if capAdd.length > 0} +
    + {#each capAdd as cap} + + +{cap} + + + {/each} +
    + {/if} +
    + +
    + + { addCapability('drop', v); }}> + + Select capability to drop... + + + {#each commonCapabilities.filter(c => !capDrop.includes(c)) as cap} + + {/each} + + + {#if capDrop.length > 0} +
    + {#each capDrop as cap} + + -{cap} + + + {/each} +
    + {/if} +
    + +
    + +
    + { if (e.key === 'Enter') { e.preventDefault(); addSecurityOption(); } }} + /> + +
    + {#if securityOptions.length > 0} +
    + {#each securityOptions as option} + + {option} + + + {/each} +
    + {/if} +

    Common options: no-new-privileges, seccomp=unconfined, apparmor=unconfined

    +
    +
    + {/if} +
    + + +
    + + {#if showHealth} +
    +
    + + +
    + {#if healthcheckEnabled} +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + {/if} +
    + {/if} +
    + + +
    + + {#if showDns} +
    +
    + +
    + { if (e.key === 'Enter') { e.preventDefault(); addDnsServer(); } }} + /> + +
    + {#if dnsServers.length > 0} +
    + {#each dnsServers as server} + + {server} + + + {/each} +
    + {/if} +
    + + +
    + +
    + { if (e.key === 'Enter') { e.preventDefault(); addDnsSearch(); } }} + /> + +
    + {#if dnsSearch.length > 0} +
    + {#each dnsSearch as domain} + + {domain} + + + {/each} +
    + {/if} +
    + + +
    + +
    + { if (e.key === 'Enter') { e.preventDefault(); addDnsOption(); } }} + /> + +
    + {#if dnsOptions.length > 0} +
    + {#each dnsOptions as option} + + {option} + + + {/each} +
    + {/if} +
    +
    + {/if} +
    + + +
    + + {#if showDevices} +
    +
    + +
    + {#each deviceMappings as mapping, index} +
    + + + +
    + {/each} +
    + {/if} +
    + + +
    + + {#if showUlimits} +
    +
    + +
    + {#each ulimits as ulimit, index} +
    + + + {ulimit.name} + + + {#each commonUlimits as name} + + {/each} + + + + + +
    + {/each} +
    + {/if} +
    + + +
    +
    + +

    Auto-update

    +
    + +
    +
    diff --git a/routes/containers/ContainerTerminal.svelte b/src/routes/containers/ContainerTerminal.svelte similarity index 100% rename from routes/containers/ContainerTerminal.svelte rename to src/routes/containers/ContainerTerminal.svelte diff --git a/routes/containers/ContainerTile.svelte b/src/routes/containers/ContainerTile.svelte similarity index 100% rename from routes/containers/ContainerTile.svelte rename to src/routes/containers/ContainerTile.svelte diff --git a/src/routes/containers/CreateContainerModal.svelte b/src/routes/containers/CreateContainerModal.svelte new file mode 100644 index 0000000..71893e8 --- /dev/null +++ b/src/routes/containers/CreateContainerModal.svelte @@ -0,0 +1,755 @@ + + + isOpen && focusFirstInput()}> + + + Create new container + + + + + {#if !skipPullTab} +
    + + + {#if envHasScanning} + + + + {/if} + + + +
    + {/if} + + + +
    + image = newImage} + /> +
    + + +
    + {#if envHasScanning} + + {:else} + +
    +
    + +

    Vulnerability scanning is disabled for this environment.

    +

    Enable it in Settings -> Environments to scan images.

    +
    +
    + {/if} +
    + + +
    + +
    + +
    +
    + {#if activeTab === 'container' && hasCriticalOrHigh} +
    + + Critical/high vulnerabilities found in image +
    + {/if} +
    +
    + + +
    +
    +
    +
    diff --git a/src/routes/containers/EditContainerModal.svelte b/src/routes/containers/EditContainerModal.svelte new file mode 100644 index 0000000..d5a28fa --- /dev/null +++ b/src/routes/containers/EditContainerModal.svelte @@ -0,0 +1,1040 @@ + + + isOpen && focusFirstInput()}> + + + + Edit container + {#if isEditingTitle} + - + { + if (e.key === 'Enter') saveEditingTitle(); + if (e.key === 'Escape') cancelEditingTitle(); + }} + /> + + + {:else if name} + - {name} + + {/if} + + + + {#if loadingData} +
    + + Loading container data... +
    + {:else} +
    + + {#if showComposeRenameWarning} +
    + + This container is part of the {composeStackName} compose stack. Renaming it may cause issues with stack management. +
    + {/if} + {#if showComposeConfigWarning} +
    + + This container is part of the {composeStackName} compose stack. Changes may be overwritten when the stack is redeployed. +
    + {/if} + + + + {#if statusMessage} +
    + {statusMessage} +
    + {/if} + + {#if error} +
    + {error} +
    + {/if} +
    + +
    + + +
    + {/if} +
    +
    diff --git a/routes/containers/FileBrowserModal.svelte b/src/routes/containers/FileBrowserModal.svelte similarity index 93% rename from routes/containers/FileBrowserModal.svelte rename to src/routes/containers/FileBrowserModal.svelte index bb0253d..872d9a9 100644 --- a/routes/containers/FileBrowserModal.svelte +++ b/src/routes/containers/FileBrowserModal.svelte @@ -22,7 +22,7 @@ - + diff --git a/routes/containers/FileBrowserPanel.svelte b/src/routes/containers/FileBrowserPanel.svelte similarity index 96% rename from routes/containers/FileBrowserPanel.svelte rename to src/routes/containers/FileBrowserPanel.svelte index 935deba..d6e87a6 100644 --- a/routes/containers/FileBrowserPanel.svelte +++ b/src/routes/containers/FileBrowserPanel.svelte @@ -69,9 +69,25 @@ initialPath?: string; canEdit?: boolean; onUsageChange?: (usage: VolumeUsageInfo[], isInUse: boolean) => void; + // File selection mode - when true, clicking a file selects it instead of opening viewer + selectMode?: boolean; + // Regex to filter which files can be selected (used with selectMode) + selectFilter?: RegExp; + // Callback when a file is selected (in selectMode) + onFileSelect?: (path: string, name: string) => void; } - let { containerId, volumeName, envId, initialPath = '/', canEdit = true, onUsageChange }: Props = $props(); + let { + containerId, + volumeName, + envId, + initialPath = '/', + canEdit = true, + onUsageChange, + selectMode = false, + selectFilter, + onFileSelect + }: Props = $props(); // For volume mode, track whether volume is in use (controls editing ability) let volumeIsInUse = $state(false); @@ -110,6 +126,15 @@ // Track if this container uses busybox (doesn't support --time-style=iso) let useSimpleLs = $state(false); + // Selection mode state + let selectedFilePath = $state(null); + + // Check if a file matches the select filter (for highlighting) + function matchesSelectFilter(name: string): boolean { + if (!selectMode || !selectFilter) return false; + return selectFilter.test(name); + } + // Sort state let sortField = $state('name'); let sortDirection = $state('asc'); @@ -696,6 +721,11 @@ if (entry.type === 'directory') { const newPath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`; navigateTo(newPath); + } else if (selectMode && entry.type === 'file') { + // In select mode, clicking a file selects it + const filePath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`; + selectedFilePath = filePath; + onFileSelect?.(filePath, entry.name); } // Symlinks are not navigable - target path is displayed for reference } @@ -924,8 +954,11 @@ {#each displayEntries() as entry (entry.name)} {@const Icon = getIcon(entry)} - {@const isClickable = entry.type === 'directory'} - + {@const isClickable = entry.type === 'directory' || (selectMode && entry.type === 'file')} + {@const isSelectable = selectMode && entry.type === 'file' && matchesSelectFilter(entry.name)} + {@const filePath = currentPath === '/' ? `/${entry.name}` : `${currentPath}/${entry.name}`} + {@const isSelected = selectMode && selectedFilePath === filePath} +
    @@ -720,6 +783,25 @@ {/if} + {:else if column.sortable} + + {:else if column.id !== 'expand' && column.id !== 'actions'} + {column.label} {/if} {/snippet} {#snippet cell(column, group, rowState)} @@ -766,6 +848,16 @@ {group.tags[0].tag} {/if} + {#if group.containers === 0} + + Unused + + {:else if group.tags.length > 1 && group.tags.some(t => t.containers === 0)} + + + Some unused + + {/if} {:else if column.id === 'tags'} @@ -848,6 +940,22 @@ {formatSize(tagInfo.size)} {:else if column.id === 'created'} {formatImageDate(tagInfo.created)} + {:else if column.id === 'used'} + {#if tagInfo.containers > 0} + + {tagInfo.containers} container{tagInfo.containers === 1 ? '' : 's'} + + {:else if tagInfo.containers === 0} + + Unused + + {:else} + + {/if} {:else if column.id === 'actions'}
    {#if $canAccess('images', 'inspect')} @@ -911,7 +1019,7 @@ {/if} - {#if $canAccess('images', 'remove')} + {#if $canAccess('images', 'remove') && tagInfo.containers === 0}
    {@render children()} - +
    diff --git a/routes/images/ImageScanModal.svelte b/src/routes/images/ImageScanModal.svelte similarity index 100% rename from routes/images/ImageScanModal.svelte rename to src/routes/images/ImageScanModal.svelte diff --git a/routes/images/PushToRegistryModal.svelte b/src/routes/images/PushToRegistryModal.svelte similarity index 100% rename from routes/images/PushToRegistryModal.svelte rename to src/routes/images/PushToRegistryModal.svelte diff --git a/routes/images/ScanResultsView.svelte b/src/routes/images/ScanResultsView.svelte similarity index 100% rename from routes/images/ScanResultsView.svelte rename to src/routes/images/ScanResultsView.svelte diff --git a/routes/images/VulnerabilityScanModal.svelte b/src/routes/images/VulnerabilityScanModal.svelte similarity index 100% rename from routes/images/VulnerabilityScanModal.svelte rename to src/routes/images/VulnerabilityScanModal.svelte diff --git a/routes/login/+page.svelte b/src/routes/login/+page.svelte similarity index 98% rename from routes/login/+page.svelte rename to src/routes/login/+page.svelte index 039aafb..7a84821 100644 --- a/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -270,17 +270,14 @@

    - Enter the code from your authenticator app + Enter the 6-digit code from your authenticator app, or use a backup code

    {/if} diff --git a/routes/logs/+page.svelte b/src/routes/logs/+page.svelte similarity index 99% rename from routes/logs/+page.svelte rename to src/routes/logs/+page.svelte index bdfe0c6..f32fad9 100644 --- a/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -294,7 +294,8 @@ import type { FavoriteGroup } from '../api/preferences/favorite-groups/+server'; } if (urlContainerId) { - // Single container from URL + // Single container from URL - always switch to single mode + layoutMode = 'single'; const container = fetchedContainers.find(c => c.id === urlContainerId || c.id.startsWith(urlContainerId)); if (container) { selectContainer(container); diff --git a/routes/logs/LogViewer.svelte b/src/routes/logs/LogViewer.svelte similarity index 100% rename from routes/logs/LogViewer.svelte rename to src/routes/logs/LogViewer.svelte diff --git a/routes/logs/LogsPanel.svelte b/src/routes/logs/LogsPanel.svelte similarity index 100% rename from routes/logs/LogsPanel.svelte rename to src/routes/logs/LogsPanel.svelte diff --git a/routes/networks/+page.svelte b/src/routes/networks/+page.svelte similarity index 100% rename from routes/networks/+page.svelte rename to src/routes/networks/+page.svelte diff --git a/routes/networks/ConnectContainerModal.svelte b/src/routes/networks/ConnectContainerModal.svelte similarity index 100% rename from routes/networks/ConnectContainerModal.svelte rename to src/routes/networks/ConnectContainerModal.svelte diff --git a/routes/networks/CreateNetworkModal.svelte b/src/routes/networks/CreateNetworkModal.svelte similarity index 99% rename from routes/networks/CreateNetworkModal.svelte rename to src/routes/networks/CreateNetworkModal.svelte index 5a108b5..fa28bb8 100644 --- a/routes/networks/CreateNetworkModal.svelte +++ b/src/routes/networks/CreateNetworkModal.svelte @@ -277,7 +277,7 @@ -
    +
    diff --git a/routes/networks/NetworkInspectModal.svelte b/src/routes/networks/NetworkInspectModal.svelte similarity index 99% rename from routes/networks/NetworkInspectModal.svelte rename to src/routes/networks/NetworkInspectModal.svelte index 5c9eb9c..983599d 100644 --- a/routes/networks/NetworkInspectModal.svelte +++ b/src/routes/networks/NetworkInspectModal.svelte @@ -61,7 +61,7 @@ - + diff --git a/routes/profile/+page.svelte b/src/routes/profile/+page.svelte similarity index 96% rename from routes/profile/+page.svelte rename to src/routes/profile/+page.svelte index 1ab3b21..50b22aa 100644 --- a/routes/profile/+page.svelte +++ b/src/routes/profile/+page.svelte @@ -26,7 +26,6 @@ } from 'lucide-svelte'; import { authStore } from '$lib/stores/auth'; import * as Alert from '$lib/components/ui/alert'; - import { licenseStore } from '$lib/stores/license'; import AvatarCropper from '$lib/components/AvatarCropper.svelte'; import * as Avatar from '$lib/components/ui/avatar'; import ChangePasswordModal from './ChangePasswordModal.svelte'; @@ -542,26 +541,19 @@

    - {#if $licenseStore.isEnterprise} - {#if profile.mfaEnabled} - - {:else} - - {/if} + {#if profile.mfaEnabled} + {:else} - - - Enterprise - + {/if}
    {:else} diff --git a/routes/profile/ChangePasswordModal.svelte b/src/routes/profile/ChangePasswordModal.svelte similarity index 100% rename from routes/profile/ChangePasswordModal.svelte rename to src/routes/profile/ChangePasswordModal.svelte diff --git a/routes/profile/DisableMfaModal.svelte b/src/routes/profile/DisableMfaModal.svelte similarity index 100% rename from routes/profile/DisableMfaModal.svelte rename to src/routes/profile/DisableMfaModal.svelte diff --git a/src/routes/profile/MfaSetupModal.svelte b/src/routes/profile/MfaSetupModal.svelte new file mode 100644 index 0000000..2241188 --- /dev/null +++ b/src/routes/profile/MfaSetupModal.svelte @@ -0,0 +1,206 @@ + + + { if (o) { resetForm(); focusFirstInput(); } else if (!showBackupCodes) onClose(); }}> + + + + {#if showBackupCodes} + + MFA enabled successfully + {:else} + + Setup two-factor authentication + {/if} + + + + {#if showBackupCodes} + +
    + + + + Save these backup codes in a safe place. Each code can only be used once to sign in if you lose access to your authenticator app. + + + +
    + {#each backupCodes as code, i} +
    + {i + 1}. + {code} +
    + {/each} +
    + +
    + + +
    +
    + + + + {:else} + +
    + {#if error} + + + {error} + + {/if} + +

    + Scan this QR code with your authenticator app (Google Authenticator, Authy, etc.) +

    + + {#if qrCode} +
    + MFA QR Code +
    + {/if} + +
    + + {secret} +
    + +
    + + +

    + Enter the code from your authenticator app to verify setup +

    +
    +
    + + + + + {/if} +
    +
    diff --git a/routes/registry/+page.svelte b/src/routes/registry/+page.svelte similarity index 85% rename from routes/registry/+page.svelte rename to src/routes/registry/+page.svelte index 65b7add..a7b5404 100644 --- a/routes/registry/+page.svelte +++ b/src/routes/registry/+page.svelte @@ -43,8 +43,13 @@ interface ExpandedImageState { loading: boolean; + loadingMore: boolean; error: string; tags: TagInfo[]; + total: number; + page: number; + pageSize: number; + hasNext: boolean; } let registries = $state([]); @@ -226,38 +231,100 @@ const { [imageName]: _, ...rest } = expandedImages; expandedImages = rest; } else { - // Expand and fetch tags - expandedImages = { - ...expandedImages, - [imageName]: { loading: true, error: '', tags: [] } - }; + // Expand and fetch first page + await fetchTagsPage(imageName, 1, true); + } + } - try { - let url = `/api/registry/tags?image=${encodeURIComponent(imageName)}`; - if (selectedRegistryId) { - url += `®istry=${selectedRegistryId}`; - } + async function loadMoreTags(imageName: string) { + const state = expandedImages[imageName]; + if (!state || state.loading || state.loadingMore || !state.hasNext) return; + await fetchTagsPage(imageName, state.page + 1, false); + } - const response = await fetch(url); - if (response.ok) { - const tags = await response.json(); - expandedImages = { - ...expandedImages, - [imageName]: { loading: false, error: '', tags } - }; - } else { - const data = await response.json(); - expandedImages = { - ...expandedImages, - [imageName]: { loading: false, error: data.error || 'Failed to fetch tags', tags: [] } - }; - } - } catch (error: any) { + async function fetchTagsPage(imageName: string, page: number, isFirstLoad: boolean) { + const currentState = expandedImages[imageName]; + + expandedImages = { + ...expandedImages, + [imageName]: { + loading: isFirstLoad, + loadingMore: !isFirstLoad, + error: '', + tags: currentState?.tags || [], + total: currentState?.total || 0, + page: currentState?.page || 0, + pageSize: 20, + hasNext: currentState?.hasNext || false + } + }; + + try { + let url = `/api/registry/tags?image=${encodeURIComponent(imageName)}&page=${page}&pageSize=20`; + if (selectedRegistryId) { + url += `®istry=${selectedRegistryId}`; + } + + const response = await fetch(url); + if (response.ok) { + const data = await response.json(); + const prevState = expandedImages[imageName]; + const existingTags = isFirstLoad ? [] : (prevState?.tags || []); expandedImages = { ...expandedImages, - [imageName]: { loading: false, error: error.message || 'Failed to fetch tags', tags: [] } + [imageName]: { + loading: false, + loadingMore: false, + error: '', + tags: [...existingTags, ...data.tags], + total: data.total, + page: data.page, + pageSize: data.pageSize, + hasNext: data.hasNext + } + }; + } else { + const data = await response.json(); + expandedImages = { + ...expandedImages, + [imageName]: { + ...expandedImages[imageName], + loading: false, + loadingMore: false, + error: data.error || 'Failed to fetch tags' + } }; } + } catch (error: any) { + expandedImages = { + ...expandedImages, + [imageName]: { + ...expandedImages[imageName], + loading: false, + loadingMore: false, + error: error.message || 'Failed to fetch tags' + } + }; + } + } + + function handleTagsWheel(event: WheelEvent, imageName: string) { + const target = event.currentTarget as HTMLElement; + + // Prevent page scroll when at top/bottom of tags list + const atTop = target.scrollTop === 0; + const atBottom = target.scrollHeight - target.scrollTop - target.clientHeight < 1; + + if ((atTop && event.deltaY < 0) || (atBottom && event.deltaY > 0)) { + event.preventDefault(); + } + + // Load more when near bottom + const state = expandedImages[imageName]; + if (!state || !state.hasNext || state.loading || state.loadingMore) return; + + if (target.scrollHeight - target.scrollTop - target.clientHeight < 50) { + loadMoreTags(imageName); } } @@ -328,7 +395,8 @@ ...expandedImages, [imageName]: { ...state, - tags: state.tags.filter(t => t.name !== tag) + tags: state.tags.filter(t => t.name !== tag), + total: Math.max(0, state.total - 1) } }; } @@ -514,9 +582,9 @@ {expandState.error}
    {:else if expandState?.tags && expandState.tags.length > 0} -
    +
    handleTagsWheel(e, result.name)}> - + @@ -590,7 +658,20 @@ {/each}
    Tag Size
    + + {#if expandState.loadingMore} +
    + + Loading more... +
    + {/if}
    + + {#if expandState.total > 0} +
    + {expandState.tags.length} of {expandState.total} tags loaded +
    + {/if} {:else}
    No tags found diff --git a/routes/registry/CopyToRegistryModal.svelte b/src/routes/registry/CopyToRegistryModal.svelte similarity index 100% rename from routes/registry/CopyToRegistryModal.svelte rename to src/routes/registry/CopyToRegistryModal.svelte diff --git a/routes/registry/ImagePullModal.svelte b/src/routes/registry/ImagePullModal.svelte similarity index 100% rename from routes/registry/ImagePullModal.svelte rename to src/routes/registry/ImagePullModal.svelte diff --git a/routes/schedules/+page.svelte b/src/routes/schedules/+page.svelte similarity index 100% rename from routes/schedules/+page.svelte rename to src/routes/schedules/+page.svelte diff --git a/routes/settings/+page.svelte b/src/routes/settings/+page.svelte similarity index 100% rename from routes/settings/+page.svelte rename to src/routes/settings/+page.svelte diff --git a/routes/settings/about/AboutTab.svelte b/src/routes/settings/about/AboutTab.svelte similarity index 98% rename from routes/settings/about/AboutTab.svelte rename to src/routes/settings/about/AboutTab.svelte index 802e389..c2547b5 100644 --- a/routes/settings/about/AboutTab.svelte +++ b/src/routes/settings/about/AboutTab.svelte @@ -3,7 +3,7 @@ import * as Card from '$lib/components/ui/card'; import { Badge } from '$lib/components/ui/badge'; import { Input } from '$lib/components/ui/input'; - import { Box, Images, HardDrive, Network, Cpu, Server, Crown, Building2, Layers, Clock, Code, Package, ExternalLink, Search, FileText, Tag, Sparkles, Bug, ChevronDown, ChevronRight, Plug, ScrollText, Shield, MessageSquarePlus, GitBranch, Coffee } from 'lucide-svelte'; + import { Box, Images, HardDrive, Network, Cpu, Server, Crown, Building2, Layers, Clock, Code, Package, ExternalLink, Search, FileText, Tag, Sparkles, Bug, ChevronDown, ChevronRight, Plug, ScrollText, Shield, MessageSquarePlus, GitBranch, Coffee, Monitor, Cog, MemoryStick, Database } from 'lucide-svelte'; import * as Tabs from '$lib/components/ui/tabs'; import { onMount, onDestroy } from 'svelte'; import { licenseStore } from '$lib/stores/license'; @@ -198,6 +198,7 @@ nodeVersion: string; platform: string; arch: string; + kernel: string; memory: { heapUsed: number; heapTotal: number; @@ -438,7 +439,7 @@
    {/if} {#if serverUptime !== null} -
    +
    Uptime {formatUptime(serverUptime)}
    @@ -558,10 +559,13 @@ {/if} | - Platform + {systemInfo.runtime.platform}/{systemInfo.runtime.arch} | - Memory + + {systemInfo.runtime.kernel} + | + {formatBytes(systemInfo.runtime.memory.rss)}
    {#if systemInfo.runtime.container.inContainer} @@ -585,7 +589,7 @@
    - + Database
    diff --git a/routes/settings/about/LicenseModal.svelte b/src/routes/settings/about/LicenseModal.svelte similarity index 100% rename from routes/settings/about/LicenseModal.svelte rename to src/routes/settings/about/LicenseModal.svelte diff --git a/routes/settings/about/PrivacyModal.svelte b/src/routes/settings/about/PrivacyModal.svelte similarity index 100% rename from routes/settings/about/PrivacyModal.svelte rename to src/routes/settings/about/PrivacyModal.svelte diff --git a/routes/settings/auth/AuthTab.svelte b/src/routes/settings/auth/AuthTab.svelte similarity index 100% rename from routes/settings/auth/AuthTab.svelte rename to src/routes/settings/auth/AuthTab.svelte diff --git a/routes/settings/auth/ldap/LdapModal.svelte b/src/routes/settings/auth/ldap/LdapModal.svelte similarity index 100% rename from routes/settings/auth/ldap/LdapModal.svelte rename to src/routes/settings/auth/ldap/LdapModal.svelte diff --git a/routes/settings/auth/ldap/LdapSubTab.svelte b/src/routes/settings/auth/ldap/LdapSubTab.svelte similarity index 100% rename from routes/settings/auth/ldap/LdapSubTab.svelte rename to src/routes/settings/auth/ldap/LdapSubTab.svelte diff --git a/routes/settings/auth/oidc/OidcModal.svelte b/src/routes/settings/auth/oidc/OidcModal.svelte similarity index 100% rename from routes/settings/auth/oidc/OidcModal.svelte rename to src/routes/settings/auth/oidc/OidcModal.svelte diff --git a/routes/settings/auth/oidc/SsoSubTab.svelte b/src/routes/settings/auth/oidc/SsoSubTab.svelte similarity index 100% rename from routes/settings/auth/oidc/SsoSubTab.svelte rename to src/routes/settings/auth/oidc/SsoSubTab.svelte diff --git a/routes/settings/auth/roles/RoleModal.svelte b/src/routes/settings/auth/roles/RoleModal.svelte similarity index 100% rename from routes/settings/auth/roles/RoleModal.svelte rename to src/routes/settings/auth/roles/RoleModal.svelte diff --git a/routes/settings/auth/roles/RolesSubTab.svelte b/src/routes/settings/auth/roles/RolesSubTab.svelte similarity index 100% rename from routes/settings/auth/roles/RolesSubTab.svelte rename to src/routes/settings/auth/roles/RolesSubTab.svelte diff --git a/routes/settings/auth/users/UserModal.svelte b/src/routes/settings/auth/users/UserModal.svelte similarity index 99% rename from routes/settings/auth/users/UserModal.svelte rename to src/routes/settings/auth/users/UserModal.svelte index 3868419..f8d96a4 100644 --- a/routes/settings/auth/users/UserModal.svelte +++ b/src/routes/settings/auth/users/UserModal.svelte @@ -235,7 +235,7 @@ toast.success('User created'); } else { const data = await response.json(); - formError = data.error || 'Failed to create user'; + formError = data.details ? `${data.error}: ${data.details}` : (data.error || 'Failed to create user'); toast.error(formError); } } catch { diff --git a/routes/settings/auth/users/UsersSubTab.svelte b/src/routes/settings/auth/users/UsersSubTab.svelte similarity index 100% rename from routes/settings/auth/users/UsersSubTab.svelte rename to src/routes/settings/auth/users/UsersSubTab.svelte diff --git a/routes/settings/config-sets/ConfigSetModal.svelte b/src/routes/settings/config-sets/ConfigSetModal.svelte similarity index 100% rename from routes/settings/config-sets/ConfigSetModal.svelte rename to src/routes/settings/config-sets/ConfigSetModal.svelte diff --git a/routes/settings/config-sets/ConfigSetsTab.svelte b/src/routes/settings/config-sets/ConfigSetsTab.svelte similarity index 100% rename from routes/settings/config-sets/ConfigSetsTab.svelte rename to src/routes/settings/config-sets/ConfigSetsTab.svelte diff --git a/routes/settings/environments/EnvironmentModal.svelte b/src/routes/settings/environments/EnvironmentModal.svelte similarity index 98% rename from routes/settings/environments/EnvironmentModal.svelte rename to src/routes/settings/environments/EnvironmentModal.svelte index b04f2f7..c0ccfd7 100644 --- a/routes/settings/environments/EnvironmentModal.svelte +++ b/src/routes/settings/environments/EnvironmentModal.svelte @@ -415,6 +415,10 @@ newLabelInput = ''; formPublicIp = environment.publicIp || ''; modalTab = 'general'; + // Reset token state for this environment (important when switching between envs) + hawserToken = null; + generatedToken = null; + pendingToken = null; // Load scanner settings, notifications, update check settings, and timezone loadScannerSettings(environment.id); loadEnvNotifications(environment.id); @@ -825,6 +829,11 @@ } } + // Reload only availability/versions without overwriting user's unsaved settings changes + async function reloadScannerAvailability(envId?: number) { + await loadScannerVersionsAsync(envId); + } + async function saveScannerSettings(envId?: number) { try { const response = await fetch('/api/settings/scanner', { @@ -930,7 +939,8 @@ checkingGrypeUpdate = true; grypeUpdateStatus = 'idle'; try { - const response = await fetch('/api/settings/scanner?checkUpdates=true'); + const envParam = environment?.id ? `&env=${environment.id}` : ''; + const response = await fetch(`/api/settings/scanner?checkUpdates=true${envParam}`); const data = await response.json(); if (data.updates) { grypeUpdateStatus = data.updates.grype?.hasUpdate ? 'update-available' : 'up-to-date'; @@ -947,7 +957,8 @@ checkingTrivyUpdate = true; trivyUpdateStatus = 'idle'; try { - const response = await fetch('/api/settings/scanner?checkUpdates=true'); + const envParam = environment?.id ? `&env=${environment.id}` : ''; + const response = await fetch(`/api/settings/scanner?checkUpdates=true${envParam}`); const data = await response.json(); if (data.updates) { trivyUpdateStatus = data.updates.trivy?.hasUpdate ? 'update-available' : 'up-to-date'; @@ -1198,7 +1209,7 @@ { if (o) focusFirstInput(); else onClose(); }}> - + {#if !isEditing} @@ -1362,7 +1373,7 @@ - +
    @@ -2112,7 +2123,7 @@ {/if} {#if !loadingScannerVersions} {#if !scannerAvailability.grype} - loadScannerSettings(environment?.id)}> + reloadScannerAvailability(environment?.id)}> +
    + {/if} + {/if} +
    + + +
    + + {#if result.discovered.length > 0} +
    +
    +

    + + Available for adoption +

    + +
    +
    + {#each result.discovered as stack} + {@const stackEnvId = getStackEnvId(stack.composePath)} + {@const stackEnv = stackEnvId ? environments.find(e => e.id === stackEnvId) : null} +
    + + + + {#if isSelected(stack.composePath) && environments.length > 1} +
    e.stopPropagation()}> + + {#if stack.runningOn && stack.runningOn.length > 0 && !stack.runningOn.some(r => r.envId === stackEnvId)} + + Running elsewhere + + + + + +

    This stack is running on a different environment ({stack.runningOn?.map(r => r.envName).join(', ')}). You can still adopt it here, but it won't affect the running containers.

    +
    +
    +
    + {/if} + { + if (v) setStackEnv(stack.composePath, parseInt(v)); + }} + > + + {#if getStackEnv(stack.composePath)} + {@const StackIcon = getIconComponent(getStackEnv(stack.composePath)?.icon || 'globe')} + + {/if} + {getStackEnv(stack.composePath)?.name || 'Select'} + + + {#each environments as env} + {@const EnvIcon = getIconComponent(env.icon || 'globe')} + +
    + + {env.name} +
    +
    + {/each} +
    +
    +
    + {/if} +
    + {/each} +
    +
    + {/if} + + + {#if adoptedStacks.length > 0} +
    +

    + + Adopted stacks +

    +
    + {#each adoptedStacks as adopted} + {@const env = environments.find(e => e.id === adopted.envId)} + {@const EnvIcon = env ? getIconComponent(env.icon || 'globe') : null} +
    + +

    {adopted.name}

    + {#if env} +
    + {#if EnvIcon} + + {/if} + {env.name} +
    + {/if} +
    + {/each} +
    +
    + {/if} + + + {#if result.skipped.length > 0} +
    +

    + + Already adopted +

    +
    + {#each result.skipped as stack} +
    + +
    +

    {stack.name}

    + {stack.composePath} +
    +
    + {/each} +
    +
    + {/if} + + + {#if result.errors.length > 0} +
    +

    + + Errors +

    +
    + {#each result.errors as error} +
    + +
    + {error.path} +

    {error.error}

    +
    +
    + {/each} +
    +
    + {/if} + + + {#if result.discovered.length === 0 && result.skipped.length === 0 && result.adopted.length === 0 && result.errors.length === 0} +
    + +

    No Docker Compose files found in the configured paths.

    +

    Make sure your paths contain docker-compose.yml, compose.yml, or similar files.

    +
    + {/if} + + +
    +

    Scanned paths

    +
    + {#each scannedPaths as path} + {path} + {/each} +
    +
    +
    + {/if} + + +
    + {#if selectedCount > 0} + {selectedCount} stack{selectedCount !== 1 ? 's' : ''} selected + {/if} +
    +
    + + {#if result && result.discovered.length > 0} + + {:else if adoptedStacks.length > 0} + + {/if} +
    +
    +
    +
    diff --git a/routes/settings/git/GitCredentialModal.svelte b/src/routes/settings/git/GitCredentialModal.svelte similarity index 100% rename from routes/settings/git/GitCredentialModal.svelte rename to src/routes/settings/git/GitCredentialModal.svelte diff --git a/routes/settings/git/GitCredentialsTab.svelte b/src/routes/settings/git/GitCredentialsTab.svelte similarity index 100% rename from routes/settings/git/GitCredentialsTab.svelte rename to src/routes/settings/git/GitCredentialsTab.svelte diff --git a/routes/settings/git/GitRepositoriesTab.svelte b/src/routes/settings/git/GitRepositoriesTab.svelte similarity index 100% rename from routes/settings/git/GitRepositoriesTab.svelte rename to src/routes/settings/git/GitRepositoriesTab.svelte diff --git a/routes/settings/git/GitRepositoryModal.svelte b/src/routes/settings/git/GitRepositoryModal.svelte similarity index 100% rename from routes/settings/git/GitRepositoryModal.svelte rename to src/routes/settings/git/GitRepositoryModal.svelte diff --git a/routes/settings/git/GitTab.svelte b/src/routes/settings/git/GitTab.svelte similarity index 100% rename from routes/settings/git/GitTab.svelte rename to src/routes/settings/git/GitTab.svelte diff --git a/routes/settings/license/LicenseTab.svelte b/src/routes/settings/license/LicenseTab.svelte similarity index 100% rename from routes/settings/license/LicenseTab.svelte rename to src/routes/settings/license/LicenseTab.svelte diff --git a/routes/settings/notifications/NotificationModal.svelte b/src/routes/settings/notifications/NotificationModal.svelte similarity index 91% rename from routes/settings/notifications/NotificationModal.svelte rename to src/routes/settings/notifications/NotificationModal.svelte index a619064..b6ece14 100644 --- a/routes/settings/notifications/NotificationModal.svelte +++ b/src/routes/settings/notifications/NotificationModal.svelte @@ -7,7 +7,8 @@ import { Badge } from '$lib/components/ui/badge'; import { TogglePill } from '$lib/components/ui/toggle-pill'; import { Checkbox } from '$lib/components/ui/checkbox'; - import { Plus, Check, RefreshCw, Mail, Zap, Info, Send, CheckCircle2, XCircle, Key, ChevronDown } from 'lucide-svelte'; + import { Plus, Check, RefreshCw, Mail, Zap, Info, Send, CheckCircle2, XCircle, Key, ChevronDown, HelpCircle } from 'lucide-svelte'; + import * as Tooltip from '$lib/components/ui/tooltip'; import { toast } from 'svelte-sonner'; import { focusFirstInput } from '$lib/utils'; @@ -45,6 +46,7 @@ let formSmtpHost = $state(''); let formSmtpPort = $state(587); let formSmtpSecure = $state(false); + let formSmtpSkipTlsVerify = $state(false); let formSmtpUsername = $state(''); let formSmtpPassword = $state(''); let formSmtpFromEmail = $state(''); @@ -68,6 +70,7 @@ formSmtpHost = ''; formSmtpPort = 587; formSmtpSecure = false; + formSmtpSkipTlsVerify = false; formSmtpUsername = ''; formSmtpPassword = ''; formSmtpFromEmail = ''; @@ -98,6 +101,7 @@ formSmtpHost = notification.config.host || ''; formSmtpPort = notification.config.port || 587; formSmtpSecure = notification.config.secure || false; + formSmtpSkipTlsVerify = notification.config.skipTlsVerify || false; formSmtpUsername = notification.config.username || ''; formSmtpPassword = ''; formSmtpFromEmail = notification.config.from_email || ''; @@ -133,6 +137,7 @@ host: formSmtpHost.trim(), port: formSmtpPort, secure: formSmtpSecure, + skipTlsVerify: formSmtpSkipTlsVerify || undefined, username: formSmtpUsername.trim() || undefined, password: formSmtpPassword || undefined, from_email: formSmtpFromEmail.trim(), @@ -339,7 +344,20 @@ {#if formType === 'smtp'}
    -

    SMTP configuration

    +
    +

    SMTP configuration

    + + + + + + +

    Gmail: smtp.gmail.com, port 587, TLS/SSL off. Use an App Password.

    +

    Outlook: smtp.office365.com, port 587, TLS/SSL off.

    +
    +
    +
    +
    @@ -350,9 +368,15 @@
    -
    - - +
    +
    + + +
    +
    + + +
    diff --git a/routes/settings/notifications/NotificationsTab.svelte b/src/routes/settings/notifications/NotificationsTab.svelte similarity index 100% rename from routes/settings/notifications/NotificationsTab.svelte rename to src/routes/settings/notifications/NotificationsTab.svelte diff --git a/routes/settings/registries/RegistriesTab.svelte b/src/routes/settings/registries/RegistriesTab.svelte similarity index 100% rename from routes/settings/registries/RegistriesTab.svelte rename to src/routes/settings/registries/RegistriesTab.svelte diff --git a/routes/settings/registries/RegistryModal.svelte b/src/routes/settings/registries/RegistryModal.svelte similarity index 100% rename from routes/settings/registries/RegistryModal.svelte rename to src/routes/settings/registries/RegistryModal.svelte diff --git a/routes/stacks/+page.svelte b/src/routes/stacks/+page.svelte similarity index 89% rename from routes/stacks/+page.svelte rename to src/routes/stacks/+page.svelte index d2b939d..8afe99b 100644 --- a/routes/stacks/+page.svelte +++ b/src/routes/stacks/+page.svelte @@ -1,18 +1,20 @@ @@ -1104,6 +1119,10 @@ Create + {/if}
    @@ -1244,10 +1263,7 @@ sortDirection = state.direction; }} onRowClick={(stack, e) => { - const hasContainers = stack.containers && stack.containers.length > 0; - if (hasContainers) { - toggleExpand(stack.name); - } + toggleExpand(stack.name); }} rowClass={(stack) => { const isExp = expandedStacks.has(stack.name); @@ -1258,7 +1274,19 @@ {#snippet cell(column, stack, rowState)} {@const source = getStackSource(stack.name)} {#if column.id === 'name'} - {stack.name} + {#if source.sourceType !== 'git'} + + + {:else} + + {stack.name} + {/if} {#if stackEnvVarCounts[stack.name]} @@ -1274,41 +1302,45 @@ {/if} {:else if column.id === 'source'} {#if source.sourceType === 'git'} - - - - - Git - - - - {#if source.repository} - {source.repository.url} ({source.repository.branch}) - {:else} - Deployed from Git repository - {/if} - - + + + Git + {:else if source.sourceType === 'internal'} - - - - - Internal - - - Created in Dockhand - + + + Internal + {:else} + + + Untracked + + {/if} + {:else if column.id === 'location'} + {#if source.composePath} + {@const dirPath = source.composePath.replace(/\/[^/]+$/, '')} - - - - External + + + {dirPath} - Created outside Dockhand + + {source.composePath} + + {:else} + {/if} {:else if column.id === 'containers'}
    @@ -1468,23 +1500,24 @@ {/if} {#if $canAccess('stacks', 'edit')} - {#if source.sourceType === 'internal'} + {#if source.sourceType === 'git' && source.gitStack} - {:else if source.sourceType === 'git' && source.gitStack} + {:else} + {/if} {/if} @@ -1868,6 +1901,13 @@ {/each}
    + {:else} +
    +
    + + No containers +
    +
    {/if} {/snippet} @@ -1907,6 +1947,12 @@ onSaved={fetchStacks} /> + showImportModal = false} + onAdopted={fetchStacks} +/> + showBatchOpModal = false} onComplete={handleBatchComplete} /> + +{#if errorDialogData} + errorDialogData = null} + /> +{/if} diff --git a/routes/stacks/ComposeGraphViewer.svelte b/src/routes/stacks/ComposeGraphViewer.svelte similarity index 100% rename from routes/stacks/ComposeGraphViewer.svelte rename to src/routes/stacks/ComposeGraphViewer.svelte diff --git a/src/routes/stacks/FilesystemBrowser.svelte b/src/routes/stacks/FilesystemBrowser.svelte new file mode 100644 index 0000000..22feab2 --- /dev/null +++ b/src/routes/stacks/FilesystemBrowser.svelte @@ -0,0 +1,394 @@ + + + { if (!isOpen) handleClose(); }}> + + + + {#if icon} + + {/if} + {title} + + {#if description} + {description} + {/if} + + +
    + + + + +
    + +
    + + {currentPath || '/'} + {#if isAdoptMode} + + {/if} +
    + + +
    + {#if loading} +
    + +
    + {:else if error} +
    +
    + +
    +

    Unable to browse files

    +

    {error}

    + +
    + {:else if filteredEntries.length === 0} +
    + +

    {selectMode === 'directory' ? 'No subdirectories' : 'Directory is empty'}

    +
    + {:else} +
    + {#each filteredEntries as entry} + {@const selectable = isSelectable(entry)} + {@const highlighted = isHighlighted(entry)} + + {/each} +
    + {/if} +
    +
    +
    + + {#if !isAdoptMode} + + {#if selectMode === 'directory'} +
    + Selected: + {currentPath || '/'} +
    + {:else if selectMode === 'file_or_directory'} +
    + {#if selectedPath} + Selected: + {selectedPath} + {:else} + Click to select file or folder, double-click to enter folder + {/if} +
    + {:else if selectedPath} +
    + Selected: + {selectedPath} +
    + {:else} +
    + Click a file to select it +
    + {/if} + + {#if selectMode === 'directory'} + + {:else if selectMode === 'file_or_directory'} + + {:else} + + {/if} +
    + {/if} +
    +
    diff --git a/routes/stacks/GitDeployProgressPopover.svelte b/src/routes/stacks/GitDeployProgressPopover.svelte similarity index 99% rename from routes/stacks/GitDeployProgressPopover.svelte rename to src/routes/stacks/GitDeployProgressPopover.svelte index 83f4a0f..2740062 100644 --- a/routes/stacks/GitDeployProgressPopover.svelte +++ b/src/routes/stacks/GitDeployProgressPopover.svelte @@ -175,7 +175,7 @@ {@render children()} + import { onMount, onDestroy } from 'svelte'; import { Button } from '$lib/components/ui/button'; import * as Dialog from '$lib/components/ui/dialog'; import * as Select from '$lib/components/ui/select'; import { Label } from '$lib/components/ui/label'; import { Input } from '$lib/components/ui/input'; import { TogglePill } from '$lib/components/ui/toggle-pill'; - import { Loader2, GitBranch, RefreshCw, Webhook, Rocket, RefreshCcw, Copy, Check, FolderGit2, Github, Key, KeyRound, Lock, FileText } from 'lucide-svelte'; + import { Loader2, GitBranch, RefreshCw, Webhook, Rocket, RefreshCcw, Copy, Check, FolderGit2, Github, Key, KeyRound, Lock, FileText, HelpCircle, GripVertical, X } from 'lucide-svelte'; + import * as Tooltip from '$lib/components/ui/tooltip'; import CronEditor from '$lib/components/cron-editor.svelte'; import StackEnvVarsPanel from '$lib/components/StackEnvVarsPanel.svelte'; import { type EnvVar, type ValidationResult } from '$lib/components/StackEnvVarsEditor.svelte'; import { toast } from 'svelte-sonner'; import { focusFirstInput } from '$lib/utils'; + import { useSidebar } from '$lib/components/ui/sidebar/context.svelte'; + + // Get sidebar state to adjust modal positioning + const sidebar = useSidebar(); + + // localStorage key for persisted split ratio + const STORAGE_KEY_SPLIT = 'dockhand-git-stack-modal-split'; interface GitCredential { id: number; @@ -91,6 +100,11 @@ let loadingFileVars = $state(false); let existingSecretKeys = $state>(new Set()); + // Resizable split panel state + let splitRatio = $state(60); // percentage for form panel + let isDraggingSplit = $state(false); + let containerRef: HTMLDivElement | null = $state(null); + // Derived state for merged variables and sources const envVarSources = $derived>(() => { const sources: Record = {}; @@ -123,6 +137,48 @@ // Derived state for selected repository let selectedRepo = $derived(formRepositoryId ? repositories.find(r => r.id === formRepositoryId) : null); + onMount(() => { + // Load saved split ratio + const savedSplit = localStorage.getItem(STORAGE_KEY_SPLIT); + if (savedSplit) { + const ratio = parseFloat(savedSplit); + if (!isNaN(ratio) && ratio >= 30 && ratio <= 80) { + splitRatio = ratio; + } + } + + // Add global mouse event listeners for split dragging + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + }); + + onDestroy(() => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }); + + // Split panel drag handlers + function startSplitDrag(e: MouseEvent) { + e.preventDefault(); + isDraggingSplit = true; + } + + function handleMouseMove(e: MouseEvent) { + if (isDraggingSplit && containerRef) { + const rect = containerRef.getBoundingClientRect(); + const newRatio = ((e.clientX - rect.left) / rect.width) * 100; + splitRatio = Math.max(30, Math.min(80, newRatio)); + } + } + + function handleMouseUp() { + if (isDraggingSplit) { + isDraggingSplit = false; + // Save split ratio + localStorage.setItem(STORAGE_KEY_SPLIT, splitRatio.toString()); + } + } + function generateWebhookSecret(): string { const array = new Uint8Array(24); crypto.getRandomValues(array); @@ -387,20 +443,40 @@ { if (isOpen) focusFirstInput(); }}> - - - - - {gitStack ? 'Edit git stack' : 'Deploy from Git'} - - - {gitStack ? 'Update git stack settings' : 'Deploy a compose stack from a Git repository'} - + + +
    +
    +
    + +
    +
    + + {gitStack ? 'Edit git stack' : 'Deploy from Git'} + + + {gitStack ? 'Update git stack settings' : 'Deploy a compose stack from a Git repository'} + +
    +
    + + + +
    -
    +
    -
    +
    +
    {#if !gitStack}
    @@ -582,9 +658,23 @@

    Path to the compose file within the repository

    - +
    - +
    + + + + + + +
    +

    All files in the git repository are cloned automatically, including any files referenced by env_file: directives.

    +

    Only use this field for additional environment variables to be passed to Docker Compose.

    +

    The contents will be parsed and passed as shell environment variables.

    +
    +
    +
    +
    {#if gitStack && envFiles.length > 0} {/if} -

    Path to the .env file within the repository (optional)

    +

    + For variable substitution from a file outside the compose directory. +

    @@ -740,25 +832,41 @@ {#if !gitStack} -
    -
    - -
    - -

    Clone and deploy the stack immediately

    +
    +
    +
    + +
    + +

    Clone and deploy the stack immediately

    +
    +
    -
    {/if} {#if formError}

    {formError}

    {/if} +
    +
    + + + -
    +
    - + {#if gitStack} + + +
    +
    + + +{/if} + + + + + + + + Adopt this stack? + + + Review the compose file before adopting. + + + + {#if previewFile} +
    + +
    +
    + Stack: + {previewFile.path.replace(/\/[^/]+$/, '').split('/').pop() || 'unknown'} + {#if previewServiceCount > 0} + + {previewServiceCount} service{previewServiceCount !== 1 ? 's' : ''} + + {/if} +
    +
    + {previewFile.path} +
    +
    + + +
    + {#if loadingPreview} +
    + +
    + {:else if previewContent} + + {/if} +
    + + +
    +
    + + What happens when you adopt: Dockhand will track this compose file, letting you edit, start, and stop the stack from the UI. Your files stay in their current location. +
    +
    +
    + {/if} + +
    + + +
    +
    +
    diff --git a/src/routes/stacks/PathBarItem.svelte b/src/routes/stacks/PathBarItem.svelte new file mode 100644 index 0000000..637f976 --- /dev/null +++ b/src/routes/stacks/PathBarItem.svelte @@ -0,0 +1,92 @@ + + +
    +
    + {label} + + {displayPath.truncated || defaultText} + + + {#if onChangeLocation} + + {/if} + +
    +{#if sourceHint} + {sourceHint} +{/if} +
    diff --git a/src/routes/stacks/RecentLocationsPanel.svelte b/src/routes/stacks/RecentLocationsPanel.svelte new file mode 100644 index 0000000..277cc7e --- /dev/null +++ b/src/routes/stacks/RecentLocationsPanel.svelte @@ -0,0 +1,120 @@ + + +
    +

    Locations

    +
    + + {#if defaultBasePath} + + {/if} + + + {#each locations.filter(l => l !== defaultBasePath) as location} +
    + + +
    + {/each} + + {#if !defaultBasePath && locations.length === 0} +

    + No locations yet. Browse folders to add them here. +

    + {/if} +
    +
    diff --git a/src/routes/stacks/StackModal.svelte b/src/routes/stacks/StackModal.svelte new file mode 100644 index 0000000..2bccc78 --- /dev/null +++ b/src/routes/stacks/StackModal.svelte @@ -0,0 +1,1695 @@ + + + { + if (isOpen) { + focusFirstInput(); + } else { + // Prevent closing if there are unsaved changes - show confirmation instead + if (isDirty) { + // Re-open the dialog and show confirmation + open = true; + showConfirmClose = true; + } else { + // No unsaved changes - reset state + handleClose(); + } + } + }} +> + + +
    +
    +
    +
    + +
    +
    + + {#if mode === 'create'} + Create compose stack + {:else} + {stackName} + {/if} + + + {#if mode === 'create'} + Create a new Docker Compose stack + {:else} + Edit compose file and environment variables + {/if} + +
    +
    +
    + +
    + +
    + + +
    + + + {#if activeTab === 'editor'} + + {/if} + + + +
    +
    +
    + +
    + {#if errors.compose} + + + {errors.compose} + + {/if} + + {#if mode === 'edit' && loading} +
    +
    + + Loading compose file... +
    +
    + {:else} + + {#if mode === 'create'} +
    +
    +
    + + errors.stackName = undefined} + /> + {#if errors.stackName} +

    {errors.stackName}

    + {/if} +
    +
    +
    + {/if} + + + {#if mode === 'edit' && needsFileLocation} +
    +
    + +
    +

    + Untracked stack. Select the compose file location to start managing this stack with Dockhand. +

    + {#if stackContainers.length > 0} +
    + Running containers: +
    + {#each stackContainers as container} + + + {container.name} + + {/each} +
    +
    + {/if} +
    +
    +
    + {/if} + + +
    + {#if activeTab === 'editor'} + +
    + +
    + copyToClipboard(workingComposePath, (v) => composePathCopied = v)} + onBrowse={openComposeBrowser} + onChangeLocation={mode === 'edit' && !needsFileLocation ? openChangeLocationBrowser : undefined} + defaultText={mode === 'create' ? 'Enter stack name above' : 'Not specified'} + sourceHint={pathSourceHint} + /> +
    + +
    + +
    + copyToClipboard(displayEnvPath, (v) => envPathCopied = v)} + onBrowse={openEnvBrowser} + isEditable={true} + isCustom={!!workingEnvPath} + defaultText={mode === 'create' ? 'Enter stack name above' : 'Not specified'} + isSuggested={isEnvPathSuggested} + onPathChange={(value) => { + workingEnvPath = value; + isDirty = true; + }} + /> +
    +
    + +
    + +
    + {#if open} +
    + {#if needsFileLocation && !composeContent} + +
    + +

    No compose file selected

    +

    + Browse to locate the compose file for this stack. The editor will load the file contents once selected. +

    + + +
    + + What happens when you select a file: Dockhand will track this compose file, letting you edit, start, and stop the stack from the UI. Your files stay in their current location. +
    +
    + {:else} + + {/if} +
    + {/if} +
    + + + +
    + { markDirty(); debouncedValidate(); }} + theme={editorTheme} + infoText="These variables will be written to a .env file in the stack directory." + /> +
    +
    + {:else if activeTab === 'graph'} + + + {/if} +
    + {/if} +
    + + +
    +
    + {#if isDirty} + Unsaved changes + {:else} + No changes + {/if} +
    + +
    + + + {#if mode === 'create'} + + + + {:else} + + + + {/if} +
    +
    +
    +
    + + + + + + Unsaved changes + + You have unsaved changes. Are you sure you want to close without saving? + + +
    + + +
    +
    +
    + + + + + + Move stack files? + + You've changed the stack location. There {pathChangeFileCount === 1 ? 'is' : 'are'} {pathChangeFileCount} file{pathChangeFileCount === 1 ? '' : 's'} in the old location that can be moved to the new location. + + + {#if pathChangeOldDir} +
    +
    + + {pathChangeOldDir} +
    +
    + {/if} +

    + Would you like to move all files to the new location, or leave them in place? +

    +
    + + + +
    +
    +
    + + + + + + Replace editor content? + + Loading a different compose file will replace the current editor content. + + +
    +
    + Current: + + {workingComposePath || '(unsaved)'} + +
    +
    + + New: + + {pendingBrowsePath} + +
    +
    +
    + + +
    +
    +
    + + + + + + + + Relocate stack? + + + All {changeLocationFileCount} file{changeLocationFileCount === 1 ? '' : 's'} in the stack folder will be moved. + + +
    +
    + From + + {changeLocationOldDir} + +
    +
    + +
    +
    + To + + {pendingNewLocation} + +
    +
    +
    + + +
    +
    +
    + + +{#if operationError} + {@const errorDialogOpen = true} + operationError = null} + /> +{/if} + + + showFileBrowser = false} +/> diff --git a/src/routes/terminal/+page.svelte b/src/routes/terminal/+page.svelte new file mode 100644 index 0000000..ebd6ca3 --- /dev/null +++ b/src/routes/terminal/+page.svelte @@ -0,0 +1,415 @@ + + +{#if $environments.length === 0 || !$currentEnvironment} +
    + + +
    +{:else} +
    + +
    +
    + +
    + +
    + +
    +
    +
    + +
    + + + +
    + + + {#if dropdownOpen} +
    + {#if filteredContainers().length === 0} +
    + {containers.length === 0 ? (mode === 'exec' ? 'No running containers' : 'No containers') : 'No matches found'} +
    + {:else} + {#each filteredContainers() as container} + + {/each} + {/if} +
    + {/if} +
    + + {#if selectedContainer} + + {/if} + + {#if !selectedContainer} + {#if mode === 'exec'} +
    + + + + + {shellOptions.find((o) => o.value === selectedShell)?.label || 'Select'} + + + {#each shellOptions as option} + + + {option.label} + + {/each} + + +
    +
    + + + + + {userOptions.find((o) => o.value === selectedUser)?.label || 'Select'} + + + {#each userOptions as option} + + + {option.label} + + {/each} + + +
    + {/if} + {/if} +
    + + +
    + {#if !selectedContainer} +
    +
    + +

    Select a container to {mode === 'exec' ? 'open shell' : 'attach'}

    +
    +
    + {:else} + +
    +
    + {#if connected} + + + {mode === 'exec' ? 'Connected' : 'Attached'} + + {:else} + {mode === 'exec' ? 'Disconnected' : 'Detached'} + {/if} +
    +
    + changeFontSize(Number(v))}> + + {terminalFontSize}px + + + {#each fontSizeOptions as size} + {size}px + {/each} + + + + + +
    +
    +
    + {#key `${selectedContainer.id}-${mode}`} + {#if mode === 'exec'} + + {:else if mode === 'attach'} + + {/if} + {/key} +
    + {/if} +
    +
    +{/if} + + diff --git a/routes/terminal/Terminal.svelte b/src/routes/terminal/Terminal.svelte similarity index 97% rename from routes/terminal/Terminal.svelte rename to src/routes/terminal/Terminal.svelte index dd550ba..ebb3226 100644 --- a/routes/terminal/Terminal.svelte +++ b/src/routes/terminal/Terminal.svelte @@ -1,7 +1,7 @@ - + diff --git a/routes/volumes/VolumeInspectModal.svelte b/src/routes/volumes/VolumeInspectModal.svelte similarity index 98% rename from routes/volumes/VolumeInspectModal.svelte rename to src/routes/volumes/VolumeInspectModal.svelte index 9875d52..ae66005 100644 --- a/routes/volumes/VolumeInspectModal.svelte +++ b/src/routes/volumes/VolumeInspectModal.svelte @@ -48,7 +48,7 @@ - + diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..fb3570f --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from 'svelte-adapter-bun'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + + kit: { + adapter: adapter({ + out: 'build' + }) + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2a48236 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,1028 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { Database } from 'bun:sqlite'; +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { homedir } from 'os'; +import { join } from 'path'; +import { defineConfig, type Plugin } from 'vite'; + +const WS_PORT = 5174; + +// ============ Docker Target Types ============ + +interface DockerTarget { + type: 'unix' | 'tcp' | 'hawser-edge'; + socket?: string; + host?: string; + port?: number; + hawserToken?: string; + environmentId?: number; +} + +interface EnvironmentRow { + id: number; + is_local?: boolean | number; + connection_type?: string; + socket_path?: string; + host?: string; + port?: number; + hawser_token?: string; +} + +// ============ Docker Target Resolution ============ + +function resolveDockerTarget( + envId: number | undefined, + getEnvironment: (id: number) => EnvironmentRow | null, + defaultSocketPath: string +): DockerTarget { + if (!envId) return { type: 'unix', socket: defaultSocketPath }; + + const env = getEnvironment(envId); + if (!env) return { type: 'unix', socket: defaultSocketPath }; + + const isLocal = typeof env.is_local === 'boolean' ? env.is_local : Boolean(env.is_local); + if (isLocal || env.connection_type === 'socket' || !env.connection_type) { + return { type: 'unix', socket: env.socket_path || defaultSocketPath }; + } + + if (env.connection_type === 'hawser-edge') { + return { type: 'hawser-edge', environmentId: envId }; + } + + return { + type: 'tcp', + host: env.host || 'localhost', + port: env.port || 2375, + hawserToken: env.connection_type === 'hawser-standard' ? env.hawser_token : undefined + }; +} + +// ============ Exec API Helpers ============ + +function buildExecStartHttpRequest(execId: string, target: DockerTarget): string { + const body = JSON.stringify({ Detach: false, Tty: true }); + const tokenHeader = target.type === 'tcp' && target.hawserToken + ? `X-Hawser-Token: ${target.hawserToken}\r\n` + : ''; + return `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\n${tokenHeader}Connection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${body.length}\r\n\r\n${body}`; +} + +// ============ Attach API Helpers =========== + +function buildAttachHttpRequest(containerId: string, target: DockerTarget): string { + const tokenHeader = + target.type === 'tcp' && target.hawserToken ? `X-Hawser-Token: ${target.hawserToken}\r\n` : ''; + return `POST /containers/${containerId}/attach?stream=1&stdout=1&stderr=1&stdin=1 HTTP/1.1\r\nHost: localhost\r\n${tokenHeader}Connection: Upgrade\r\nUpgrade: tcp\r\n\r\n`; +} + +// ============ Stream Processing ============ + +function processTerminalOutput( + data: string, + state: { headersStripped: boolean; isChunked: boolean } +): string | null { + let text = data; + + if (!state.headersStripped) { + if (text.toLowerCase().includes('transfer-encoding: chunked')) { + state.isChunked = true; + } + const headerEnd = text.indexOf('\r\n\r\n'); + if (headerEnd > -1) { + text = text.slice(headerEnd + 4); + state.headersStripped = true; + } else if (text.startsWith('HTTP/')) { + return null; + } + } + + if (state.isChunked && text) { + text = text.replace(/^[0-9a-fA-F]+\r\n/gm, '').replace(/\r\n$/g, ''); + } + + return text || null; +} + +// ============ Hawser Edge Exec Messages ============ + +function createExecStartMessage(execId: string, containerId: string, shell: string, user: string, cols = 120, rows = 30) { + return { type: 'exec_start', execId, containerId, cmd: shell, user, cols, rows }; +} + +function createExecInputMessage(execId: string, data: string) { + return { type: 'exec_input', execId, data: Buffer.from(data).toString('base64') }; +} + +function createExecResizeMessage(execId: string, cols: number, rows: number) { + return { type: 'exec_resize', execId, cols, rows }; +} + +function createExecEndMessage(execId: string, reason = 'user_closed') { + return { type: 'exec_end', execId, reason }; +} + +// Get build info +function getGitCommit(): string | null { + // Check COMMIT file (created by CI/CD before docker build) + try { + if (existsSync('COMMIT')) { + const commit = require('fs').readFileSync('COMMIT', 'utf-8').trim(); + if (commit && commit !== 'unknown') { + return commit; + } + } + } catch { + // ignore + } + // Fall back to git command (local dev) + try { + return execSync('git rev-parse --short HEAD').toString().trim(); + } catch { + return null; + } +} + +function getGitBranch(): string | null { + // Check BRANCH file (created by CI/CD before docker build) + try { + if (existsSync('BRANCH')) { + const branch = require('fs').readFileSync('BRANCH', 'utf-8').trim(); + if (branch && branch !== 'unknown') { + return branch; + } + } + } catch { + // ignore + } + // Fall back to git command (local dev) + try { + return execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); + } catch { + return null; + } +} + +function getGitTag(): string | null { + // First check env var (set by CI/CD via Docker build-arg) + if (process.env.APP_VERSION) { + return process.env.APP_VERSION; + } + // Check VERSION file (created by CI/CD before docker build) + try { + if (existsSync('VERSION')) { + const version = require('fs').readFileSync('VERSION', 'utf-8').trim(); + if (version && version !== 'unknown') { + return version; + } + } + } catch { + // ignore + } + // Fall back to git tag (local dev) + try { + return execSync('git describe --tags --abbrev=0 2>/dev/null').toString().trim(); + } catch { + return null; + } +} + +// Plugin to externalize bun: protocol modules +function bunExternals(): Plugin { + return { + name: 'bun-externals', + enforce: 'pre', + resolveId(source) { + if (source.startsWith('bun:')) { + return { id: source, external: true }; + } + return null; + } + }; +} + +// Detect Docker socket path +function detectDockerSocket(): string { + if (process.env.DOCKER_SOCKET && existsSync(process.env.DOCKER_SOCKET)) return process.env.DOCKER_SOCKET; + if (process.env.DOCKER_HOST?.startsWith('unix://')) { + const p = process.env.DOCKER_HOST.replace('unix://', ''); + if (existsSync(p)) return p; + } + const candidates = [ + '/var/run/docker.sock', + join(homedir(), '.docker/run/docker.sock'), + join(homedir(), '.orbstack/run/docker.sock'), + '/run/docker.sock' + ]; + for (const s of candidates) { + if (existsSync(s)) return s; + } + return '/var/run/docker.sock'; +} + +// Lazy database connection for environment lookup +let _db: Database | null = null; +function getDb(): Database | null { + if (!_db) { + // Database is in data/db/dockhand.db (same as main app) + const dbPath = join(process.cwd(), 'data', 'db', 'dockhand.db'); + if (existsSync(dbPath)) { + _db = new Database(dbPath, { readonly: true }); + } + } + return _db; +} + +function getEnvironment(id: number): { host: string; port: number; is_local: boolean; connection_type?: string; hawser_token?: string } | null { + const db = getDb(); + if (!db) return null; + const row = db.prepare('SELECT * FROM environments WHERE id = ?').get(id) as any; + return row ? { ...row, is_local: Boolean(row.is_local) } : null; +} + +function getDockerTarget(envId?: number): DockerTarget { + const dockerSocketPath = detectDockerSocket(); + return resolveDockerTarget( + envId, + (id) => getEnvironment(id) as EnvironmentRow | null, + dockerSocketPath + ); +} + +async function createExecForWs(containerId: string, cmd: string[], user: string, target: ReturnType): Promise<{ Id: string }> { + const headers: Record = { 'Content-Type': 'application/json' }; + const fetchOpts: any = { + method: 'POST', + headers, + body: JSON.stringify({ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: cmd, User: user }) + }; + let url: string; + if (target.type === 'unix') { + url = 'http://localhost/containers/' + containerId + '/exec'; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/containers/' + containerId + '/exec'; + if (target.hawserToken) { + headers['X-Hawser-Token'] = target.hawserToken; + } + } + const res = await fetch(url, fetchOpts); + if (!res.ok) throw new Error('Failed to create exec: ' + (await res.text())); + return res.json(); +} + +async function resizeExecForWs(execId: string, cols: number, rows: number, target: ReturnType): Promise { + try { + const fetchOpts: any = { method: 'POST' }; + let url: string; + if (target.type === 'unix') { + url = 'http://localhost/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + fetchOpts.unix = target.socket; + } else { + url = 'http://' + target.host + ':' + target.port + '/exec/' + execId + '/resize?h=' + rows + '&w=' + cols; + if (target.hawserToken) { + fetchOpts.headers = { 'X-Hawser-Token': target.hawserToken }; + } + } + await fetch(url, fetchOpts); + } catch { + // Ignore resize errors + } +} + +// Map to track Docker streams per WebSocket (keyed by unique connection ID) +// Includes WebSocket reference for orphan detection +const dockerStreams = new Map; state: { isChunked: boolean }; ws: any }>(); + +// Counter for unique WebSocket connection IDs +let wsConnectionCounter = 0; + +// Map to track Edge exec sessions (execId -> frontend WebSocket) +const edgeExecSessions = new Map(); + +// Cleanup interval reference - only started in dev mode +let cleanupInterval: ReturnType | null = null; + +// Cleanup function for orphaned sessions +function startCleanupInterval() { + if (cleanupInterval) return; // Already running + + // Cleanup orphaned sessions every 5 minutes to prevent memory leaks + // Only removes sessions where the WebSocket is no longer open (readyState !== 1) + // This catches sessions where close handlers failed to fire + cleanupInterval = setInterval(() => { + let dockerCleaned = 0; + let edgeCleaned = 0; + + for (const [connId, session] of dockerStreams.entries()) { + // readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED + if (session.ws?.readyState !== 1) { + try { + session.stream?.end?.(); + } catch { /* ignore */ } + dockerStreams.delete(connId); + dockerCleaned++; + } + } + + for (const [execId, session] of edgeExecSessions.entries()) { + if (session.ws?.readyState !== 1) { + edgeExecSessions.delete(execId); + edgeCleaned++; + } + } + + if (dockerCleaned > 0 || edgeCleaned > 0) { + console.log(`[WS Cleanup] Removed ${dockerCleaned} orphaned docker streams, ${edgeCleaned} orphaned edge sessions`); + } + }, 5 * 60 * 1000); +} + +// Hawser Edge connection types (mirrors hawser.ts) +interface EdgeConnection { + ws: WebSocket; + environmentId: number; + agentId: string; + agentName: string; + agentVersion: string; + dockerVersion: string; + hostname: string; + capabilities: string[]; + connectedAt: Date; + lastHeartbeat: Date; + pendingRequests: Map; + pendingStreamRequests: Map; + pingInterval?: ReturnType; // Server-side ping to keep connection alive through proxies +} + +// Container event from edge agent (matches hawser.ts) +interface ContainerEventData { + containerId: string; + containerName?: string; + image?: string; + action: string; + actorAttributes?: Record; + timestamp: string; +} + +// Metrics data structure from Hawser agent +interface HawserMetrics { + cpuUsage: number; + cpuCores: number; + memoryTotal: number; + memoryUsed: number; + memoryFree: number; + diskTotal: number; + diskUsed: number; + diskFree: number; + networkRxBytes: number; + networkTxBytes: number; +} + +// Use globalThis to share connections with hawser.ts module +declare global { + var __hawserEdgeConnections: Map | undefined; + var __hawserSendMessage: ((envId: number, message: string) => boolean) | undefined; + var __hawserHandleContainerEvent: ((envId: number, event: ContainerEventData) => Promise) | undefined; + var __hawserHandleMetrics: ((envId: number, metrics: HawserMetrics) => Promise) | undefined; +} +const edgeConnections: Map = + globalThis.__hawserEdgeConnections ?? (globalThis.__hawserEdgeConnections = new Map()); + +// Function to send messages through the WebSocket (needed because ws.send must be called from vite context) +globalThis.__hawserSendMessage = (envId: number, message: string): boolean => { + const conn = edgeConnections.get(envId); + if (!conn || !conn.ws) { + return false; + } + + try { + conn.ws.send(message); + return true; + } catch (e) { + console.error(`[Hawser WS] sendMessage error:`, e); + return false; + } +}; + +// Map WebSocket to environmentId for quick lookup on close/message +const wsToEnvId = new Map(); + +// WebSocket server for terminal connections and Hawser Edge in development mode +function webSocketPlugin(): Plugin { + return { + name: 'websocket', + configureServer() { + // Start cleanup interval for dev mode only + startCleanupInterval(); + + const dockerSocketPath = detectDockerSocket(); + console.log(`[Terminal WS] Detected Docker socket at: ${dockerSocketPath}`); + + // Start a Bun.serve WebSocket server on a separate port + Bun.serve({ + port: WS_PORT, + fetch(req, server) { + // Upgrade HTTP requests to WebSocket + if (server.upgrade(req, { data: { url: req.url } })) { + return; // Return nothing if upgrade succeeds + } + return new Response('WebSocket server', { status: 200 }); + }, + websocket: { + async open(ws) { + const url = new URL((ws.data as any).url, `http://localhost:${WS_PORT}`); + + // Check if this is a Hawser Edge connection + if (url.pathname === '/api/hawser/connect') { + console.log('[Hawser WS] New connection pending authentication'); + // Hawser connections wait for hello message to authenticate + return; + } + + // Assign unique connection ID to this WebSocket + const connId = `ws-${++wsConnectionCounter}`; + (ws.data as any).connId = connId; + + // Terminal connection handling + const pathParts = url.pathname.split('/'); + const containerIdIndex = pathParts.indexOf('containers') + 1; + const containerId = pathParts[containerIdIndex]; + + const isAttach = url.pathname.includes('/attach'); + + const shell = url.searchParams.get('shell') || '/bin/sh'; + const user = url.searchParams.get('user') || 'root'; + const envIdParam = url.searchParams.get('envId'); + const envId = envIdParam ? parseInt(envIdParam, 10) : undefined; + + if (!containerId) { + ws.send(JSON.stringify({ type: 'error', message: 'No container ID' })); + ws.close(); + return; + } + + console.log(envId) + const target = getDockerTarget(envId); + const mode = isAttach ? 'attach' : 'exec'; + console.log('[Terminal WS] Open connId:', connId, 'container:', containerId, 'target:', 'mode:', mode, target.type); + + try { + // Handle Hawser Edge mode differently - use WebSocket protocol + if (target.type === 'hawser-edge') { + const conn = edgeConnections.get(target.environmentId); + if (!conn) { + ws.send(JSON.stringify({ type: 'error', message: 'Edge agent not connected' })); + ws.close(); + return; + } + + if(isAttach) { + // For attach, send attach_start message + const attachId = crypto.randomUUID(); + console.log('[Terminal WS] Starting Edge attach:', attachId, 'container:', containerId); + edgeExecSessions.set(attachId, { ws, execId: attachId, environmentId: target.environmentId }); + + (ws.data as any).edgeExecId = attachId; + conn.ws.send(JSON.stringify({ type: 'attach', containerId, attachId })); + return; + } + + // Generate unique exec ID + const execId = crypto.randomUUID(); + console.log('[Terminal WS] Starting Edge exec:', execId, 'container:', containerId); + + // Track this session + edgeExecSessions.set(execId, { ws, execId, environmentId: target.environmentId }); + (ws.data as any).edgeExecId = execId; + + // Send exec_start to the agent (using shared helper) + const execStartMsg = createExecStartMessage(execId, containerId, shell, user); + conn.ws.send(JSON.stringify(execStartMsg)); + return; + } + + let httpRequest: string + let execId: string; + + if(isAttach) { + // Attach mode + execId = crypto.randomUUID(); + httpRequest = buildAttachHttpRequest(containerId, target); + console.log("[Terminal WS] Attaching to container: ", containerId, "execId:", execId); + } else { + // Exec mode + const exec = await createExecForWs(containerId, [shell], user, target); + httpRequest = buildExecStartHttpRequest(exec.Id, target); + console.log("[Terminal WS] Created exec: ", exec.Id, "for container:", containerId); + } + + // Track connection state (using object for mutability across closures) + let headersStripped = false; + const state = { isChunked: false }; + + // Create socket handler for Docker connection + const socketHandler = { + data(socket: any, data: Buffer) { + if (ws.readyState === 1) { + let text = new TextDecoder().decode(data); + // Skip HTTP headers in first response (only once) + if (!headersStripped) { + // Check for chunked encoding in headers + if (text.toLowerCase().includes('transfer-encoding: chunked')) { + state.isChunked = true; + } + const headerEnd = text.indexOf('\r\n\r\n'); + if (headerEnd > -1) { + text = text.slice(headerEnd + 4); + headersStripped = true; + } else if (text.startsWith('HTTP/')) { + // Headers split across packets, skip this entire packet + return; + } + } + // Strip chunked encoding framing if detected + if (state.isChunked && text) { + // Remove chunk size lines (hex number followed by \r\n) + text = text.replace(/^[0-9a-fA-F]+\r\n/gm, '').replace(/\r\n$/g, ''); + } + if (text) { + ws.send(JSON.stringify({ type: 'output', data: text })); + } + } + }, + close() { + if (ws.readyState === 1) { + ws.send(JSON.stringify({ type: 'exit' })); + ws.close(); + } + }, + error() {}, + open(socket: any) { + socket.write(httpRequest); + } + }; + + let dockerStream: any; + if (target.type === 'unix') { + dockerStream = await Bun.connect({ unix: target.socket, socket: socketHandler }); + } else if (target.type === 'tcp') { + dockerStream = await Bun.connect({ hostname: target.host, port: target.port, socket: socketHandler }); + } + + dockerStreams.set(connId, { stream: dockerStream, execId, target, state, ws }); + console.log('[Terminal WS] Stream stored for connId:', connId, 'total streams:', dockerStreams.size); + } catch (error: any) { + console.error('[Terminal WS] Error:', error.message); + ws.send(JSON.stringify({ type: 'error', message: error.message })); + ws.close(); + } + }, + async message(ws, message) { + const url = new URL((ws.data as any).url, `http://localhost:${WS_PORT}`); + const connId = (ws.data as any).connId as string | undefined; + console.log('[WS Message] connId:', connId, 'edgeExecId:', (ws.data as any)?.edgeExecId, 'pathname:', url.pathname.slice(0, 50)); + + // Handle Hawser Edge messages + if (url.pathname === '/api/hawser/connect') { + try { + // Debug: Log raw message info + const msgType = typeof message; + const msgLen = typeof message === 'string' ? message.length : + message instanceof ArrayBuffer ? message.byteLength : + (message as Buffer).length || 0; + console.log(`[Hawser WS] Received message: type=${msgType}, length=${msgLen}`); + + // Convert message to string properly (handles both string and ArrayBuffer) + let messageStr: string; + if (typeof message === 'string') { + messageStr = message; + } else if (message instanceof ArrayBuffer) { + messageStr = new TextDecoder().decode(message); + } else if (Buffer.isBuffer(message)) { + messageStr = message.toString('utf-8'); + } else { + // Uint8Array or similar + messageStr = new TextDecoder().decode(new Uint8Array(message as ArrayBuffer)); + } + + console.log(`[Hawser WS] Decoded string length: ${messageStr.length}`); + if (messageStr.length > 0) { + console.log(`[Hawser WS] First 200 chars: ${messageStr.slice(0, 200)}`); + } + + const msg = JSON.parse(messageStr); + console.log(`[Hawser WS] Parsed message type: ${msg.type}`); + await handleHawserMessage(ws, msg); + } catch (error: any) { + console.error('[Hawser WS] Error handling message:', error.message); + // More detailed debug output + const msgType = typeof message; + const msgLen = typeof message === 'string' ? message.length : + message instanceof ArrayBuffer ? message.byteLength : + (message as Buffer).length || 0; + console.error(`[Hawser WS] Message details: type=${msgType}, length=${msgLen}`); + if (typeof message === 'string' && message.length > 0) { + console.error(`[Hawser WS] Message preview: ${message.slice(0, 500)}`); + } else if (message instanceof ArrayBuffer && message.byteLength > 0) { + const preview = new TextDecoder().decode(message.slice(0, 500)); + console.error(`[Hawser WS] ArrayBuffer preview: ${preview}`); + } else if (Buffer.isBuffer(message) && message.length > 0) { + console.error(`[Hawser WS] Buffer preview: ${message.toString('utf-8').slice(0, 500)}`); + } + ws.send(JSON.stringify({ type: 'error', error: error.message })); + } + return; + } + + // Check if this is an Edge exec session + const edgeExecId = (ws.data as any)?.edgeExecId; + if (edgeExecId) { + const session = edgeExecSessions.get(edgeExecId); + if (session) { + const conn = edgeConnections.get(session.environmentId); + if (conn) { + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input') { + // Forward input to agent (using shared helper) + conn.ws.send(JSON.stringify(createExecInputMessage(edgeExecId, msg.data))); + } else if (msg.type === 'resize') { + // Forward resize to agent (using shared helper) + conn.ws.send(JSON.stringify(createExecResizeMessage(edgeExecId, msg.cols, msg.rows))); + } + } catch (e) { + console.error('[Terminal WS] Error handling Edge message:', e); + } + } + } + return; + } + + // Terminal message handling (direct Docker connection) + if (!connId) { + console.log('[Terminal WS] No connId for terminal message'); + return; + } + const d = dockerStreams.get(connId); + if (!d) { + console.log('[Terminal WS] No stream for connId:', connId, 'streams:', [...dockerStreams.keys()]); + return; + } + console.log('[Terminal WS] Found stream for connId:', connId); + + try { + const msg = JSON.parse(message.toString()); + if (msg.type === 'input' && d.stream) { + // Always write raw input - chunked encoding only affects reading output + d.stream.write(msg.data); + } else if (msg.type === 'resize' && d.execId) { + resizeExecForWs(d.execId, msg.cols, msg.rows, d.target); + } + } catch { + // If not JSON, treat as raw input + if (d.stream) { + d.stream.write(message); + } + } + }, + close(ws) { + // Check if it's a Hawser connection + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + console.log(`[Hawser WS] Agent disconnected: ${conn.agentId}`); + // Clear server-side ping interval + if (conn.pingInterval) { + clearInterval(conn.pingInterval); + conn.pingInterval = undefined; + } + // Reject pending requests + for (const [, pending] of conn.pendingRequests) { + clearTimeout(pending.timeout); + pending.reject(new Error('Connection closed')); + } + // Clean up pending stream requests + for (const [, pending] of conn.pendingStreamRequests) { + pending.onEnd('Connection closed'); + } + edgeConnections.delete(envId); + } + wsToEnvId.delete(ws); + return; + } + + // Check if it's an Edge exec session + const edgeExecId = (ws.data as any)?.edgeExecId; + if (edgeExecId) { + const session = edgeExecSessions.get(edgeExecId); + if (session) { + // Send exec_end to agent (using shared helper) + const conn = edgeConnections.get(session.environmentId); + if (conn) { + conn.ws.send(JSON.stringify(createExecEndMessage(edgeExecId))); + } + edgeExecSessions.delete(edgeExecId); + console.log(`[Terminal WS] Edge exec session closed: ${edgeExecId}`); + } + return; + } + + // Terminal connection cleanup (direct Docker) + const connId = (ws.data as any)?.connId as string | undefined; + if (connId) { + const d = dockerStreams.get(connId); + if (d?.stream) { + d.stream.end(); + } + dockerStreams.delete(connId); + } + } + } + }); + + console.log(`[Terminal WS] WebSocket server running on port ${WS_PORT}`); + } + }; +} + +// Handle Hawser Edge protocol messages +async function handleHawserMessage(ws: any, msg: any) { + if (msg.type === 'hello') { + // Validate token using the app's hawser module + // For dev mode, we'll do a simplified validation + console.log(`[Hawser WS] Hello from agent: ${msg.agentName} (${msg.agentId})`); + + // In dev mode, we need to validate the token against the database + const db = getDb(); + if (!db) { + ws.send(JSON.stringify({ type: 'error', error: 'Database not available' })); + ws.close(); + return; + } + + // Simple token validation (in production this would use argon2 verification) + // For dev mode, just check if a token exists for any environment + const tokens = db.prepare('SELECT * FROM hawser_tokens WHERE is_active = 1').all() as any[]; + + // For dev mode, accept any valid token format and use the first environment with a token + const token = tokens.find((t: any) => msg.token && msg.token.startsWith(t.token_prefix.slice(0, 4))); + + if (!token) { + console.log('[Hawser WS] Invalid token'); + ws.send(JSON.stringify({ type: 'error', error: 'Invalid token' })); + ws.close(); + return; + } + + const environmentId = token.environment_id; + + // Update environment with agent info + try { + db.prepare(`UPDATE environments SET + hawser_last_seen = datetime('now'), + hawser_agent_id = ?, + hawser_agent_name = ?, + hawser_version = ?, + hawser_capabilities = ? + WHERE id = ?`).run( + msg.agentId, + msg.agentName, + msg.version, + JSON.stringify(msg.capabilities || []), + environmentId + ); + } catch (e) { + // Read-only DB in dev mode, ignore + } + + // Close any existing connection for this environment + const existing = edgeConnections.get(environmentId); + if (existing) { + const pendingCount = existing.pendingRequests.size; + const streamCount = existing.pendingStreamRequests.size; + console.log( + `[Hawser WS] Replacing existing connection for environment ${environmentId}. ` + + `Rejecting ${pendingCount} pending requests and ${streamCount} stream requests.` + ); + + // Reject all pending requests before closing + for (const [requestId, pending] of existing.pendingRequests) { + console.log(`[Hawser WS] Rejecting pending request ${requestId} due to connection replacement`); + clearTimeout(pending.timeout); + pending.reject(new Error('Connection replaced by new agent')); + } + for (const [requestId, pending] of existing.pendingStreamRequests) { + console.log(`[Hawser WS] Ending stream request ${requestId} due to connection replacement`); + pending.onEnd?.('Connection replaced by new agent'); + } + existing.pendingRequests.clear(); + existing.pendingStreamRequests.clear(); + + existing.ws.close(1000, 'Replaced by new connection'); + wsToEnvId.delete(existing.ws); + } + + // Store connection in shared map (accessible by hawser.ts via globalThis) + const connection: EdgeConnection = { + ws, + environmentId, + agentId: msg.agentId, + agentName: msg.agentName, + agentVersion: msg.version || 'unknown', + dockerVersion: msg.dockerVersion || 'unknown', + hostname: msg.hostname || 'unknown', + capabilities: msg.capabilities || [], + connectedAt: new Date(), + lastHeartbeat: new Date(), + pendingRequests: new Map(), + pendingStreamRequests: new Map() + }; + + edgeConnections.set(environmentId, connection); + wsToEnvId.set(ws, environmentId); + + // Send welcome + ws.send(JSON.stringify({ + type: 'welcome', + environmentId, + message: `Welcome ${msg.agentName}! Connected to Dockhand dev server.` + })); + + // Start server-side ping interval to keep connection alive through Traefik/proxies + // Traefik has ~10s idle timeout, so we ping every 5 seconds + connection.pingInterval = setInterval(() => { + try { + ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); + } catch (e) { + // Connection likely closed, clear interval + if (connection.pingInterval) { + clearInterval(connection.pingInterval); + connection.pingInterval = undefined; + } + } + }, 5000); + + console.log(`[Hawser WS] Agent ${msg.agentName} connected for environment ${environmentId}`); + } else if (msg.type === 'ping') { + // Agent sent ping - respond with pong to keep connection alive + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + conn.lastHeartbeat = new Date(); + } + } + ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); + } else if (msg.type === 'pong') { + // Heartbeat response - update last seen + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + conn.lastHeartbeat = new Date(); + } + } + } else if (msg.type === 'response') { + // Response to a request we sent + const envId = wsToEnvId.get(ws); + if (envId) { + const conn = edgeConnections.get(envId); + if (conn) { + const pending = conn.pendingRequests.get(msg.requestId); + if (pending) { + clearTimeout(pending.timeout); + conn.pendingRequests.delete(msg.requestId); + + // Body is now a string (either plain text/JSON or base64-encoded binary) + // isBinary flag indicates if base64 decoding is needed + pending.resolve({ + statusCode: msg.statusCode, + headers: msg.headers || {}, + body: msg.body || '', + isBinary: msg.isBinary || false + }); + } + } + } + } else if (msg.type === 'stream') { + // Streaming data from agent + const envId = wsToEnvId.get(ws); + if (!envId) { + console.warn(`[Hawser WS] Stream data from unknown WebSocket, requestId=${msg.requestId}`); + return; + } + const conn = edgeConnections.get(envId); + if (!conn) { + console.warn(`[Hawser WS] Stream data for unknown environment ${envId}, requestId=${msg.requestId}`); + return; + } + const pending = conn.pendingStreamRequests?.get(msg.requestId); + if (!pending) { + console.warn(`[Hawser WS] Stream data for unknown request ${msg.requestId} on env ${envId}`); + return; + } + pending.onData(msg.data, msg.stream); + } else if (msg.type === 'stream_end') { + // Stream ended + const envId = wsToEnvId.get(ws); + if (!envId) { + console.warn(`[Hawser WS] Stream end from unknown WebSocket, requestId=${msg.requestId}`); + return; + } + const conn = edgeConnections.get(envId); + if (!conn) { + console.warn(`[Hawser WS] Stream end for unknown environment ${envId}, requestId=${msg.requestId}`); + return; + } + const pending = conn.pendingStreamRequests.get(msg.requestId); + if (!pending) { + console.warn(`[Hawser WS] Stream end for unknown request ${msg.requestId} on env ${envId}`); + return; + } + conn.pendingStreamRequests.delete(msg.requestId); + pending.onEnd(msg.reason); + } else if (msg.type === 'metrics') { + // Metrics from agent - save to database for dashboard graphs + const envId = wsToEnvId.get(ws); + if (envId && msg.metrics) { + if (globalThis.__hawserHandleMetrics) { + globalThis.__hawserHandleMetrics(envId, msg.metrics).catch((err) => { + console.error(`[Hawser WS] Error saving metrics:`, err); + }); + } + } + } else if (msg.type === 'exec_ready') { + // Exec session is ready + const session = edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) { + console.log(`[Hawser WS] Exec ready: ${msg.execId}`); + // Frontend doesn't need explicit ready message, it's already waiting for output + } + } else if (msg.type === 'exec_output') { + // Terminal output from exec session + const session = edgeExecSessions.get(msg.execId); + if (session?.ws?.readyState === 1) { + // Decode base64 data + const data = Buffer.from(msg.data, 'base64').toString('utf-8'); + session.ws.send(JSON.stringify({ type: 'output', data })); + } + } else if (msg.type === 'exec_end') { + // Exec session ended + const session = edgeExecSessions.get(msg.execId); + if (session) { + console.log(`[Hawser WS] Exec ended: ${msg.execId} (reason: ${msg.reason})`); + if (session.ws?.readyState === 1) { + session.ws.send(JSON.stringify({ type: 'exit' })); + session.ws.close(); + } + edgeExecSessions.delete(msg.execId); + } + } else if (msg.type === 'container_event') { + // Container event from edge agent + const envId = wsToEnvId.get(ws); + if (envId && msg.event) { + // Call the global handler registered by hawser.ts + if (globalThis.__hawserHandleContainerEvent) { + globalThis.__hawserHandleContainerEvent(envId, msg.event).catch((err) => { + console.error('[Hawser WS] Error handling container event:', err); + }); + } + } + } else if (msg.type === 'error' && msg.requestId) { + // Error might be for an exec session + const session = edgeExecSessions.get(msg.requestId); + if (session?.ws?.readyState === 1) { + console.error(`[Hawser WS] Exec error: ${msg.error}`); + session.ws.send(JSON.stringify({ type: 'error', message: msg.error })); + session.ws.close(); + edgeExecSessions.delete(msg.requestId); + } + } +} + +export default defineConfig({ + plugins: [bunExternals(), tailwindcss(), sveltekit(), webSocketPlugin()], + define: { + __BUILD_DATE__: JSON.stringify(new Date().toISOString()), + __BUILD_COMMIT__: JSON.stringify(getGitCommit()), + __BUILD_BRANCH__: JSON.stringify(getGitBranch()), + __APP_VERSION__: JSON.stringify(getGitTag()) + }, + optimizeDeps: { + include: ['lucide-svelte', '@xterm/xterm', '@xterm/addon-fit'] + }, + build: { + target: 'esnext', + minify: 'esbuild', + sourcemap: false, + rollupOptions: { + external: [/^bun:/] + } + }, + ssr: { + external: [/^bun:/] + } +});