From 2584e92af2e1ecdb7c0a1ec8a4c1f9867e2cc324 Mon Sep 17 00:00:00 2001 From: pratik Date: Thu, 16 Oct 2025 13:02:50 -0500 Subject: [PATCH 1/3] Update build process, gitignore packagelock: --- .gitignore | 4 +++ Dockerfile | 31 +++++++++++++++++- Dockerfile.frontend | 40 ----------------------- Dockerfile.frontend.prebuilt | 20 ------------ app/main.py | 26 +++++++++++++++ build-for-airgap.sh | 63 ------------------------------------ docker-compose.yml | 14 -------- nginx.conf | 36 --------------------- quickstart-airgap.sh | 12 ++----- quickstart.sh | 2 +- 10 files changed, 64 insertions(+), 184 deletions(-) delete mode 100644 Dockerfile.frontend delete mode 100644 Dockerfile.frontend.prebuilt delete mode 100755 build-for-airgap.sh delete mode 100644 nginx.conf diff --git a/.gitignore b/.gitignore index 64db696..63e3afe 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ helm/charts/ tmp/ temp/ *.tmp + +# Node.js +package-lock.json +**/package-lock.json diff --git a/Dockerfile b/Dockerfile index 66de827..b12df05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,30 @@ +# Multi-stage build: First stage builds Angular frontend +FROM node:24-alpine AS frontend-build + +# Accept npm registry as build argument +ARG NPM_REGISTRY=https://registry.npmjs.org/ + +WORKDIR /frontend + +# Copy package files +COPY frontend/package*.json ./ + +# Configure npm registry if custom registry is provided +RUN if [ "$NPM_REGISTRY" != "https://registry.npmjs.org/" ]; then \ + echo "Using custom npm registry: $NPM_REGISTRY"; \ + npm config set registry "$NPM_REGISTRY"; \ + fi + +# Install dependencies (ignore package-lock.json if using custom registry) +RUN npm install + +# Copy source code +COPY frontend/ ./ + +# Build for production +RUN npm run build:prod + +# Second stage: Python backend with Angular frontend FROM python:3.11-alpine WORKDIR /app @@ -20,7 +47,9 @@ COPY app/ ./app/ COPY utils/ ./utils/ COPY alembic/ ./alembic/ COPY alembic.ini . -COPY static/ ./static/ + +# Copy built Angular frontend from first stage to static directory +COPY --from=frontend-build /frontend/dist/frontend/browser ./static/ # Create non-root user (Alpine uses adduser instead of useradd) RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app diff --git a/Dockerfile.frontend b/Dockerfile.frontend deleted file mode 100644 index 21ad07e..0000000 --- a/Dockerfile.frontend +++ /dev/null @@ -1,40 +0,0 @@ -# Multi-stage build for Angular frontend -FROM node:24-alpine AS build - -# Accept npm registry as build argument -ARG NPM_REGISTRY=https://registry.npmjs.org/ - -WORKDIR /app - -# Copy package files -COPY frontend/package*.json ./ - -# Configure npm registry and regenerate package-lock.json if custom registry is provided -RUN if [ "$NPM_REGISTRY" != "https://registry.npmjs.org/" ]; then \ - echo "Using custom npm registry: $NPM_REGISTRY"; \ - npm config set registry "$NPM_REGISTRY"; \ - rm -f package-lock.json; \ - npm install --package-lock-only; \ - fi - -# Install dependencies -RUN npm ci - -# Copy source code -COPY frontend/ ./ - -# Build for production -RUN npm run build:prod - -# Final stage - nginx to serve static files -FROM nginx:alpine - -# Copy built Angular app to nginx -COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/Dockerfile.frontend.prebuilt b/Dockerfile.frontend.prebuilt deleted file mode 100644 index f5599ce..0000000 --- a/Dockerfile.frontend.prebuilt +++ /dev/null @@ -1,20 +0,0 @@ -# Dockerfile for pre-built Angular frontend (air-gapped/restricted environments) -# -# IMPORTANT: You must build the Angular app BEFORE running docker-compose! -# Run this command first: ./build-for-airgap.sh -# OR manually: cd frontend && npm install && npm run build:prod -# -# This Dockerfile expects frontend/dist/frontend/browser to exist - -FROM nginx:alpine - -# Copy pre-built Angular app to nginx -# If this step fails, you need to run: ./build-for-airgap.sh -COPY frontend/dist/frontend/browser /usr/share/nginx/html - -# Copy nginx configuration -COPY nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/app/main.py b/app/main.py index 6ac02e3..e6cd10a 100644 --- a/app/main.py +++ b/app/main.py @@ -84,6 +84,32 @@ async def ui_root(): } +# Catch-all route for Angular SPA routing - must be last +@app.get("/{full_path:path}") +async def serve_spa(full_path: str): + """Serve Angular SPA for all non-API routes""" + # If it's an API route, it should have been handled by routers above + # If it's a static file request, try to serve it + if full_path.startswith("static/"): + file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), full_path) + if os.path.exists(file_path): + return FileResponse(file_path) + + # For all other routes (Angular routes), serve index.html + index_path = os.path.join(static_dir, "index.html") + if os.path.exists(index_path): + return FileResponse(index_path) + else: + return { + "message": "Warehouse13 - Enterprise Test Artifact Storage", + "version": "1.0.0", + "docs": "/docs", + "ui": "UI not found. Serving API only.", + "deployment_mode": settings.deployment_mode, + "storage_backend": settings.storage_backend + } + + @app.get("/health") async def health_check(): """Health check endpoint""" diff --git a/build-for-airgap.sh b/build-for-airgap.sh deleted file mode 100755 index d91cf77..0000000 --- a/build-for-airgap.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -set -e - -echo "=========================================" -echo "Building Angular for Air-Gapped Deployment" -echo "=========================================" -echo "" - -# Check if we're in the right directory -if [ ! -f "docker-compose.yml" ]; then - echo "Error: Must run from project root directory" - exit 1 -fi - -# Check if node is installed -if ! command -v node &> /dev/null; then - echo "Error: Node.js is not installed. Please install Node.js 18+ first." - exit 1 -fi - -# Check if npm is installed -if ! command -v npm &> /dev/null; then - echo "Error: npm is not installed. Please install npm first." - exit 1 -fi - -echo "Step 1/3: Installing dependencies..." -cd frontend -npm install - -echo "" -echo "Step 2/3: Building Angular production bundle..." -npm run build:prod - -echo "" -echo "Step 3/3: Verifying build output..." -if [ -d "dist/frontend/browser" ]; then - echo "✓ Build successful!" - echo "✓ Output: frontend/dist/frontend/browser" - ls -lh dist/frontend/browser | head -5 -else - echo "✗ Build failed - output directory not found" - exit 1 -fi - -cd .. - -echo "" -echo "=========================================" -echo "Build Complete!" -echo "=========================================" -echo "" -echo "Next steps:" -echo "1. Update docker-compose.yml:" -echo " Change: dockerfile: Dockerfile.frontend" -echo " To: dockerfile: Dockerfile.frontend.prebuilt" -echo "" -echo "2. Deploy:" -echo " docker-compose up -d --build" -echo "" -echo "See DEPLOYMENT.md for more details." -echo "=========================================" diff --git a/docker-compose.yml b/docker-compose.yml index 8a2f2d1..d9bdac4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,20 +59,6 @@ services: timeout: 10s retries: 3 - frontend: - build: - context: . - dockerfile: Dockerfile.frontend.prebuilt - ports: - - "4200:80" - depends_on: - - api - healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost/"] - interval: 30s - timeout: 10s - retries: 3 - volumes: postgres_data: minio_data: diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index bcc9963..0000000 --- a/nginx.conf +++ /dev/null @@ -1,36 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Gzip compression - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json; - - # Angular routes - serve index.html for all routes - location / { - try_files $uri $uri/ /index.html; - } - - # Proxy API requests to backend - location /api { - proxy_pass http://api:8000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } -} diff --git a/quickstart-airgap.sh b/quickstart-airgap.sh index a726a32..4c6c03f 100755 --- a/quickstart-airgap.sh +++ b/quickstart-airgap.sh @@ -28,17 +28,12 @@ if ! command -v docker-compose &> /dev/null; then exit 1 fi -echo "Step 1: Building Angular frontend locally..." -echo "===========================================" -./build-for-airgap.sh - -echo "" -echo "Step 2: Starting Docker containers..." +echo "Step 1: Starting Docker containers..." echo "===========================================" docker-compose up -d --build echo "" -echo "Step 3: Waiting for services to be ready..." +echo "Step 2: Waiting for services to be ready..." sleep 15 echo "" @@ -46,8 +41,7 @@ echo "=========================================" echo "Services are running!" echo "=========================================" echo "" -echo "Frontend: http://localhost:4200" -echo "API: http://localhost:8000" +echo "Web UI: http://localhost:8000" echo "API Docs: http://localhost:8000/docs" echo "MinIO Console: http://localhost:9001" echo " Username: minioadmin" diff --git a/quickstart.sh b/quickstart.sh index 3bbafca..591d37c 100755 --- a/quickstart.sh +++ b/quickstart.sh @@ -41,7 +41,7 @@ echo "=========================================" echo "Services are running!" echo "=========================================" echo "" -echo "API: http://localhost:8000" +echo "Web UI: http://localhost:8000" echo "API Docs: http://localhost:8000/docs" echo "MinIO Console: http://localhost:9001" echo " Username: minioadmin" From 122e3f2edc6a4723384e15d24371a38c4ea2f799 Mon Sep 17 00:00:00 2001 From: pratik Date: Thu, 16 Oct 2025 13:38:11 -0500 Subject: [PATCH 2/3] Update gitignore, combined docker for frotnend and api --- .gitignore | 3 + README.md | 7 +- app/main.py | 20 +- docker-compose.yml | 3 +- API.md => docs/API.md | 0 ARCHITECTURE.md => docs/ARCHITECTURE.md | 0 DEPLOYMENT.md => docs/DEPLOYMENT.md | 0 FEATURES.md => docs/FEATURES.md | 0 FRONTEND_SETUP.md => docs/FRONTEND_SETUP.md | 0 SUMMARY.md => docs/SUMMARY.md | 0 frontend/angular.json | 3 + quickstart.bat | 106 ---- quickstart.ps1 | 2 +- static/css/styles.css | 564 ------------------- static/index.html | 251 --------- static/js/app.js | 592 -------------------- 16 files changed, 18 insertions(+), 1533 deletions(-) rename API.md => docs/API.md (100%) rename ARCHITECTURE.md => docs/ARCHITECTURE.md (100%) rename DEPLOYMENT.md => docs/DEPLOYMENT.md (100%) rename FEATURES.md => docs/FEATURES.md (100%) rename FRONTEND_SETUP.md => docs/FRONTEND_SETUP.md (100%) rename SUMMARY.md => docs/SUMMARY.md (100%) delete mode 100644 quickstart.bat delete mode 100644 static/css/styles.css delete mode 100644 static/index.html delete mode 100644 static/js/app.js diff --git a/.gitignore b/.gitignore index 63e3afe..13e92a9 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ temp/ # Node.js package-lock.json **/package-lock.json + +# Built static files (generated during Docker build from Angular) +static/ diff --git a/README.md b/README.md index b894398..2c37805 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,6 @@ A lightweight, cloud-native API for storing and querying test artifacts includin .\quickstart.ps1 ``` -**Windows (Command Prompt):** -```batch -quickstart.bat -``` - ### Air-Gapped/Restricted Environment Deployment **For environments with restricted npm access:** @@ -65,7 +60,7 @@ This script: 2. Packages pre-built files into Docker 3. Starts all services -See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed instructions. +See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for detailed instructions. ### Manual Setup with Docker Compose diff --git a/app/main.py b/app/main.py index e6cd10a..a6c252d 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse @@ -39,10 +39,8 @@ app.add_middleware( app.include_router(artifacts_router) app.include_router(seed_router) -# Mount static files +# Static directory setup static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static") -if os.path.exists(static_dir): - app.mount("/static", StaticFiles(directory=static_dir), name="static") @app.on_event("startup") @@ -87,15 +85,13 @@ async def ui_root(): # Catch-all route for Angular SPA routing - must be last @app.get("/{full_path:path}") async def serve_spa(full_path: str): - """Serve Angular SPA for all non-API routes""" - # If it's an API route, it should have been handled by routers above - # If it's a static file request, try to serve it - if full_path.startswith("static/"): - file_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), full_path) - if os.path.exists(file_path): - return FileResponse(file_path) + """Serve Angular SPA static files and handle client-side routing""" + # Try to serve static file first (JS, CSS, images, etc.) + file_path = os.path.join(static_dir, full_path) + if os.path.exists(file_path) and os.path.isfile(file_path): + return FileResponse(file_path) - # For all other routes (Angular routes), serve index.html + # For all other routes (Angular client-side routes), serve index.html index_path = os.path.join(static_dir, "index.html") if os.path.exists(index_path): return FileResponse(index_path) diff --git a/docker-compose.yml b/docker-compose.yml index d9bdac4..0fdcc5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,8 @@ services: timeout: 5s retries: 5 - api: + app: + container_name: warehouse13-app build: . ports: - "8000:8000" diff --git a/API.md b/docs/API.md similarity index 100% rename from API.md rename to docs/API.md diff --git a/ARCHITECTURE.md b/docs/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to docs/ARCHITECTURE.md diff --git a/DEPLOYMENT.md b/docs/DEPLOYMENT.md similarity index 100% rename from DEPLOYMENT.md rename to docs/DEPLOYMENT.md diff --git a/FEATURES.md b/docs/FEATURES.md similarity index 100% rename from FEATURES.md rename to docs/FEATURES.md diff --git a/FRONTEND_SETUP.md b/docs/FRONTEND_SETUP.md similarity index 100% rename from FRONTEND_SETUP.md rename to docs/FRONTEND_SETUP.md diff --git a/SUMMARY.md b/docs/SUMMARY.md similarity index 100% rename from SUMMARY.md rename to docs/SUMMARY.md diff --git a/frontend/angular.json b/frontend/angular.json index 5784175..919a41c 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -93,5 +93,8 @@ } } } + }, + "cli": { + "analytics": false } } diff --git a/quickstart.bat b/quickstart.bat deleted file mode 100644 index d0f8808..0000000 --- a/quickstart.bat +++ /dev/null @@ -1,106 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -echo ========================================= -echo Warehouse13 - Quick Start -echo ========================================= -echo. - -REM Check if Docker is installed -where docker >nul 2>nul -if %errorlevel% neq 0 ( - echo Error: Docker is not installed. Please install Docker Desktop first. - echo Visit: https://www.docker.com/products/docker-desktop - pause - exit /b 1 -) - -REM Check if Docker Compose is available -where docker-compose >nul 2>nul -if %errorlevel% neq 0 ( - REM Try docker compose (new version) - docker compose version >nul 2>nul - if %errorlevel% neq 0 ( - echo Error: Docker Compose is not available. - echo Please ensure Docker Desktop is running. - pause - exit /b 1 - ) - set COMPOSE_CMD=docker compose -) else ( - set COMPOSE_CMD=docker-compose -) - -REM Create .env file if it doesn't exist -if not exist .env ( - echo Creating .env file from .env.example... - copy .env.example .env >nul - echo [OK] .env file created -) else ( - echo [OK] .env file already exists -) - -echo. -echo Building and starting services with Docker Compose... -%COMPOSE_CMD% up -d --build - -if %errorlevel% neq 0 ( - echo. - echo Error: Failed to start services. - echo Make sure Docker Desktop is running. - pause - exit /b 1 -) - -echo. -echo Waiting for services to be ready... -timeout /t 15 /nobreak >nul - -echo. -echo ========================================= -echo Services are running! -echo ========================================= -echo. -echo Web UI: http://localhost:8000 -echo API Docs: http://localhost:8000/docs -echo MinIO Console: http://localhost:9001 -echo Username: minioadmin -echo Password: minioadmin -echo. -echo To view logs: %COMPOSE_CMD% logs -f -echo To stop: %COMPOSE_CMD% down -echo. -echo ========================================= -echo Testing the API... -echo ========================================= -echo. - -REM Wait a bit more for API to be fully ready -timeout /t 5 /nobreak >nul - -REM Test health endpoint -curl -s http://localhost:8000/health | findstr "healthy" >nul 2>nul -if %errorlevel% equ 0 ( - echo [OK] API is healthy! - echo. - echo ========================================= - echo Open your browser to get started: - echo http://localhost:8000 - echo ========================================= -) else ( - echo [WARNING] API is not responding yet. - echo Please wait a moment and check http://localhost:8000 -) - -echo. -echo ========================================= -echo Setup complete! -echo ========================================= -echo. -echo Press any key to open the UI in your browser... -pause >nul - -REM Open browser -start http://localhost:8000 - -exit /b 0 diff --git a/quickstart.ps1 b/quickstart.ps1 index 920c584..03804f0 100644 --- a/quickstart.ps1 +++ b/quickstart.ps1 @@ -121,7 +121,7 @@ Write-Host "Useful Commands:" -ForegroundColor Cyan Write-Host " Generate seed data: " -NoNewline Write-Host "Use the 'Generate Seed Data' button in the UI" -ForegroundColor Yellow Write-Host " View logs: " -NoNewline -Write-Host "$composeCmd logs -f api" -ForegroundColor Yellow +Write-Host "$composeCmd logs -f app" -ForegroundColor Yellow Write-Host " Restart services: " -NoNewline Write-Host "$composeCmd restart" -ForegroundColor Yellow Write-Host " Stop all: " -NoNewline diff --git a/static/css/styles.css b/static/css/styles.css deleted file mode 100644 index edee773..0000000 --- a/static/css/styles.css +++ /dev/null @@ -1,564 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; - background: #0f172a; - min-height: 100vh; - padding: 20px; - color: #e2e8f0; -} - -.container { - max-width: 1400px; - margin: 0 auto; - background: #1e293b; - border-radius: 12px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); - overflow: hidden; -} - -header { - background: linear-gradient(135deg, #1e3a8a 0%, #4338ca 100%); - color: white; - padding: 30px; - display: flex; - justify-content: space-between; - align-items: center; -} - -header h1 { - font-size: 28px; - font-weight: 600; - display: flex; - align-items: center; - gap: 12px; -} - -.logo { - font-family: 'Courier New', monospace; - font-weight: 700; - font-size: 24px; - color: #60a5fa; - letter-spacing: -1px; - padding: 2px 4px; - border: 2px solid #60a5fa; - border-radius: 4px; - background: rgba(96, 165, 250, 0.1); -} - -.header-info { - display: flex; - gap: 10px; -} - -.badge { - background: rgba(255, 255, 255, 0.2); - padding: 6px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: 500; - text-transform: uppercase; - backdrop-filter: blur(10px); -} - -.tabs { - display: flex; - background: #0f172a; - border-bottom: 2px solid #334155; -} - -.tab-button { - flex: 1; - padding: 16px 24px; - background: none; - border: none; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s; - color: #94a3b8; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -.tab-button:hover { - background: #1e293b; - color: #e2e8f0; -} - -.tab-button.active { - background: #1e293b; - color: #60a5fa; - border-bottom: 3px solid #60a5fa; -} - -.tab-content { - display: none; - padding: 30px; -} - -.tab-content.active { - display: block; -} - -.toolbar { - display: flex; - gap: 10px; - margin-bottom: 20px; - align-items: center; -} - -.filter-inline { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background: #0f172a; - border-radius: 6px; - border: 1px solid #334155; - min-width: 250px; -} - -.filter-inline input { - flex: 1; - padding: 4px 8px; - background: transparent; - border: none; - color: #e2e8f0; - font-size: 14px; -} - -.filter-inline input:focus { - outline: none; -} - -.filter-inline input::placeholder { - color: #64748b; -} - -.btn-clear { - background: none; - border: none; - cursor: pointer; - padding: 4px; - border-radius: 4px; - color: #64748b; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.3s; -} - -.btn-clear:hover { - background: #334155; - color: #e2e8f0; -} - -.btn { - padding: 10px 20px; - border: none; - border-radius: 6px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.3s; - display: inline-flex; - align-items: center; - gap: 8px; -} - -.btn-primary { - background: #3b82f6; - color: white; -} - -.btn-primary:hover { - background: #2563eb; - transform: translateY(-1px); -} - -.btn-secondary { - background: #334155; - color: #e2e8f0; -} - -.btn-secondary:hover { - background: #475569; -} - -.btn-danger { - background: #ef4444; - color: white; -} - -.btn-danger:hover { - background: #dc2626; -} - -.btn-success { - background: #10b981; - color: white; -} - -.btn-large { - padding: 14px 28px; - font-size: 16px; -} - -.count-badge { - background: #1e3a8a; - color: #93c5fd; - padding: 8px 16px; - border-radius: 20px; - font-size: 13px; - font-weight: 600; - margin-left: auto; -} - -.table-container { - overflow-x: auto; - border: 1px solid #334155; - border-radius: 8px; - background: #0f172a; -} - -table { - width: 100%; - border-collapse: collapse; - font-size: 14px; -} - -thead { - background: #1e293b; -} - -th { - padding: 14px 12px; - text-align: left; - font-weight: 600; - color: #94a3b8; - border-bottom: 2px solid #334155; - white-space: nowrap; - text-transform: uppercase; - font-size: 12px; - letter-spacing: 0.5px; -} - -th.sortable { - cursor: pointer; - user-select: none; - transition: color 0.3s; -} - -th.sortable:hover { - color: #60a5fa; -} - -.sort-indicator { - display: inline-block; - margin-left: 5px; - font-size: 10px; - color: #64748b; -} - -th.sort-asc .sort-indicator::after { - content: '▲'; - color: #60a5fa; -} - -th.sort-desc .sort-indicator::after { - content: '▼'; - color: #60a5fa; -} - -td { - padding: 16px 12px; - border-bottom: 1px solid #1e293b; - color: #cbd5e1; -} - -tbody tr:hover { - background: #1e293b; -} - -.loading { - text-align: center; - color: #64748b; - padding: 40px !important; -} - -.result-badge { - padding: 4px 10px; - border-radius: 12px; - font-size: 12px; - font-weight: 600; - text-transform: uppercase; -} - -.result-pass { - background: #064e3b; - color: #6ee7b7; -} - -.result-fail { - background: #7f1d1d; - color: #fca5a5; -} - -.result-skip { - background: #78350f; - color: #fcd34d; -} - -.result-error { - background: #7f1d1d; - color: #fca5a5; -} - -.tag { - display: inline-block; - background: #1e3a8a; - color: #93c5fd; - padding: 3px 8px; - border-radius: 10px; - font-size: 11px; - margin: 2px; -} - -.file-type-badge { - background: #1e3a8a; - color: #93c5fd; - padding: 4px 8px; - border-radius: 6px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; -} - -.pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 20px; - margin-top: 20px; - padding: 20px; -} - -#page-info { - font-weight: 500; - color: #94a3b8; -} - -.upload-section, .query-section { - max-width: 800px; - margin: 0 auto; -} - -.form-group { - margin-bottom: 20px; -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; -} - -label { - display: block; - font-weight: 500; - color: #cbd5e1; - margin-bottom: 6px; - font-size: 14px; -} - -input[type="text"], -input[type="file"], -input[type="datetime-local"], -select, -textarea { - width: 100%; - padding: 10px 14px; - border: 1px solid #334155; - border-radius: 6px; - font-size: 14px; - font-family: inherit; - transition: border-color 0.3s; - background: #0f172a; - color: #e2e8f0; -} - -input:focus, -select:focus, -textarea:focus { - outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -small { - color: #64748b; - font-size: 12px; - display: block; - margin-top: 4px; -} - -#upload-status { - margin-top: 20px; - padding: 14px; - border-radius: 6px; - display: none; -} - -#upload-status.success { - background: #064e3b; - color: #6ee7b7; - display: block; -} - -#upload-status.error { - background: #7f1d1d; - color: #fca5a5; - display: block; -} - -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - backdrop-filter: blur(4px); -} - -.modal.active { - display: flex; - align-items: center; - justify-content: center; -} - -.modal-content { - background: #1e293b; - padding: 30px; - border-radius: 12px; - max-width: 700px; - max-height: 80vh; - overflow-y: auto; - position: relative; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); - border: 1px solid #334155; -} - -.close { - position: absolute; - right: 20px; - top: 20px; - font-size: 28px; - font-weight: bold; - color: #64748b; - cursor: pointer; - transition: color 0.3s; -} - -.close:hover { - color: #e2e8f0; -} - -.detail-row { - margin-bottom: 16px; - padding-bottom: 16px; - border-bottom: 1px solid #334155; -} - -.detail-row:last-child { - border-bottom: none; -} - -.detail-label { - font-weight: 600; - color: #94a3b8; - margin-bottom: 4px; -} - -.detail-value { - color: #cbd5e1; -} - -pre { - background: #0f172a; - padding: 12px; - border-radius: 6px; - overflow-x: auto; - font-size: 12px; - border: 1px solid #334155; -} - -code { - background: #0f172a; - padding: 2px 6px; - border-radius: 4px; - font-size: 12px; - color: #93c5fd; -} - -.action-buttons { - display: flex; - gap: 8px; -} - -.icon-btn { - background: none; - border: none; - cursor: pointer; - padding: 8px; - border-radius: 4px; - transition: all 0.3s; - color: #94a3b8; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.icon-btn:hover { - background: #334155; - color: #e2e8f0; - transform: scale(1.1); -} - -/* Ensure SVG icons inherit color */ -.icon-btn svg { - stroke: currentColor; -} - -@media (max-width: 768px) { - .form-row { - grid-template-columns: 1fr; - } - - header { - flex-direction: column; - gap: 15px; - text-align: center; - } - - .table-container { - font-size: 12px; - } - - th, td { - padding: 8px 6px; - } - - .toolbar { - flex-wrap: wrap; - } -} diff --git a/static/index.html b/static/index.html deleted file mode 100644 index a41a8ad..0000000 --- a/static/index.html +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - Warehouse13 - Test Artifact Data Lake - - - - -
-
-

-
- - -
-
- - - - -
-
- - - - - - -
- - - -
-
- -
- - - - - - - - - - - - - - - -
- Sim Source - - Artifacts - - Date - - Uploaded By - Actions
Loading artifacts...
-
- - -
- - -
-
-

Upload Artifact

-
-
- - - Supported: CSV, JSON, binary files, PCAP -
- -
-
- - -
-
- - -
-
- -
-
- - - Use same ID for multiple artifacts from same source -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
- - -
- -
- - -
- -
- - -
- - -
-
-
-
- - -
-
-

Query Artifacts

-
-
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- - - -
-
-
- - - -
- - - - - diff --git a/static/js/app.js b/static/js/app.js deleted file mode 100644 index 07cb8fd..0000000 --- a/static/js/app.js +++ /dev/null @@ -1,592 +0,0 @@ -// API Base URL -const API_BASE = '/api/v1'; - -// Pagination -let currentPage = 1; -let pageSize = 25; -let totalArtifacts = 0; - -// Auto-refresh -let autoRefreshEnabled = true; -let autoRefreshInterval = null; -const REFRESH_INTERVAL_MS = 5000; // 5 seconds - -// Sorting and filtering -let allArtifacts = []; // Store all artifacts for client-side sorting/filtering -let currentSortColumn = null; -let currentSortDirection = 'asc'; - -// Load API info on page load -window.addEventListener('DOMContentLoaded', () => { - loadApiInfo(); - loadArtifacts(); - startAutoRefresh(); -}); - -// Load API information -async function loadApiInfo() { - try { - const response = await fetch('/api'); - const data = await response.json(); - - document.getElementById('deployment-mode').textContent = `Mode: ${data.deployment_mode}`; - document.getElementById('storage-backend').textContent = `Storage: ${data.storage_backend}`; - } catch (error) { - console.error('Error loading API info:', error); - } -} - -// Load artifacts -async function loadArtifacts(limit = pageSize, offset = 0) { - try { - const response = await fetch(`${API_BASE}/artifacts/?limit=${limit}&offset=${offset}`); - const artifacts = await response.json(); - - allArtifacts = artifacts; // Store for sorting/filtering - displayArtifacts(artifacts); - updatePagination(artifacts.length); - } catch (error) { - console.error('Error loading artifacts:', error); - document.getElementById('artifacts-tbody').innerHTML = ` - - Error loading artifacts: ${error.message} - - `; - } -} - -// Display artifacts in table -function displayArtifacts(artifacts) { - const tbody = document.getElementById('artifacts-tbody'); - - if (artifacts.length === 0) { - tbody.innerHTML = 'No artifacts found. Upload some files to get started!'; - document.getElementById('artifact-count').textContent = '0 artifacts'; - return; - } - - // Apply current sort if active - let displayedArtifacts = artifacts; - if (currentSortColumn) { - displayedArtifacts = applySorting([...artifacts]); - } - - tbody.innerHTML = displayedArtifacts.map(artifact => ` - - ${artifact.sim_source_id || artifact.test_suite || '-'} - - - ${escapeHtml(artifact.filename)} - - ${artifact.tags && artifact.tags.length > 0 ? `
${formatTags(artifact.tags)}
` : ''} - - ${formatDate(artifact.created_at)} - ${artifact.test_name || '-'} - -
- - -
- - - `).join(''); - - document.getElementById('artifact-count').textContent = `${displayedArtifacts.length} artifacts`; - - // Re-initialize Lucide icons for dynamically added content - lucide.createIcons(); -} - -// Format result badge -function formatResult(result) { - if (!result) return '-'; - const className = `result-badge result-${result}`; - return `${result}`; -} - -// Format tags -function formatTags(tags) { - if (!tags || tags.length === 0) return '-'; - return tags.map(tag => `${escapeHtml(tag)}`).join(' '); -} - -// Format bytes -function formatBytes(bytes) { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; -} - -// Format date -function formatDate(dateString) { - const date = new Date(dateString); - return date.toLocaleString(); -} - -// Escape HTML -function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -// Show artifact detail -async function showDetail(id) { - try { - const response = await fetch(`${API_BASE}/artifacts/${id}`); - const artifact = await response.json(); - - const detailContent = document.getElementById('detail-content'); - detailContent.innerHTML = ` -
-
ID
-
${artifact.id}
-
-
-
Filename
-
${escapeHtml(artifact.filename)}
-
-
-
File Type
-
${artifact.file_type}
-
-
-
Size
-
${formatBytes(artifact.file_size)}
-
-
-
Storage Path
-
${artifact.storage_path}
-
-
-
Uploaded By
-
${artifact.test_name || '-'}
-
-
-
Sim Source
-
${artifact.test_suite || '-'}
-
-
-
Test Result
-
${formatResult(artifact.test_result)}
-
- ${artifact.test_config ? ` -
-
Test Config
-
${JSON.stringify(artifact.test_config, null, 2)}
-
- ` : ''} - ${artifact.custom_metadata ? ` -
-
Custom Metadata
-
${JSON.stringify(artifact.custom_metadata, null, 2)}
-
- ` : ''} - ${artifact.description ? ` -
-
Description
-
${escapeHtml(artifact.description)}
-
- ` : ''} - ${artifact.tags && artifact.tags.length > 0 ? ` -
-
Tags
-
${formatTags(artifact.tags)}
-
- ` : ''} -
-
Version
-
${artifact.version || '-'}
-
-
-
Created
-
${formatDate(artifact.created_at)}
-
-
-
Updated
-
${formatDate(artifact.updated_at)}
-
-
- - -
- `; - - document.getElementById('detail-modal').classList.add('active'); - - // Re-initialize Lucide icons for modal content - lucide.createIcons(); - } catch (error) { - alert('Error loading artifact details: ' + error.message); - } -} - -// Close detail modal -function closeDetailModal() { - document.getElementById('detail-modal').classList.remove('active'); -} - -// Download artifact -async function downloadArtifact(id, filename) { - try { - const response = await fetch(`${API_BASE}/artifacts/${id}/download`); - const blob = await response.blob(); - - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (error) { - alert('Error downloading artifact: ' + error.message); - } -} - -// Delete artifact -async function deleteArtifact(id) { - if (!confirm('Are you sure you want to delete this artifact? This cannot be undone.')) { - return; - } - - try { - const response = await fetch(`${API_BASE}/artifacts/${id}`, { - method: 'DELETE' - }); - - if (response.ok) { - loadArtifacts((currentPage - 1) * pageSize, pageSize); - alert('Artifact deleted successfully'); - } else { - throw new Error('Failed to delete artifact'); - } - } catch (error) { - alert('Error deleting artifact: ' + error.message); - } -} - -// Upload artifact -async function uploadArtifact(event) { - event.preventDefault(); - - const form = event.target; - const formData = new FormData(); - - // Add file - const fileInput = document.getElementById('file'); - formData.append('file', fileInput.files[0]); - - // Add optional fields - const fields = ['test_name', 'test_suite', 'test_result', 'version', 'description', 'sim_source_id']; - fields.forEach(field => { - const value = form.elements[field]?.value; - if (value) formData.append(field, value); - }); - - // Add tags (convert comma-separated to JSON array) - const tags = document.getElementById('tags').value; - if (tags) { - const tagsArray = tags.split(',').map(t => t.trim()).filter(t => t); - formData.append('tags', JSON.stringify(tagsArray)); - } - - // Add JSON fields - const testConfig = document.getElementById('test-config').value; - if (testConfig) { - try { - JSON.parse(testConfig); // Validate - formData.append('test_config', testConfig); - } catch (e) { - showUploadStatus('Invalid Test Config JSON', false); - return; - } - } - - const customMetadata = document.getElementById('custom-metadata').value; - if (customMetadata) { - try { - JSON.parse(customMetadata); // Validate - formData.append('custom_metadata', customMetadata); - } catch (e) { - showUploadStatus('Invalid Custom Metadata JSON', false); - return; - } - } - - try { - const response = await fetch(`${API_BASE}/artifacts/upload`, { - method: 'POST', - body: formData - }); - - if (response.ok) { - const artifact = await response.json(); - showUploadStatus(`Successfully uploaded: ${artifact.filename}`, true); - form.reset(); - loadArtifacts(); - } else { - const error = await response.json(); - throw new Error(error.detail || 'Upload failed'); - } - } catch (error) { - showUploadStatus('Upload failed: ' + error.message, false); - } -} - -// Show upload status -function showUploadStatus(message, success) { - const status = document.getElementById('upload-status'); - status.textContent = message; - status.className = success ? 'success' : 'error'; - - setTimeout(() => { - status.style.display = 'none'; - }, 5000); -} - -// Query artifacts -async function queryArtifacts(event) { - event.preventDefault(); - - const query = {}; - - const filename = document.getElementById('q-filename').value; - if (filename) query.filename = filename; - - const fileType = document.getElementById('q-type').value; - if (fileType) query.file_type = fileType; - - const testName = document.getElementById('q-test-name').value; - if (testName) query.test_name = testName; - - const suite = document.getElementById('q-suite').value; - if (suite) query.test_suite = suite; - - const result = document.getElementById('q-result').value; - if (result) query.test_result = result; - - const tags = document.getElementById('q-tags').value; - if (tags) { - query.tags = tags.split(',').map(t => t.trim()).filter(t => t); - } - - const startDate = document.getElementById('q-start-date').value; - if (startDate) query.start_date = new Date(startDate).toISOString(); - - const endDate = document.getElementById('q-end-date').value; - if (endDate) query.end_date = new Date(endDate).toISOString(); - - query.limit = 100; - query.offset = 0; - - try { - const response = await fetch(`${API_BASE}/artifacts/query`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(query) - }); - - const artifacts = await response.json(); - - // Switch to artifacts tab and display results - showTab('artifacts'); - displayArtifacts(artifacts); - } catch (error) { - alert('Query failed: ' + error.message); - } -} - -// Clear query form -function clearQuery() { - document.getElementById('query-form').reset(); -} - -// Generate seed data -async function generateSeedData() { - const count = prompt('How many artifacts to generate? (1-100)', '10'); - if (!count) return; - - const num = parseInt(count); - if (isNaN(num) || num < 1 || num > 100) { - alert('Please enter a number between 1 and 100'); - return; - } - - try { - const response = await fetch(`/api/v1/seed/generate/${num}`, { - method: 'POST' - }); - - const result = await response.json(); - - if (response.ok) { - alert(result.message); - loadArtifacts(); - } else { - throw new Error(result.detail || 'Generation failed'); - } - } catch (error) { - alert('Error generating seed data: ' + error.message); - } -} - -// Tab navigation -function showTab(tabName) { - // Hide all tabs - document.querySelectorAll('.tab-content').forEach(tab => { - tab.classList.remove('active'); - }); - document.querySelectorAll('.tab-button').forEach(btn => { - btn.classList.remove('active'); - }); - - // Show selected tab - document.getElementById(tabName + '-tab').classList.add('active'); - event.target.classList.add('active'); -} - -// Pagination -function updatePagination(count) { - const pageInfo = document.getElementById('page-info'); - pageInfo.textContent = `Page ${currentPage}`; - - document.getElementById('prev-btn').disabled = currentPage === 1; - document.getElementById('next-btn').disabled = count < pageSize; -} - -function previousPage() { - if (currentPage > 1) { - currentPage--; - loadArtifacts(pageSize, (currentPage - 1) * pageSize); - } -} - -function nextPage() { - currentPage++; - loadArtifacts(pageSize, (currentPage - 1) * pageSize); -} - -// Auto-refresh functions -function startAutoRefresh() { - if (autoRefreshInterval) { - clearInterval(autoRefreshInterval); - } - - if (autoRefreshEnabled) { - autoRefreshInterval = setInterval(() => { - // Only refresh if on the artifacts tab - const artifactsTab = document.getElementById('artifacts-tab'); - if (artifactsTab && artifactsTab.classList.contains('active')) { - loadArtifacts(pageSize, (currentPage - 1) * pageSize); - } - }, REFRESH_INTERVAL_MS); - } -} - -function toggleAutoRefresh() { - autoRefreshEnabled = !autoRefreshEnabled; - - const toggleBtn = document.getElementById('auto-refresh-toggle'); - if (autoRefreshEnabled) { - toggleBtn.textContent = 'Auto-refresh: ON'; - toggleBtn.classList.remove('btn-secondary'); - toggleBtn.classList.add('btn-success'); - startAutoRefresh(); - } else { - toggleBtn.textContent = 'Auto-refresh: OFF'; - toggleBtn.classList.remove('btn-success'); - toggleBtn.classList.add('btn-secondary'); - if (autoRefreshInterval) { - clearInterval(autoRefreshInterval); - autoRefreshInterval = null; - } - } -} - -// Apply sorting to artifacts array -function applySorting(artifacts) { - if (!currentSortColumn) return artifacts; - - return artifacts.sort((a, b) => { - let aVal = a[currentSortColumn] || ''; - let bVal = b[currentSortColumn] || ''; - - // Handle date sorting - if (currentSortColumn === 'created_at') { - aVal = new Date(aVal).getTime(); - bVal = new Date(bVal).getTime(); - } else { - // String comparison (case insensitive) - aVal = String(aVal).toLowerCase(); - bVal = String(bVal).toLowerCase(); - } - - if (aVal < bVal) return currentSortDirection === 'asc' ? -1 : 1; - if (aVal > bVal) return currentSortDirection === 'asc' ? 1 : -1; - return 0; - }); -} - -// Sorting functionality -function sortTable(column) { - // Toggle sort direction if clicking same column - if (currentSortColumn === column) { - currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; - } else { - currentSortColumn = column; - currentSortDirection = 'asc'; - } - - // Update sort indicators - document.querySelectorAll('th.sortable').forEach(th => { - th.classList.remove('sort-asc', 'sort-desc'); - }); - - const sortedHeader = event.target.closest('th'); - sortedHeader.classList.add(`sort-${currentSortDirection}`); - - // Apply filter and sort - filterTable(); -} - -// Filtering functionality - searches across all columns -function filterTable() { - const searchTerm = document.getElementById('filter-search').value.toLowerCase(); - - const filteredArtifacts = allArtifacts.filter(artifact => { - if (!searchTerm) return true; - - // Search across all relevant fields - const searchableText = [ - artifact.test_suite || '', - artifact.filename || '', - artifact.test_name || '', - formatDate(artifact.created_at) - ].join(' ').toLowerCase(); - - return searchableText.includes(searchTerm); - }); - - displayArtifacts(filteredArtifacts); -} - -// Clear all filters -function clearFilters() { - document.getElementById('filter-search').value = ''; - filterTable(); -} From 5920bf1617c158616ca97d59e292cfd3203e74a9 Mon Sep 17 00:00:00 2001 From: pratik Date: Thu, 16 Oct 2025 13:49:12 -0500 Subject: [PATCH 3/3] Update MR --- build-for-airgap.sh | 70 ++++++++++++++++++++++++++++++++++++++++++++ quickstart-airgap.sh | 9 ++++-- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 build-for-airgap.sh diff --git a/build-for-airgap.sh b/build-for-airgap.sh new file mode 100644 index 0000000..6279808 --- /dev/null +++ b/build-for-airgap.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -e + +echo "=========================================" +echo "Building Angular for Air-Gapped Deployment" +echo "=========================================" +echo "" + +# Check if we're in the right directory +if [ ! -f "docker-compose.yml" ]; then + echo "Error: Must run from project root directory" + exit 1 +fi + +# Check if node is installed +if ! command -v node &> /dev/null; then + echo "Error: Node.js is not installed. Please install Node.js 24+ first." + exit 1 +fi + +# Check if npm is installed +if ! command -v npm &> /dev/null; then + echo "Error: npm is not installed. Please install npm first." + exit 1 +fi + +echo "Step 1/3: Installing dependencies..." +cd frontend +npm install + +echo "" +echo "Step 2/3: Building Angular production bundle..." +npm run build:prod + +echo "" +echo "Step 3/3: Copying to static directory..." +if [ -d "dist/frontend/browser" ]; then + echo "✓ Build successful!" + echo "✓ Output: frontend/dist/frontend/browser" + + # Copy to static directory for local FastAPI serving + cd .. + rm -rf static/* + cp -r frontend/dist/frontend/browser/* static/ + echo "✓ Copied to static/ directory" + + ls -lh static/ | head -10 +else + echo "✗ Build failed - output directory not found" + exit 1 +fi + +echo "" +echo "=========================================" +echo "Build Complete!" +echo "=========================================" +echo "" +echo "The Angular app has been built and copied to static/" +echo "You can now:" +echo "" +echo "1. Run locally with FastAPI:" +echo " uvicorn app.main:app --reload" +echo " Access at: http://localhost:8000" +echo "" +echo "2. Deploy with Docker:" +echo " docker-compose up -d --build" +echo " (Docker will rebuild Angular during build)" +echo "" +echo "=========================================" diff --git a/quickstart-airgap.sh b/quickstart-airgap.sh index 4c6c03f..0f6097f 100755 --- a/quickstart-airgap.sh +++ b/quickstart-airgap.sh @@ -28,12 +28,17 @@ if ! command -v docker-compose &> /dev/null; then exit 1 fi -echo "Step 1: Starting Docker containers..." +echo "Step 1: Building Angular frontend locally..." +echo "===========================================" +./build-for-airgap.sh + +echo "" +echo "Step 2: Starting Docker containers..." echo "===========================================" docker-compose up -d --build echo "" -echo "Step 2: Waiting for services to be ready..." +echo "Step 3: Waiting for services to be ready..." sleep 15 echo ""