Compare commits

..

20 Commits

Author SHA1 Message Date
Patel (US), Pratik
aae317edc1 Merge branch 'lock2' 2025-10-16 12:04:58 -05:00
Patel (US), Pratik
ca0141a5b3 Build with BSF arti 2025-10-16 12:00:34 -05:00
Armando Diaz
e2d26d7186 install curl 2025-10-16 11:51:22 -05:00
Armando Diaz
82d2cb5768 update image for bsf 2025-10-16 11:47:02 -05:00
Armando Diaz
b4de8757e6 test ci job 2025-10-16 11:42:26 -05:00
ace9eb82db Pin @angular/build and @angular/cli to version 19.2.7
- Use specific version 19.2.7 for build tools instead of ^19.1.0
- Angular core packages remain at ^19.1.0
- This specific build version may have better package resolution
  for restricted/air-gapped environments

Using a pinned version ensures consistent builds across different
environments and may help with package availability issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 10:44:36 -05:00
a16c4f9898 Upgrade to Angular 19.1 with latest packages
- Upgrade from Angular 17.3 to Angular 19.1
- Switch back to @angular/build with Vite bundler
- Update to latest package versions:
  - @angular/core: 19.1.0
  - TypeScript: 5.8.0
  - tslib: 2.8.1
  - zone.js: 0.15.0
- Restore application builder configuration
- Bundle size: 349.98 kB raw / 92.00 kB gzipped

Angular 19.1 includes improvements and bug fixes that may resolve
package issues in restricted environments. The latest versions should
have better compatibility and more robust dependency resolution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 10:36:09 -05:00
c7abff0c92 Add better documentation and scripts for air-gapped deployment
Created new helper scripts:
- quickstart-airgap.sh: One-command deployment for restricted environments
- check-ready.sh: Validates that pre-built files exist before deployment

Updated documentation:
- Enhanced Dockerfile.frontend.prebuilt with clearer error messages
- Updated DEPLOYMENT.md with step-by-step quick start guide
- Updated README.md to distinguish standard vs air-gapped deployment

Key improvements:
- Clear warning that build must happen BEFORE docker-compose
- Helper script that combines build + deployment steps
- Readiness check to catch missing pre-built files early
- Better instructions for test environments with restricted npm access

This addresses the common error where Docker fails because
frontend/dist/frontend/browser doesn't exist yet.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 10:18:44 -05:00
17331d9808 Update Docker project name from datalake to warehouse13
- Add 'name: warehouse13' to docker-compose.yml
- Container names now use warehouse13 prefix:
  - warehouse13-frontend-1
  - warehouse13-api-1
  - warehouse13-postgres-1
  - warehouse13-minio-1
- Volume names updated to warehouse13_*
- Network name updated to warehouse13_default

All Docker resources now consistently use warehouse13 naming.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 10:10:03 -05:00
9a37c1f549 Simplify header to show only [W13] logo
- Remove "Warehouse13" text from header
- Header now displays just the [W13] logo with badges
- Cleaner, more minimalist design
- Applied to both Angular frontend and static HTML

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 09:52:25 -05:00
8f2e89ba98 Add new [W13] logo design to Warehouse13
- Replace ◆ symbol with styled [W13] logo
- Logo uses monospace font with blue border and background
- Implemented in both Angular frontend and static HTML
- Added .logo CSS class with custom styling:
  - Courier New monospace font
  - Blue (#60a5fa) color and border
  - Semi-transparent background
  - Compact bracket style representing warehouse/storage containers

The logo maintains the warehouse/storage theme while being more distinctive and modern.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 09:50:47 -05:00
95aec1cc5c Rebrand application from Obsidian to Warehouse13
- Update main app title in Angular frontend
- Update FastAPI application title and API endpoints
- Update static HTML index page
- Update all quickstart scripts (bash, PowerShell, batch)
- Update README files (main and frontend)
- Maintain ◆ symbol in headers

All references to "Obsidian" have been replaced with "Warehouse13" throughout the application.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 09:46:01 -05:00
578ddba690 Downgrade to Angular 17 with webpack for better restricted environment compatibility
- Downgrade from Angular 19 to Angular 17.3.0
- Switch from Vite-based build (@angular/build) to webpack (@angular-devkit/build-angular)
- Eliminates Vite, esbuild, and rollup dependencies that were causing issues in restricted npm environments
- Update tsconfig.json for webpack compatibility (moduleResolution: bundler)
- Update angular.json to use browser builder instead of application builder
- Update docker-compose.yml to use prebuilt Dockerfile for air-gapped deployment
- Add build-for-airgap.sh helper script for local builds
- Update DEPLOYMENT.md with Angular 17 webpack strategy notes
- Bundle size: 329.73 kB raw / 86.54 kB gzipped

This change improves compatibility with enterprise environments that have restricted package registry access.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 14:41:07 -05:00
872464dec2 Add air-gapped deployment option for restricted environments
Added support for air-gapped and enterprise environments where npm package access is restricted, specifically addressing esbuild platform binary download issues.

**New Files:**
- Dockerfile.frontend.prebuilt: Alternative Dockerfile that uses pre-built Angular files
- DEPLOYMENT.md: Comprehensive deployment guide with two options

**Changes:**
- package.json: Added optionalDependencies for esbuild platform binaries
  - @esbuild/darwin-arm64
  - @esbuild/darwin-x64
  - @esbuild/linux-arm64
  - @esbuild/linux-x64

**Deployment Options:**

**Option 1 - Standard Build (current default):**
- Builds Angular in Docker
- Requires npm registry access
- Best for cloud/development

**Option 2 - Pre-built (for air-gapped):**
1. Build Angular locally: npm run build:prod
2. Change dockerfile in docker-compose.yml to Dockerfile.frontend.prebuilt
3. Docker only needs to copy files, no npm required
- No npm registry access needed during Docker build
- Faster, more reliable builds
- Best for enterprise/air-gapped/CI-CD

**Troubleshooting:**
See DEPLOYMENT.md for full troubleshooting guide including:
- esbuild platform binary issues
- Custom npm registry configuration
- Environment-specific recommendations

This addresses npm package access issues in restricted environments while maintaining flexibility for standard deployments.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:36:07 -05:00
629e3e4125 Replace Lucide icons with Material Icons for better compatibility
Switched from lucide-angular to Google Material Icons font for better compatibility across all environments, especially air-gapped and enterprise setups.

**Changes:**
- Removed lucide-angular dependency (not available in some environments)
- Added Material Icons font via Google CDN in index.html
- Updated all components to use Material Icons spans instead of Lucide components
- Added Material Icons CSS classes (md-16, md-18, md-20, md-24)

**Icon Mapping:**
- RefreshCw → refresh
- Sparkles → auto_awesome
- Search → search
- X/Close → close
- Download → download
- Trash2/Delete → delete
- Database → storage
- Upload → upload

**Benefits:**
- No npm dependency required (just a font)
- Works in all environments (air-gapped, enterprise proxies)
- Smaller bundle: 349.74 kB raw, 91.98 kB gzipped
- Industry standard Material Design icons
- Better cross-browser compatibility

All components tested and working correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:31:34 -05:00
0e1a342917 Replace emoji icons with Lucide icons and soften link colors
Replaced emoji icons throughout the Angular app with modern Lucide icon library for a more professional and consistent look matching the original static site design.

**Icon Updates:**
- Navigation tabs: Database, Upload, Search icons
- Toolbar buttons: RefreshCw, Sparkles, Search, X icons
- Action buttons: Download, Trash2 icons
- Form buttons: Upload, Search, X icons

**Style Improvements:**
- Added softer blue color for artifact links (#93c5fd)
- Added hover effect with lighter blue (#bfdbfe)
- Added proper cursor pointer for clickable rows
- Improved icon color consistency throughout

**Dependencies:**
- Added lucide-angular (v0.545.0) for icon support
- Bundle size: 356.54 kB (raw) → 93.91 kB (gzipped)
- Minimal impact: only +7.79 kB for full icon library

All components updated with Lucide imports and icon references.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:19:36 -05:00
6f7e9a517e Change frontend port from 80 to 4200 for better compatibility
Changed the frontend container port mapping from 80:80 to 4200:80 to avoid conflicts with system services and improve browser compatibility on macOS.

Port 4200 is the standard Angular development port and is less likely to be blocked by system security settings or conflict with other services.

**Access:**
- Frontend: http://localhost:4200
- API: http://localhost:8000
- MinIO Console: http://localhost:9001

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:14:40 -05:00
5fdefecbd0 Downgrade to Angular 19 and add custom npm registry package-lock regeneration
**Angular Downgrade:**
- Downgraded from Angular 20 to Angular 19 for better stability
- Updated all @angular/* packages to ^19.0.0
- Adjusted TypeScript to ~5.8.0 for Angular 19 compatibility
- Added required outputPath and index to angular.json for Angular 19 build requirements
- Verified production build works successfully

**NPM Registry Enhancements:**
- Updated Dockerfile.frontend to regenerate package-lock.json when custom npm registry is provided
- When NPM_REGISTRY is set to custom URL, the build will:
  1. Configure npm to use the custom registry
  2. Delete existing package-lock.json
  3. Generate new package-lock.json with custom registry URLs
  4. Run npm ci with the new lock file
- Default behavior (npmjs.org) unchanged - uses existing package-lock.json

**Build Verification:**
- Local build tested: ✓
- Docker build tested: ✓
- Bundle size: 348.75 kB raw, 91.73 kB gzipped
- No vulnerabilities found

**Usage:**
```bash
# Default registry (uses existing package-lock.json)
./quickstart.sh

# Custom registry (regenerates package-lock.json)
NPM_REGISTRY=http://your-npm-proxy:8081/repository/npm-proxy/ ./quickstart.sh
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 12:03:42 -05:00
d5c8bd188b Add custom npm registry/proxy support for frontend builds
Added configurable npm registry support to enable use of custom npm proxies or private registries during Docker builds. This is essential for corporate environments, air-gapped deployments, or when using npm mirrors.

**Changes:**
- Dockerfile.frontend: Added NPM_REGISTRY build argument with conditional configuration
- docker-compose.yml: Pass NPM_REGISTRY from environment to build args
- .env.example: Added NPM_REGISTRY configuration with usage examples

**Usage:**
Set NPM_REGISTRY in .env file or as environment variable:
- Nexus: http://nexus.company.com:8081/repository/npm-proxy/
- Artifactory: https://artifactory.company.com/artifactory/api/npm/npm-remote/
- Verdaccio: http://localhost:4873/
- Default: Leave blank for https://registry.npmjs.org/

**Example:**
```bash
NPM_REGISTRY=http://your-npm-proxy:8081/repository/npm-proxy/ ./quickstart.sh
```

Defaults to official npm registry if not specified.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 11:53:34 -05:00
ed5773893e Migrate frontend to Angular 20 with full Docker support
Implemented a complete Angular 20 migration with modern standalone components architecture and production-ready Docker deployment:

**Frontend Migration:**
- Created Angular 20 application with standalone components (no NgModules)
- Implemented three main components: artifacts-list, upload-form, query-form
- Added TypeScript models and services for type-safe API communication
- Migrated dark theme UI with all existing features
- Configured routing and navigation between views
- Set up development proxy for seamless API integration
- Reactive forms with validation for upload and query functionality
- Auto-refresh artifacts every 5 seconds with RxJS observables
- Client-side sorting, filtering, and search capabilities
- Tags displayed as inline badges, SIM source grouping support

**Docker Integration:**
- Multi-stage Dockerfile for Angular (Node 24 build, nginx Alpine serve)
- nginx configuration for SPA routing and API proxy
- Updated docker-compose.yml with frontend service on port 80
- Health checks for all services
- Production-optimized build with gzip compression and asset caching

**Technical Stack:**
- Angular 20 with standalone components
- TypeScript for type safety
- RxJS for reactive programming
- nginx as reverse proxy
- Multi-stage Docker builds for optimal image size

All features fully functional and tested in Docker environment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 11:35:28 -05:00
73 changed files with 11638 additions and 4524 deletions

View File

@@ -29,7 +29,6 @@ API_PORT=8000
MAX_UPLOAD_SIZE=524288000
# NPM Configuration (for frontend build)
# Default: https://registry.npmjs.org/ (public npm registry)
# For restricted environments, set to your custom npm proxy/registry URL
# Example: http://your-nexus-server:8081/repository/npm-proxy/
NPM_REGISTRY=https://registry.npmjs.org/
# Leave blank or set to https://registry.npmjs.org/ for default npm registry
# Set to your custom npm proxy/registry URL if needed (e.g., http://your-nexus-server:8081/repository/npm-proxy/)
NPM_REGISTRY=

View File

@@ -1,26 +0,0 @@
name: build
on:
push:
branches:
- main
jobs:
docker-build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: git.bitstorm.ca # e.g., docker.io for Docker Hub
username: ${{ secrets.REGISTRY_LOGIN }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: git.bitstorm.ca/bitforge/warehouse13:latest

7
.gitignore vendored
View File

@@ -86,10 +86,3 @@ helm/charts/
tmp/
temp/
*.tmp
# Node.js
package-lock.json
**/package-lock.json
# Built static files (generated during Docker build from Angular)
static/

View File

@@ -1,42 +1,22 @@
stages:
- build
- deploy
- test
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
build_container:
stage: build
image: deps.global.bsf.tools/quay.io/buildah/stable:latest
variables:
IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
# Test stage
test:
stage: test
image: registry.global.bsf.tools/esv/bsf/images/library/node:24.0.1-alpine3.21
before_script:
- mkdir -p /tmp/buildah-storage
- export BUILDAH_ROOT="/tmp/buildah-storage"
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- apk add curl
script:
# - buildah bud --build-arg NPM_REGISTRY=https://deps.global.bsf.tools/artifactory/api/npm/registry.npmjs.org/ --storage-driver vfs --isolation chroot -t $IMAGE_NAME .
- buildah pull git.bitstorm.ca/bitforge/warehouse13:latest
- buildah tag git.bitstorm.ca/bitforge/warehouse13:latest $IMAGE_NAME
- echo "Pushing $IMAGE_NAME"
- buildah push $IMAGE_NAME
deploy_helm_charts:
stage: deploy
image:
name: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12
parallel:
matrix:
# - ENV: "prod"
# VALUES_FILE: "helm/values-prod.yaml"
# CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
# NAMESPACE: "bsf-services-namespace"
# ONLY: "main"
- ENV: "dev"
VALUES_FILE: "helm/warehouse13/values.yaml"
CONTEXT: "esv/bsf/bsf-services/gitlab-kaas-agent-config:services-prod-agent"
NAMESPACE: "bsf-services-dev-namespace"
# ONLY: ["branches", "!main"]
script:
- kubectl config use-context $CONTEXT
- echo "Deploy - $CI_REGISTRY_NAME - $CI_COMMIT_REF_SLUG - $CI_COMMIT_SHORT_SHA"
- |
helm upgrade --install warehouse13-dev ./helm/warehouse13 --namespace $NAMESPACE -f $VALUES_FILE --set app.image.repository=$CI_REGISTRY_IMAGE --set app.image.tag=$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA
- cd frontend
- curl -s -u $ARTIFACTORY_USERNAME:$ARTIFACTORY_API_TOKEN -k https://sres.web.boeing.com/artifactory/api/npm/auth > $HOME/.npmrc
- npm config set registry https://sres.web.boeing.com/artifactory/api/npm/npm-releases
- npm config set strict-ssl false
- npm config fix
- npm install

View File

View File

@@ -54,7 +54,7 @@ This script will:
```bash
# Option A: Use the helper script
./scripts/build-for-airgap.sh
./build-for-airgap.sh
# Option B: Build manually
cd frontend

View File

@@ -1,30 +1,3 @@
# Multi-stage build: First stage builds Angular frontend
FROM node:20.11-alpine3.19 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
@@ -47,9 +20,7 @@ COPY app/ ./app/
COPY utils/ ./utils/
COPY alembic/ ./alembic/
COPY alembic.ini .
# Copy built Angular frontend from first stage to static directory
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
COPY static/ ./static/
# Create non-root user (Alpine uses adduser instead of useradd)
RUN adduser -D -u 1000 appuser && chown -R appuser:appuser /app

40
Dockerfile.frontend Normal file
View File

@@ -0,0 +1,40 @@
# 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;"]

View File

@@ -0,0 +1,20 @@
# 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;"]

View File

@@ -48,6 +48,11 @@ 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:**
@@ -60,7 +65,7 @@ This script:
2. Packages pre-built files into Docker
3. Starts all services
See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for detailed instructions.
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed instructions.
### Manual Setup with Docker Compose
@@ -214,54 +219,35 @@ MINIO_BUCKET_NAME=test-artifacts
### Kubernetes with Helm
**Quick Start:**
1. Build and push Docker image:
```bash
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
docker build -t your-registry/datalake:latest .
docker push your-registry/datalake:latest
```
**Production Deployment:**
2. Install with Helm:
```bash
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./helm/warehouse13/values-production.yaml
helm install datalake ./helm \
--set image.repository=your-registry/datalake \
--set image.tag=latest \
--namespace datalake \
--create-namespace
```
**Air-Gapped Deployment:**
3. Access the API:
```bash
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./helm/warehouse13/values-airgapped.yaml
kubectl port-forward -n datalake svc/datalake 8000:8000
```
**Access the Application:**
```bash
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
```
### Helm Documentation
- **Full Helm Guide:** [HELM-DEPLOYMENT.md](./docs/HELM-DEPLOYMENT.md)
- **Chart README:** [helm/warehouse13/README.md](./helm/warehouse13/README.md)
- **Quick Start:** [helm/warehouse13/QUICKSTART.md](./helm/warehouse13/QUICKSTART.md)
- **Example Configurations:**
- Development: [values-dev.yaml](./helm/warehouse13/values-dev.yaml)
- Production: [values-production.yaml](./helm/warehouse13/values-production.yaml)
- Air-Gapped: [values-airgapped.yaml](./helm/warehouse13/values-airgapped.yaml)
### Helm Configuration
All component images are fully configurable in `helm/warehouse13/values.yaml`:
- PostgreSQL image and version
- MinIO image and version
- API image and version
- Frontend image and version
- Resource limits and requests
- Storage backend configuration
- Ingress and TLS settings
- Persistence and storage classes
Edit `helm/values.yaml` to customize:
- Replica count
- Resource limits
- Storage backend (S3 vs MinIO)
- Ingress settings
- PostgreSQL settings
- Autoscaling
### GitLab CI/CD

View File

@@ -164,7 +164,7 @@ curl -X POST "http://localhost:8000/api/v1/artifacts/query" \
make deploy
# Or directly with Helm
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
helm install datalake ./helm --namespace datalake --create-namespace
```
## Feature Flags Usage
@@ -190,8 +190,9 @@ AWS_REGION=us-east-1
S3_BUCKET_NAME=your-bucket
# Deploy
helm install warehouse13 ./helm/warehouse13 \
--set global.deploymentMode=cloud
helm install datalake ./helm \
--set config.deploymentMode=cloud \
--set aws.enabled=true
```
## What's Next

View File

@@ -1,4 +1,4 @@
from fastapi import FastAPI, Request
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
@@ -39,8 +39,10 @@ app.add_middleware(
app.include_router(artifacts_router)
app.include_router(seed_router)
# Static directory setup
# Mount static files
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")
@@ -82,30 +84,6 @@ 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 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 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)
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"""

29
scripts/build-for-airgap.sh → build-for-airgap.sh Normal file → Executable file
View File

@@ -15,7 +15,7 @@ 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."
echo "Error: Node.js is not installed. Please install Node.js 18+ first."
exit 1
fi
@@ -34,37 +34,30 @@ echo "Step 2/3: Building Angular production bundle..."
npm run build:prod
echo ""
echo "Step 3/3: Copying to static directory..."
echo "Step 3/3: Verifying build output..."
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
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 "The Angular app has been built and copied to static/"
echo "You can now:"
echo "Next steps:"
echo "1. Update docker-compose.yml:"
echo " Change: dockerfile: Dockerfile.frontend"
echo " To: dockerfile: Dockerfile.frontend.prebuilt"
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 "2. Deploy:"
echo " docker-compose up -d --build"
echo " (Docker will rebuild Angular during build)"
echo ""
echo "See DEPLOYMENT.md for more details."
echo "========================================="

View File

@@ -18,7 +18,7 @@ else
echo " Expected: frontend/dist/frontend/browser"
echo ""
echo " You need to build the Angular app first:"
echo " Run: ./scripts/build-for-airgap.sh"
echo " Run: ./build-for-airgap.sh"
echo " OR: cd frontend && npm install && npm run build:prod"
echo ""
errors=$((errors + 1))

View File

@@ -36,12 +36,8 @@ services:
timeout: 5s
retries: 5
app:
container_name: warehouse13-app
build:
context: .
args:
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
api:
build: .
ports:
- "8000:8000"
environment:
@@ -63,6 +59,20 @@ 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:

View File

@@ -1,517 +0,0 @@
# Warehouse13 - Kubernetes Deployment with Helm
This guide covers deploying Warehouse13 to Kubernetes using the official Helm chart.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Quick Start](#quick-start)
3. [Deployment Scenarios](#deployment-scenarios)
4. [Configuration](#configuration)
5. [Post-Deployment](#post-deployment)
6. [Upgrading](#upgrading)
7. [Troubleshooting](#troubleshooting)
## Prerequisites
- Kubernetes 1.19+ cluster
- Helm 3.0+
- kubectl configured to access your cluster
- Persistent volume provisioner (for production deployments)
### Installing Helm
```bash
# macOS
brew install helm
# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
# Windows
choco install kubernetes-helm
```
## Quick Start
### 1. Standard Deployment (Internet Access)
```bash
# Create namespace
kubectl create namespace warehouse13
# Install with default values
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13
# Wait for pods to be ready
kubectl wait --for=condition=ready pod \
--all --namespace warehouse13 --timeout=300s
```
### 2. Access the Application
```bash
# Frontend
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
# API
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
# MinIO Console
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
```
Then visit:
- Frontend: http://localhost:4200
- API Docs: http://localhost:8000/docs
- MinIO Console: http://localhost:9001
## Deployment Scenarios
### Development Environment
For local testing or CI/CD:
```bash
helm install warehouse13-dev ./helm/warehouse13 \
--namespace warehouse13-dev \
--create-namespace \
--values ./helm/warehouse13/values-dev.yaml
```
**Features:**
- Single replica for all services
- emptyDir storage (no persistence)
- Minimal resource requests
- Always pull latest dev images
### Production Environment
For production with ingress and high availability:
```bash
# First, update the values file with your domain and secrets
cp ./helm/warehouse13/values-production.yaml ./my-production-values.yaml
# Edit the file:
# - Set postgres.auth.password
# - Set minio.auth.rootUser and rootPassword
# - Set ingress.hosts[0].host to your domain
# - Update storageClass for your environment
# Install
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./my-production-values.yaml
```
**Features:**
- 3 replicas for API and frontend
- Persistent storage with PVCs
- Ingress with TLS support
- Resource limits and requests
- Health checks enabled
- Pod anti-affinity for distribution
### Air-Gapped Environment
For restricted/disconnected environments:
```bash
# 1. First, push images to your internal registry
# Example using harbor.internal.example.com
# Pull images (on internet-connected machine)
docker pull postgres:15-alpine
docker pull minio/minio:latest
docker pull warehouse13/api:v1.0.0
docker pull warehouse13/frontend:v1.0.0
# Tag for internal registry
docker tag postgres:15-alpine harbor.internal.example.com/library/postgres:15-alpine
docker tag minio/minio:latest harbor.internal.example.com/library/minio:latest
docker tag warehouse13/api:v1.0.0 harbor.internal.example.com/warehouse13/api:v1.0.0
docker tag warehouse13/frontend:v1.0.0 harbor.internal.example.com/warehouse13/frontend:v1.0.0
# Push to internal registry
docker push harbor.internal.example.com/library/postgres:15-alpine
docker push harbor.internal.example.com/library/minio:latest
docker push harbor.internal.example.com/warehouse13/api:v1.0.0
docker push harbor.internal.example.com/warehouse13/frontend:v1.0.0
# 2. Update the values file with your registry
cp ./helm/warehouse13/values-airgapped.yaml ./my-airgapped-values.yaml
# Edit to match your environment:
# - Update all image.repository values
# - Set secure passwords
# - Configure storage classes
# - Add node selectors/tolerations if needed
# 3. Install on air-gapped cluster
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./my-airgapped-values.yaml
```
**Features:**
- All images from custom registry
- Local storage class support
- Node selectors for specific nodes
- Tolerations for tainted nodes
## Configuration
### Configurable Images
All component images can be customized:
```yaml
# PostgreSQL
postgres:
image:
repository: postgres # or your-registry/postgres
tag: 15-alpine
pullPolicy: IfNotPresent
# MinIO
minio:
image:
repository: minio/minio # or your-registry/minio
tag: latest
pullPolicy: IfNotPresent
# API Backend
api:
image:
repository: warehouse13/api # or your-registry/warehouse13-api
tag: v1.0.0
pullPolicy: IfNotPresent
# Frontend
frontend:
image:
repository: warehouse13/frontend # or your-registry/warehouse13-frontend
tag: v1.0.0
pullPolicy: IfNotPresent
```
### Quick Image Override
```bash
# Override images from command line
helm install warehouse13 ./helm/warehouse13 \
--set postgres.image.repository=myregistry.com/postgres \
--set postgres.image.tag=15-alpine \
--set minio.image.repository=myregistry.com/minio \
--set minio.image.tag=latest \
--set api.image.repository=myregistry.com/warehouse13-api \
--set api.image.tag=v1.0.0 \
--set frontend.image.repository=myregistry.com/warehouse13-frontend \
--set frontend.image.tag=v1.0.0
```
### Storage Configuration
```yaml
# PostgreSQL storage
postgres:
persistence:
enabled: true
size: 50Gi
storageClass: "fast-ssd" # or "" for default
# MinIO storage
minio:
persistence:
enabled: true
size: 500Gi
storageClass: "bulk-storage" # or "" for default
```
### Resource Configuration
```yaml
# API resources
api:
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
# Frontend resources
frontend:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
```
### Ingress Configuration
```yaml
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
hosts:
- host: warehouse13.example.com
paths:
- path: /
pathType: Prefix
backend: frontend
- path: /api
pathType: Prefix
backend: api
tls:
- secretName: warehouse13-tls
hosts:
- warehouse13.example.com
```
## Post-Deployment
### Verify Installation
```bash
# Check all pods are running
kubectl get pods -n warehouse13
# Check services
kubectl get svc -n warehouse13
# Check PVCs
kubectl get pvc -n warehouse13
# Check ingress (if enabled)
kubectl get ingress -n warehouse13
```
### View Logs
```bash
# API logs
kubectl logs -n warehouse13 -l app.kubernetes.io/component=api --tail=100 -f
# Frontend logs
kubectl logs -n warehouse13 -l app.kubernetes.io/component=frontend --tail=100 -f
# PostgreSQL logs
kubectl logs -n warehouse13 warehouse13-postgres-0 --tail=100 -f
# MinIO logs
kubectl logs -n warehouse13 warehouse13-minio-0 --tail=100 -f
```
### Initialize MinIO Bucket
```bash
# Port-forward to MinIO console
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
# Open http://localhost:9001
# Login with credentials from values.yaml
# Create bucket: "artifacts"
```
## Upgrading
### Upgrade to New Version
```bash
# Update image tags in values file
# Then run upgrade
helm upgrade warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--values ./my-production-values.yaml \
--wait \
--timeout 10m
# Check rollout status
kubectl rollout status deployment/warehouse13-api -n warehouse13
kubectl rollout status deployment/warehouse13-frontend -n warehouse13
```
### Rollback
```bash
# View revision history
helm history warehouse13 -n warehouse13
# Rollback to previous version
helm rollback warehouse13 -n warehouse13
# Rollback to specific revision
helm rollback warehouse13 2 -n warehouse13
```
### Update Values Only
```bash
# Update configuration without changing images
helm upgrade warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--values ./my-updated-values.yaml \
--reuse-values
```
## Backup and Restore
### PostgreSQL Backup
```bash
# Create backup
kubectl exec -n warehouse13 warehouse13-postgres-0 -- \
pg_dump -U warehouse13user warehouse13 > backup-$(date +%Y%m%d).sql
# Restore
cat backup-20241016.sql | kubectl exec -i -n warehouse13 warehouse13-postgres-0 -- \
psql -U warehouse13user warehouse13
```
### MinIO Backup
```bash
# Install MinIO Client
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
# Configure
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9000:9000
mc alias set w13 http://localhost:9000 <access-key> <secret-key>
# Backup bucket
mc mirror w13/artifacts ./backup/artifacts-$(date +%Y%m%d)
# Restore
mc mirror ./backup/artifacts-20241016 w13/artifacts
```
### Full Backup
```bash
# Backup all PVCs
for pvc in $(kubectl get pvc -n warehouse13 -o name); do
pvc_name=$(basename $pvc)
kubectl get -n warehouse13 $pvc -o yaml > backup-${pvc_name}.yaml
done
# Backup Helm values
helm get values warehouse13 -n warehouse13 > backup-values.yaml
```
## Troubleshooting
### Pods Not Starting
```bash
# Check pod status
kubectl get pods -n warehouse13
# Describe pod for events
kubectl describe pod <pod-name> -n warehouse13
# Check logs
kubectl logs <pod-name> -n warehouse13
# Common issues:
# - ImagePullBackOff: Check image repository and credentials
# - Pending: Check PVC status and node resources
# - CrashLoopBackOff: Check application logs
```
### PVC Issues
```bash
# Check PVC status
kubectl get pvc -n warehouse13
# Describe PVC
kubectl describe pvc <pvc-name> -n warehouse13
# Common issues:
# - Pending: No storage class or insufficient storage
# - Bound: PVC is healthy
```
### Database Connection Issues
```bash
# Test PostgreSQL connection
kubectl exec -it -n warehouse13 warehouse13-postgres-0 -- \
psql -U warehouse13user -d warehouse13
# Check database logs
kubectl logs -n warehouse13 warehouse13-postgres-0 --tail=100
# Verify secret
kubectl get secret -n warehouse13 warehouse13-secrets -o yaml
```
### Ingress Not Working
```bash
# Check ingress status
kubectl get ingress -n warehouse13
kubectl describe ingress -n warehouse13 warehouse13-ingress
# Check ingress controller logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller
# Verify TLS certificate
kubectl get certificate -n warehouse13
kubectl describe certificate -n warehouse13 warehouse13-tls
```
### Performance Issues
```bash
# Check resource usage
kubectl top pods -n warehouse13
kubectl top nodes
# Check if pods are being throttled
kubectl describe pod <pod-name> -n warehouse13 | grep -A 5 "State:"
# Increase resources
helm upgrade warehouse13 ./helm/warehouse13 \
--set api.resources.limits.memory=2Gi \
--set api.resources.limits.cpu=2000m
```
## Uninstalling
```bash
# Uninstall the release
helm uninstall warehouse13 -n warehouse13
# Delete PVCs (data will be lost!)
kubectl delete pvc -n warehouse13 -l app.kubernetes.io/instance=warehouse13
# Delete namespace
kubectl delete namespace warehouse13
```
## Additional Resources
- [Helm Chart README](./helm/warehouse13/README.md)
- [Values Documentation](./helm/warehouse13/values.yaml)
- [Docker Deployment Guide](./DEPLOYMENT.md)
- [Main README](./README.md)
## Support
For issues and questions:
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
- Helm Chart Issues: Tag with `helm` label

View File

@@ -1,220 +0,0 @@
# NPM Package Age Policy
## Requirement
All npm packages must be **at least 2 weeks old** before they can be used in this project. This ensures:
- Package stability
- Security vulnerability disclosure time
- Compliance with organizational security policies
## How It Works
### 1. Package Version Pinning
The project uses exact version pinning in `package.json` to prevent automatic updates:
```json
{
"dependencies": {
"@angular/core": "19.2.7", // Exact version, not "^19.2.7" or "~19.2.7"
// ...
}
}
```
The `frontend/.npmrc` file enforces this:
```
save-exact=true
package-lock=true
```
### 2. Automated Age Checking
Use the provided script to verify all packages meet the 2-week requirement:
```bash
# Check if all packages are at least 2 weeks old
node scripts/check-package-age.js
```
This script:
- Queries npm registry for publish dates
- Calculates age of each package
- Fails if any package is newer than 14 days
- Shows detailed age information for all packages
### 3. Installation Process
**Always use `npm ci` instead of `npm install`:**
```bash
cd frontend
npm ci # Installs exact versions from package-lock.json
```
**Why `npm ci`?**
- Uses exact versions from `package-lock.json`
- Doesn't update `package-lock.json`
- Ensures reproducible builds
- Faster than `npm install`
## Updating Packages
When you need to add or update packages:
### Step 1: Add Package to package.json
```bash
# Find a version that's at least 2 weeks old
npm view <package-name> time
# Add exact version to package.json
"dependencies": {
"new-package": "1.2.3"
}
```
### Step 2: Verify Age
```bash
node scripts/check-package-age.js
```
### Step 3: Update Lock File
```bash
cd frontend
npm install --package-lock-only
```
### Step 4: Install and Test
```bash
npm ci
npm run build:prod
```
## CI/CD Integration
Add the age check to your CI/CD pipeline:
### GitLab CI Example
```yaml
verify_package_age:
stage: validate
image: node:18-alpine
script:
- node scripts/check-package-age.js
only:
- merge_requests
- main
```
### GitHub Actions Example
```yaml
- name: Check Package Age
run: node scripts/check-package-age.js
```
## Troubleshooting
### "Package is too new" Error
If a package fails the age check:
1. **Find an older version:**
```bash
npm view <package-name> versions --json
npm view <package-name>@<older-version> time
```
2. **Update package.json with older version**
3. **Re-run age check:**
```bash
node scripts/check-package-age.js
```
### Can't Find Old Enough Version
If no version meets the 2-week requirement:
- Wait until the package is at least 2 weeks old
- Look for alternative packages
- Request an exception through your security team
## Example Workflow
```bash
# 1. Check current package ages
node scripts/check-package-age.js
# 2. If all pass, install dependencies
cd frontend
npm ci
# 3. Build application
npm run build:prod
# 4. For air-gapped deployment
../scripts/build-for-airgap.sh
```
## Scripts Reference
| Script | Purpose |
|--------|---------|
| `scripts/check-package-age.js` | Verify all packages are ≥ 2 weeks old |
| `scripts/pin-old-versions.sh` | Helper script to validate and pin versions |
| `scripts/build-for-airgap.sh` | Build frontend for air-gapped deployment |
## Best Practices
1. **Always commit `package-lock.json`**
- Ensures everyone uses the same versions
- Required for reproducible builds
2. **Use `npm ci` in CI/CD**
- Faster than `npm install`
- Enforces lock file versions
- Prevents surprises
3. **Regular audits**
```bash
# Check for security vulnerabilities
npm audit
# Check package ages
node scripts/check-package-age.js
```
4. **Version ranges to avoid**
- ❌ `^1.2.3` (allows minor/patch updates)
- ❌ `~1.2.3` (allows patch updates)
- ❌ `*` or `latest` (allows any version)
- ✅ `1.2.3` (exact version only)
## Package Age Check Output
```
Checking package ages (must be at least 2 weeks old)...
✓ @angular/common@19.2.7 - 45 days old
✓ @angular/compiler@19.2.7 - 45 days old
✓ rxjs@7.8.0 - 180 days old
❌ new-package@1.0.0 - 5 days old (published 2025-01-12)
================================================================================
❌ FAILED: 1 package(s) are newer than 2 weeks:
- new-package@1.0.0 (5 days old, published 2025-01-12)
```
## Support
For questions or exceptions:
- Review with security team
- Document in project README
- Update this policy as needed

View File

@@ -93,8 +93,5 @@
}
}
}
},
"cli": {
"analytics": false
}
}

9625
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,20 +23,20 @@
},
"private": true,
"dependencies": {
"@angular/common": "19.2.x",
"@angular/compiler": "19.2.x",
"@angular/core": "19.2.x",
"@angular/forms": "19.2.x",
"@angular/platform-browser": "19.2.x",
"@angular/router": "19.2.x",
"@angular/common": "^19.1.0",
"@angular/compiler": "^19.1.0",
"@angular/core": "^19.1.0",
"@angular/forms": "^19.1.0",
"@angular/platform-browser": "^19.1.0",
"@angular/router": "^19.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.8.1",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/build": "19.2.x",
"@angular/cli": "19.2.x",
"@angular/compiler-cli": "19.2.x",
"@angular/build": "<=19.2.7",
"@angular/cli": "<=19.2.7",
"@angular/compiler-cli": "^19.1.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.9.0",
"karma": "~6.4.0",
@@ -44,7 +44,7 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "5.x.x",
"typescript": "~5.8.0",
"undici-types": "7.12.0",
"node-releases": "2.0.21",
"node-gyp": "11.4.2",

View File

@@ -1,134 +0,0 @@
/* App Layout */
.app-layout {
display: flex;
min-height: 100vh;
background: #0f172a;
color: #e2e8f0;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 280px;
transition: margin-right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.main-content.sidebar-collapsed {
margin-right: 80px;
}
/* Top Header */
.top-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
background: #1e293b;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: sticky;
top: 0;
z-index: 100;
}
.top-header h1 {
display: flex;
align-items: center;
gap: 12px;
font-size: 20px;
font-weight: 600;
color: #f1f5f9;
margin: 0;
}
.logo {
font-family: 'Courier New', monospace;
color: #3b82f6;
font-weight: 700;
letter-spacing: -1px;
}
.header-info {
display: flex;
gap: 12px;
align-items: center;
}
.badge {
padding: 6px 14px;
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* Content Area */
.content-area {
flex: 1;
padding: 24px;
overflow-y: auto;
}
/* Responsive Adjustments */
@media (max-width: 1024px) {
.main-content {
margin-right: 240px;
}
}
@media (max-width: 768px) {
.main-content {
margin-right: 0;
}
.top-header {
padding: 12px 16px;
}
.top-header h1 {
font-size: 18px;
}
.content-area {
padding: 16px;
}
.header-info {
flex-direction: column;
gap: 6px;
align-items: flex-end;
}
}
/* Smooth Scrolling */
.content-area::-webkit-scrollbar {
width: 8px;
}
.content-area::-webkit-scrollbar-thumb {
background: rgba(59, 130, 246, 0.3);
border-radius: 4px;
}
.content-area::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
/* Global Material Icons Sizing */
.material-icons.md-16 {
font-size: 16px;
}
.material-icons.md-18 {
font-size: 18px;
}
.material-icons.md-20 {
font-size: 20px;
}
.material-icons.md-24 {
font-size: 24px;
}

View File

@@ -2,14 +2,10 @@ import { Routes } from '@angular/router';
import { ArtifactsListComponent } from './components/artifacts-list/artifacts-list';
import { UploadFormComponent } from './components/upload-form/upload-form';
import { QueryFormComponent } from './components/query-form/query-form';
import { SettingsComponent } from './components/settings/settings';
import { ProfileComponent } from './components/profile/profile';
export const routes: Routes = [
{ path: '', redirectTo: '/artifacts', pathMatch: 'full' },
{ path: 'artifacts', component: ArtifactsListComponent },
{ path: 'upload', component: UploadFormComponent },
{ path: 'query', component: QueryFormComponent },
{ path: 'settings', component: SettingsComponent },
{ path: 'profile', component: ProfileComponent }
{ path: 'query', component: QueryFormComponent }
];

View File

@@ -1,38 +1,43 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { ArtifactService } from './services/artifact';
import { NavSidebarComponent } from './components/nav-sidebar/nav-sidebar';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, NavSidebarComponent],
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive],
template: `
<div class="app-layout">
<app-nav-sidebar (sidebarToggled)="onSidebarToggle($event)"></app-nav-sidebar>
<main class="main-content" [class.sidebar-collapsed]="isSidebarCollapsed">
<header class="top-header">
<h1><span class="logo">[W13]</span> Warehouse13</h1>
<div class="container">
<header>
<h1><span class="logo">[W13]</span></h1>
<div class="header-info">
<span class="badge">{{ deploymentMode }}</span>
<span class="badge">{{ storageBackend }}</span>
</div>
</header>
<div class="content-area">
<nav class="tabs">
<a routerLink="/artifacts" routerLinkActive="active" class="tab-button">
<span class="material-icons md-16">storage</span> Artifacts
</a>
<a routerLink="/upload" routerLinkActive="active" class="tab-button">
<span class="material-icons md-16">upload</span> Upload
</a>
<a routerLink="/query" routerLinkActive="active" class="tab-button">
<span class="material-icons md-16">search</span> Query
</a>
</nav>
<router-outlet></router-outlet>
</div>
</main>
</div>
`,
styleUrls: ['./app.css']
})
export class AppComponent implements OnInit {
deploymentMode: string = '';
storageBackend: string = '';
isSidebarCollapsed: boolean = false;
constructor(private artifactService: ArtifactService) {}
@@ -45,8 +50,4 @@ export class AppComponent implements OnInit {
error: (err) => console.error('Failed to load API info:', err)
});
}
onSidebarToggle(isCollapsed: boolean) {
this.isSidebarCollapsed = isCollapsed;
}
}

View File

@@ -1,276 +0,0 @@
.sidebar {
position: fixed;
right: 0;
top: 0;
height: 100vh;
background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 280px;
display: flex;
flex-direction: column;
z-index: 1000;
overflow: hidden;
}
.sidebar.collapsed {
width: 80px;
}
/* Toggle Button */
.toggle-btn {
position: absolute;
left: -18px;
top: 20px;
width: 36px;
height: 36px;
border-radius: 50%;
background: #3b82f6;
border: 2px solid #0f172a;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 12px rgba(59, 130, 246, 0.5);
transition: all 0.2s ease;
z-index: 1001;
overflow: visible;
}
.toggle-btn:hover {
background: #2563eb;
transform: scale(1.1);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.6);
}
.toggle-btn .material-icons {
font-size: 22px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* User Section */
.user-section {
padding: 24px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin-top: 40px;
}
.sidebar.collapsed .user-section {
padding: 24px 8px;
}
.user-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.3s ease;
}
.sidebar.collapsed .user-avatar {
width: 48px;
height: 48px;
}
.avatar-text {
font-size: 20px;
font-weight: 600;
color: white;
letter-spacing: 1px;
}
.user-info {
text-align: center;
width: 100%;
overflow: hidden;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-email {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-role {
font-size: 11px;
color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
padding: 4px 12px;
border-radius: 12px;
display: inline-block;
}
/* Navigation Items */
.nav-items {
flex: 1;
padding: 16px 8px;
overflow-y: auto;
overflow-x: hidden;
}
.nav-items::-webkit-scrollbar {
width: 6px;
}
.nav-items::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.nav-items::-webkit-scrollbar-track {
background: transparent;
}
.nav-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
margin-bottom: 4px;
border-radius: 8px;
color: #cbd5e1;
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
position: relative;
}
.sidebar.collapsed .nav-item {
justify-content: center;
padding: 12px 8px;
}
.nav-item:hover {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.nav-item.active {
background: rgba(59, 130, 246, 0.15);
color: #3b82f6;
}
.nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 24px;
background: #3b82f6;
border-radius: 0 4px 4px 0;
}
.nav-icon {
font-size: 24px;
flex-shrink: 0;
}
.nav-label {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar.collapsed .nav-label {
display: none;
}
/* Footer */
.sidebar-footer {
padding: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.app-info {
text-align: center;
}
.app-name {
font-size: 14px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
}
.app-version {
font-size: 11px;
color: #64748b;
}
/* Responsive Design */
@media (max-width: 1024px) {
.sidebar {
width: 240px;
}
.sidebar.collapsed {
width: 70px;
}
}
@media (max-width: 768px) {
.sidebar {
width: 100%;
transform: translateX(100%);
}
.sidebar:not(.collapsed) {
transform: translateX(0);
}
.sidebar.collapsed {
width: 60px;
transform: translateX(0);
}
.toggle-btn {
left: auto;
right: 16px;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.nav-label,
.user-info {
animation: slideIn 0.3s ease;
}

View File

@@ -1,40 +0,0 @@
<aside class="sidebar" [class.collapsed]="isCollapsed">
<!-- Toggle Button -->
<button class="toggle-btn" (click)="toggleSidebar()" aria-label="Toggle sidebar">
<span class="material-icons">{{ isCollapsed ? 'chevron_right' : 'chevron_left' }}</span>
</button>
<!-- User Profile Section -->
<div class="user-section">
<div class="user-avatar">
<span class="avatar-text">{{ user.avatar }}</span>
</div>
<div class="user-info" *ngIf="!isCollapsed">
<div class="user-name">{{ user.name }}</div>
<div class="user-email">{{ user.email }}</div>
<div class="user-role">{{ user.role }}</div>
</div>
</div>
<!-- Navigation Items -->
<nav class="nav-items">
<a
*ngFor="let item of navItems"
[routerLink]="item.route"
routerLinkActive="active"
class="nav-item"
[attr.aria-label]="item.label"
>
<span class="material-icons nav-icon">{{ item.icon }}</span>
<span class="nav-label" *ngIf="!isCollapsed">{{ item.label }}</span>
</a>
</nav>
<!-- App Info Footer -->
<div class="sidebar-footer" *ngIf="!isCollapsed">
<div class="app-info">
<div class="app-name">Warehouse13</div>
<div class="app-version">v1.0.0</div>
</div>
</div>
</aside>

View File

@@ -1,43 +0,0 @@
import { Component, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink, RouterLinkActive } from '@angular/router';
interface NavItem {
route: string;
label: string;
icon: string;
}
@Component({
selector: 'app-nav-sidebar',
standalone: true,
imports: [CommonModule, RouterLink, RouterLinkActive],
templateUrl: './nav-sidebar.html',
styleUrls: ['./nav-sidebar.css']
})
export class NavSidebarComponent {
isCollapsed = false;
@Output() sidebarToggled = new EventEmitter<boolean>();
// Hardcoded user data for now (will be replaced with OAuth)
user = {
name: 'John Doe',
email: 'john.doe@warehouse13.com',
avatar: 'JD',
role: 'Administrator'
};
navItems: NavItem[] = [
{ route: '/artifacts', label: 'Artifacts', icon: 'inventory_2' },
{ route: '/upload', label: 'Upload', icon: 'cloud_upload' },
{ route: '/query', label: 'Query', icon: 'search' },
{ route: '/profile', label: 'Profile', icon: 'person' },
{ route: '/settings', label: 'Settings', icon: 'settings' }
];
toggleSidebar() {
this.isCollapsed = !this.isCollapsed;
this.sidebarToggled.emit(this.isCollapsed);
}
}

View File

@@ -1,395 +0,0 @@
.profile-container {
max-width: 1000px;
margin: 0 auto;
padding: 24px;
}
.page-header {
margin-bottom: 32px;
}
.page-header h1 {
display: flex;
align-items: center;
gap: 12px;
font-size: 28px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 8px 0;
}
.page-header h1 .material-icons {
font-size: 32px;
color: #3b82f6;
}
.subtitle {
color: #94a3b8;
margin: 0;
}
.profile-content {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Profile Card */
.profile-card {
background: #1e293b;
border-radius: 12px;
padding: 32px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.profile-header {
display: flex;
align-items: flex-start;
gap: 24px;
margin-bottom: 32px;
}
.profile-avatar-large {
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 4px 24px rgba(59, 130, 246, 0.3);
}
.avatar-text {
font-size: 36px;
font-weight: 600;
color: white;
letter-spacing: 2px;
}
.profile-info {
flex: 1;
}
.profile-info h2 {
font-size: 24px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 8px 0;
}
.email {
color: #94a3b8;
margin: 0 0 12px 0;
font-size: 14px;
}
.profile-meta {
display: flex;
align-items: center;
gap: 16px;
}
.badge-role {
padding: 6px 14px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.department {
display: flex;
align-items: center;
gap: 6px;
color: #cbd5e1;
font-size: 13px;
}
.department .material-icons {
font-size: 18px;
}
.btn-edit {
padding: 10px 20px;
border-radius: 8px;
border: 1px solid #3b82f6;
background: transparent;
color: #3b82f6;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.btn-edit:hover {
background: rgba(59, 130, 246, 0.1);
}
.profile-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
padding-top: 24px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-item {
display: flex;
align-items: flex-start;
gap: 12px;
}
.detail-item > .material-icons {
font-size: 20px;
color: #64748b;
margin-top: 2px;
}
.detail-item label {
display: block;
font-size: 12px;
color: #94a3b8;
margin-bottom: 4px;
}
.detail-item span {
display: block;
font-size: 14px;
color: #f1f5f9;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.stat-card {
background: #1e293b;
border-radius: 12px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 16px;
transition: all 0.2s ease;
}
.stat-card:hover {
border-color: #3b82f6;
transform: translateY(-2px);
}
.stat-icon {
font-size: 36px;
color: #3b82f6;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
}
.stat-label {
font-size: 12px;
color: #94a3b8;
}
/* Activity Section */
.activity-section,
.security-section {
background: #1e293b;
border-radius: 12px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.activity-section h2,
.security-section h2 {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.activity-section h2 .material-icons,
.security-section h2 .material-icons {
font-size: 24px;
color: #3b82f6;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.activity-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px;
background: rgba(15, 23, 42, 0.5);
border-radius: 8px;
transition: all 0.2s ease;
}
.activity-item:hover {
background: rgba(59, 130, 246, 0.05);
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: rgba(59, 130, 246, 0.1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.activity-icon .material-icons {
font-size: 20px;
color: #3b82f6;
}
.activity-details {
flex: 1;
}
.activity-action {
font-size: 14px;
color: #f1f5f9;
margin-bottom: 4px;
}
.activity-file {
font-size: 12px;
color: #94a3b8;
}
.activity-time {
font-size: 12px;
color: #64748b;
white-space: nowrap;
}
/* Security Section */
.security-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.security-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: rgba(15, 23, 42, 0.5);
border-radius: 8px;
}
.security-info {
display: flex;
align-items: flex-start;
gap: 16px;
flex: 1;
}
.security-info > .material-icons {
font-size: 24px;
color: #64748b;
margin-top: 2px;
}
.security-info h3 {
font-size: 15px;
font-weight: 500;
color: #f1f5f9;
margin: 0 0 4px 0;
}
.security-info p {
font-size: 13px;
color: #94a3b8;
margin: 0;
}
.btn-secondary {
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #334155;
background: transparent;
color: #cbd5e1;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
}
.btn-secondary:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
}
/* Responsive */
@media (max-width: 768px) {
.profile-container {
padding: 16px;
}
.profile-header {
flex-direction: column;
text-align: center;
}
.profile-avatar-large {
margin: 0 auto;
}
.profile-info {
text-align: center;
}
.profile-meta {
justify-content: center;
}
.btn-edit {
width: 100%;
justify-content: center;
}
.stats-grid {
grid-template-columns: 1fr;
}
.security-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.btn-secondary {
width: 100%;
}
}

View File

@@ -1,128 +0,0 @@
<div class="profile-container">
<header class="page-header">
<h1>
<span class="material-icons">person</span>
Profile
</h1>
<p class="subtitle">Manage your account information</p>
</header>
<div class="profile-content">
<!-- User Info Card -->
<section class="profile-card">
<div class="profile-header">
<div class="profile-avatar-large">
<span class="avatar-text">{{ user.avatar }}</span>
</div>
<div class="profile-info">
<h2>{{ user.name }}</h2>
<p class="email">{{ user.email }}</p>
<div class="profile-meta">
<span class="badge badge-role">{{ user.role }}</span>
<span class="department">
<span class="material-icons">business</span>
{{ user.department }}
</span>
</div>
</div>
<button class="btn-edit">
<span class="material-icons">edit</span>
Edit Profile
</button>
</div>
<div class="profile-details">
<div class="detail-item">
<span class="material-icons">event</span>
<div>
<label>Joined</label>
<span>{{ user.joinDate }}</span>
</div>
</div>
<div class="detail-item">
<span class="material-icons">schedule</span>
<div>
<label>Last Login</label>
<span>{{ user.lastLogin }}</span>
</div>
</div>
</div>
</section>
<!-- Stats Grid -->
<section class="stats-grid">
<div class="stat-card" *ngFor="let stat of stats">
<span class="material-icons stat-icon">{{ stat.icon }}</span>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</section>
<!-- Recent Activity -->
<section class="activity-section">
<h2>
<span class="material-icons">history</span>
Recent Activity
</h2>
<div class="activity-list">
<div class="activity-item" *ngFor="let activity of recentActivity">
<div class="activity-icon">
<span class="material-icons">{{
activity.action.includes('Uploaded') ? 'cloud_upload' :
activity.action.includes('query') ? 'search' :
activity.action.includes('Downloaded') ? 'cloud_download' :
'settings'
}}</span>
</div>
<div class="activity-details">
<div class="activity-action">{{ activity.action }}</div>
<div class="activity-file">{{ activity.file }}</div>
</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</section>
<!-- Security Section -->
<section class="security-section">
<h2>
<span class="material-icons">security</span>
Security
</h2>
<div class="security-content">
<div class="security-item">
<div class="security-info">
<span class="material-icons">lock</span>
<div>
<h3>Password</h3>
<p>Last changed 3 months ago</p>
</div>
</div>
<button class="btn-secondary">Change Password</button>
</div>
<div class="security-item">
<div class="security-info">
<span class="material-icons">verified_user</span>
<div>
<h3>Two-Factor Authentication</h3>
<p>Add an extra layer of security</p>
</div>
</div>
<button class="btn-secondary">Enable 2FA</button>
</div>
<div class="security-item">
<div class="security-info">
<span class="material-icons">devices</span>
<div>
<h3>Active Sessions</h3>
<p>Manage your active login sessions</p>
</div>
</div>
<button class="btn-secondary">View Sessions</button>
</div>
</div>
</section>
</div>
</div>

View File

@@ -1,36 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-profile',
standalone: true,
imports: [CommonModule],
templateUrl: './profile.html',
styleUrls: ['./profile.css']
})
export class ProfileComponent {
// Hardcoded user data (will be replaced with OAuth integration)
user = {
name: 'John Doe',
email: 'john.doe@warehouse13.com',
avatar: 'JD',
role: 'Administrator',
department: 'Engineering',
joinDate: 'January 15, 2024',
lastLogin: 'October 16, 2025, 2:30 PM'
};
stats = [
{ label: 'Artifacts Uploaded', value: '1,234', icon: 'cloud_upload' },
{ label: 'Queries Run', value: '567', icon: 'search' },
{ label: 'Storage Used', value: '45.2 GB', icon: 'storage' },
{ label: 'Active Since', value: '9 months', icon: 'schedule' }
];
recentActivity = [
{ action: 'Uploaded artifact', file: 'test_results.csv', time: '2 hours ago' },
{ action: 'Ran query', file: 'integration tests', time: '5 hours ago' },
{ action: 'Downloaded artifact', file: 'performance_metrics.json', time: '1 day ago' },
{ action: 'Updated settings', file: 'notification preferences', time: '2 days ago' }
];
}

View File

@@ -1,227 +0,0 @@
.settings-container {
max-width: 900px;
margin: 0 auto;
padding: 24px;
}
.page-header {
margin-bottom: 32px;
}
.page-header h1 {
display: flex;
align-items: center;
gap: 12px;
font-size: 28px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 8px 0;
}
.page-header h1 .material-icons {
font-size: 32px;
color: #3b82f6;
}
.subtitle {
color: #94a3b8;
margin: 0;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.settings-section {
background: #1e293b;
border-radius: 12px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.settings-section h2 {
font-size: 18px;
font-weight: 600;
color: #f1f5f9;
margin: 0 0 20px 0;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.setting-item:last-child {
border-bottom: none;
}
.setting-info {
display: flex;
align-items: flex-start;
gap: 16px;
flex: 1;
}
.setting-info > .material-icons {
font-size: 24px;
color: #64748b;
margin-top: 2px;
}
.setting-info h3 {
font-size: 15px;
font-weight: 500;
color: #f1f5f9;
margin: 0 0 4px 0;
}
.setting-info p {
font-size: 13px;
color: #94a3b8;
margin: 0;
}
.setting-control {
display: flex;
align-items: center;
gap: 12px;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 48px;
height: 26px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 26px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #3b82f6;
}
input:checked + .slider:before {
transform: translateX(22px);
}
/* Theme Buttons */
.theme-btn {
padding: 8px 20px;
border-radius: 8px;
border: 1px solid #334155;
background: transparent;
color: #cbd5e1;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.theme-btn:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
}
.theme-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: white;
}
/* Buttons */
.btn-secondary {
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #334155;
background: transparent;
color: #cbd5e1;
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
text-decoration: none;
display: inline-block;
}
.btn-secondary:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3b82f6;
color: #3b82f6;
}
/* Badge */
.badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.badge-success {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
/* Code */
.endpoint {
padding: 6px 12px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #3b82f6;
}
/* Responsive */
@media (max-width: 768px) {
.settings-container {
padding: 16px;
}
.setting-item {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.setting-control {
width: 100%;
justify-content: flex-end;
}
}

View File

@@ -1,167 +0,0 @@
<div class="settings-container">
<header class="page-header">
<h1>
<span class="material-icons">settings</span>
Settings
</h1>
<p class="subtitle">Manage your Warehouse13 preferences</p>
</header>
<div class="settings-content">
<!-- General Settings -->
<section class="settings-section">
<h2>General</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">palette</span>
<div>
<h3>Theme</h3>
<p>Choose your preferred color scheme</p>
</div>
</div>
<div class="setting-control">
<button
[class.active]="settings.theme === 'dark'"
(click)="changeTheme('dark')"
class="theme-btn">
Dark
</button>
<button
[class.active]="settings.theme === 'light'"
(click)="changeTheme('light')"
class="theme-btn">
Light
</button>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">notifications</span>
<div>
<h3>Notifications</h3>
<p>Enable desktop notifications for updates</p>
</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input
type="checkbox"
[checked]="settings.notifications"
(change)="toggleNotifications()">
<span class="slider"></span>
</label>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">refresh</span>
<div>
<h3>Auto Refresh</h3>
<p>Automatically refresh artifact list</p>
</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input
type="checkbox"
[checked]="settings.autoRefresh"
(change)="toggleAutoRefresh()">
<span class="slider"></span>
</label>
</div>
</div>
</section>
<!-- Storage Settings -->
<section class="settings-section">
<h2>Storage</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">storage</span>
<div>
<h3>Default Storage Backend</h3>
<p>Currently using: MinIO</p>
</div>
</div>
<div class="setting-control">
<span class="badge badge-success">Connected</span>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">cloud</span>
<div>
<h3>Upload Limit</h3>
<p>Maximum file size: 500MB</p>
</div>
</div>
</div>
</section>
<!-- API Settings -->
<section class="settings-section">
<h2>API</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">link</span>
<div>
<h3>API Endpoint</h3>
<p>Backend service endpoint</p>
</div>
</div>
<div class="setting-control">
<code class="endpoint">/api/v1</code>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">key</span>
<div>
<h3>API Keys</h3>
<p>Manage API authentication tokens</p>
</div>
</div>
<div class="setting-control">
<button class="btn-secondary">Manage Keys</button>
</div>
</div>
</section>
<!-- About Section -->
<section class="settings-section">
<h2>About</h2>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">info</span>
<div>
<h3>Version</h3>
<p>Warehouse13 v1.0.0</p>
</div>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<span class="material-icons">description</span>
<div>
<h3>Documentation</h3>
<p>View API documentation and guides</p>
</div>
</div>
<div class="setting-control">
<a href="/docs" target="_blank" class="btn-secondary">
View Docs
</a>
</div>
</div>
</section>
</div>
</div>

View File

@@ -1,30 +0,0 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-settings',
standalone: true,
imports: [CommonModule],
templateUrl: './settings.html',
styleUrls: ['./settings.css']
})
export class SettingsComponent {
settings = {
theme: 'dark',
notifications: true,
autoRefresh: false,
refreshInterval: 30
};
toggleNotifications() {
this.settings.notifications = !this.settings.notifications;
}
toggleAutoRefresh() {
this.settings.autoRefresh = !this.settings.autoRefresh;
}
changeTheme(theme: string) {
this.settings.theme = theme;
}
}

13
helm/Chart.yaml Normal file
View File

@@ -0,0 +1,13 @@
apiVersion: v2
name: datalake
description: Test Artifact Data Lake - Store and query test artifacts
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- testing
- artifacts
- storage
- data-lake
maintainers:
- name: Your Team

View File

@@ -1,42 +0,0 @@
# Helm Charts
This directory contains Helm charts for deploying Warehouse13.
## Current Chart (Recommended)
**Location:** `./warehouse13/`
The latest, fully-featured Helm chart with:
- Warehouse13 branding
- Configurable images for all components
- Multiple deployment scenarios (dev, production, air-gapped)
- Comprehensive documentation
- Example values files
**Usage:**
```bash
helm install warehouse13 ./warehouse13
```
**Documentation:** See [warehouse13/README.md](./warehouse13/README.md)
## Migration from Legacy Chart
If you were using an older version of the chart, migration is straightforward:
```bash
# Uninstall old chart (if named "datalake" or other name)
helm uninstall <old-release-name>
# Install new chart
helm install warehouse13 ./warehouse13 --namespace warehouse13 --create-namespace
# Or upgrade in place (if compatible)
helm upgrade <old-release-name> ./warehouse13
```
Note: Check your values.yaml configuration and update image repositories, resource limits, and other settings as needed.
## Quick Start
See [../docs/HELM-DEPLOYMENT.md](../docs/HELM-DEPLOYMENT.md) for comprehensive deployment guide.

View File

@@ -1,14 +1,14 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "warehouse13.name" -}}
{{- define "datalake.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "warehouse13.fullname" -}}
{{- define "datalake.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
@@ -24,16 +24,16 @@ Create a default fully qualified app name.
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "warehouse13.chart" -}}
{{- define "datalake.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "warehouse13.labels" -}}
helm.sh/chart: {{ include "warehouse13.chart" . }}
{{ include "warehouse13.selectorLabels" . }}
{{- define "datalake.labels" -}}
helm.sh/chart: {{ include "datalake.chart" . }}
{{ include "datalake.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
@@ -43,29 +43,18 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
{{/*
Selector labels
*/}}
{{- define "warehouse13.selectorLabels" -}}
app.kubernetes.io/name: {{ include "warehouse13.name" . }}
{{- define "datalake.selectorLabels" -}}
app.kubernetes.io/name: {{ include "datalake.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "warehouse13.serviceAccountName" -}}
{{- define "datalake.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "warehouse13.fullname" .) .Values.serviceAccount.name }}
{{- default (include "datalake.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
PostgreSQL connection string
*/}}
{{- define "warehouse13.postgresUrl" -}}
{{- if .Values.app.env.databaseUrl }}
{{- .Values.app.env.databaseUrl }}
{{- else }}
{{- printf "postgresql://%s:%s@warehouse13-postgres:%d/%s" .Values.postgres.auth.username .Values.postgres.auth.password (.Values.postgres.service.port | int) .Values.postgres.auth.database }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,111 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "datalake.fullname" . }}
labels:
{{- include "datalake.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "datalake.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "datalake.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "datalake.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "datalake.fullname" . }}-secrets
key: database-url
- name: STORAGE_BACKEND
value: {{ .Values.config.storageBackend | quote }}
- name: MAX_UPLOAD_SIZE
value: {{ .Values.config.maxUploadSize | quote }}
{{- if eq .Values.config.storageBackend "s3" }}
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: {{ include "datalake.fullname" . }}-secrets
key: aws-access-key-id
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "datalake.fullname" . }}-secrets
key: aws-secret-access-key
- name: AWS_REGION
value: {{ .Values.aws.region | quote }}
- name: S3_BUCKET_NAME
value: {{ .Values.aws.bucketName | quote }}
{{- else }}
- name: MINIO_ENDPOINT
value: "{{ include "datalake.fullname" . }}-minio:9000"
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "datalake.fullname" . }}-secrets
key: minio-access-key
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "datalake.fullname" . }}-secrets
key: minio-secret-key
- name: MINIO_BUCKET_NAME
value: "test-artifacts"
- name: MINIO_SECURE
value: "false"
{{- end }}
{{- with .Values.env }}
{{- toYaml . | nindent 8 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -2,9 +2,9 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: warehouse13-ingress
name: {{ include "datalake.fullname" . }}
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
{{- include "datalake.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
@@ -33,9 +33,9 @@ spec:
pathType: {{ .pathType }}
backend:
service:
name: {{ printf "warehouse13-%s" .backend }}
name: {{ include "datalake.fullname" $ }}
port:
number: {{ $.Values.app.service.port }}
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,16 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "datalake.fullname" . }}-secrets
labels:
{{- include "datalake.labels" . | nindent 4 }}
type: Opaque
stringData:
database-url: "postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ include "datalake.fullname" . }}-postgresql:5432/{{ .Values.postgresql.auth.database }}"
{{- if .Values.aws.enabled }}
aws-access-key-id: {{ .Values.aws.accessKeyId | quote }}
aws-secret-access-key: {{ .Values.aws.secretAccessKey | quote }}
{{- else }}
minio-access-key: {{ .Values.minio.rootUser | quote }}
minio-secret-key: {{ .Values.minio.rootPassword | quote }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "datalake.fullname" . }}
labels:
{{- include "datalake.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "datalake.selectorLabels" . | nindent 4 }}

View File

@@ -2,9 +2,9 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "warehouse13.serviceAccountName" . }}
name: {{ include "datalake.serviceAccountName" . }}
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
{{- include "datalake.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}

111
helm/values.yaml Normal file
View File

@@ -0,0 +1,111 @@
replicaCount: 1
image:
repository: datalake
pullPolicy: IfNotPresent
tag: "latest"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 1000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1000
service:
type: ClusterIP
port: 8000
targetPort: 8000
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: datalake.local
paths:
- path: /
pathType: Prefix
tls: []
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# Application configuration
config:
storageBackend: minio # or "s3"
maxUploadSize: 524288000 # 500MB
# PostgreSQL configuration
postgresql:
enabled: true
auth:
username: user
password: password
database: datalake
primary:
persistence:
enabled: true
size: 10Gi
# MinIO configuration (for self-hosted storage)
minio:
enabled: true
mode: standalone
rootUser: minioadmin
rootPassword: minioadmin
persistence:
enabled: true
size: 50Gi
service:
type: ClusterIP
port: 9000
consoleService:
port: 9001
# AWS S3 configuration (when using AWS)
aws:
enabled: false
accessKeyId: ""
secretAccessKey: ""
region: us-east-1
bucketName: test-artifacts
# Environment variables
env:
- name: API_HOST
value: "0.0.0.0"
- name: API_PORT
value: "8000"

View File

@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -1,281 +0,0 @@
# Warehouse13 Architecture
## Overview
Warehouse13 uses a **unified application container** that includes both the frontend and backend in a single Docker image using a multi-stage build.
## Docker Build Strategy
### Multi-Stage Dockerfile
```dockerfile
# Stage 1: Build Angular Frontend
FROM node:24-alpine AS frontend-build
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build:prod
# Stage 2: Python Backend with Static Frontend
FROM python:3.11-alpine
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install -r requirements.txt
# Copy backend code
COPY app/ ./app/
# Copy built frontend from stage 1
COPY --from=frontend-build /frontend/dist/frontend/browser ./static/
# Run FastAPI server
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### Benefits
1. **Simplified Deployment** - Single container to manage
2. **Reduced Resource Usage** - No separate nginx container needed
3. **Easier Scaling** - Scale one deployment instead of two
4. **Consistent Versioning** - Frontend and backend versions always match
5. **Faster Deployments** - Fewer containers to orchestrate
## Service Architecture
```
┌─────────────────────────────────────────┐
│ warehouse13-app │
│ ┌────────────────────────────────────┐ │
│ │ FastAPI Backend (Port 8000) │ │
│ │ ├── /api/* → REST API │ │
│ │ ├── /health → Health check │ │
│ │ ├── /docs → API documentation │ │
│ │ └── /* → Angular SPA │ │
│ │ │ │
│ │ Static Files: /static/ │ │
│ │ └── Angular build output │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
├────────────────┐
↓ ↓
┌──────────┐ ┌────────────┐
│PostgreSQL│ │ MinIO │
│(Metadata)│ │ (Blobs) │
└──────────┘ └────────────┘
```
## Helm Chart Structure
### Single Application Deployment
The Helm chart creates:
1. **1 Deployment**: `warehouse13-app`
- Runs the unified container
- Configurable replicas (default: 2)
- Health checks on `/health` endpoint
2. **1 Service**: `warehouse13-app`
- Exposes port 8000
- Routes all traffic to the application
3. **Optional Ingress**
- All paths route to `warehouse13-app:8000`
- FastAPI handles routing internally
### Kubernetes Resources
```yaml
# warehouse13-app Deployment
- Replicas: 2 (configurable)
- Port: 8000
- Health checks: /health
- Environment: DATABASE_URL, MINIO_* vars
# warehouse13-app Service
- Type: ClusterIP
- Port: 8000 → 8000
# Ingress (optional)
- Path: / → warehouse13-app:8000
```
## Configuration
### Image Configuration
In `values.yaml`:
```yaml
app:
enabled: true
image:
repository: warehouse13/app # Single unified image
tag: latest
pullPolicy: IfNotPresent
replicas: 2
resources:
requests:
memory: "384Mi" # Combined frontend + backend
cpu: "350m"
limits:
memory: "768Mi"
cpu: "750m"
```
### Accessing the Application
**Via Port Forward:**
```bash
kubectl port-forward svc/warehouse13-app 8000:8000
```
Then access:
- Frontend: http://localhost:8000
- API: http://localhost:8000/api
- API Docs: http://localhost:8000/docs
- Health: http://localhost:8000/health
**Via Ingress:**
```yaml
ingress:
enabled: true
hosts:
- host: warehouse13.example.com
paths:
- path: /
pathType: Prefix
backend: app # All traffic to one service
```
## Migration from Separate Services
If you previously had separate `api` and `frontend` deployments:
### Before (Old Architecture)
```yaml
# values.yaml (old)
api:
image: warehouse13/api
replicas: 2
frontend:
image: warehouse13/frontend
replicas: 2
# Two deployments, two services
```
### After (Current Architecture)
```yaml
# values.yaml (current)
app:
image: warehouse13/app # Unified image
replicas: 2
# One deployment, one service
```
### Migration Steps
1. **Update values.yaml** - Change from `api`/`frontend` to `app`
2. **Update image references** - Use `warehouse13/app` instead of separate images
3. **Update ingress** - Point all paths to `app` backend
4. **Deploy** - Helm will handle the transition
5. **Verify** - Check that both frontend and API work through single service
## Development Workflow
### Building the Image
```bash
# Build unified image
docker build -t warehouse13/app:dev .
# Or for air-gapped environments with custom registry
docker build \
--build-arg NPM_REGISTRY=https://registry.npmjs.org/ \
-t warehouse13/app:v1.0.0 .
```
### Testing Locally
```bash
docker run -p 8000:8000 \
-e DATABASE_URL=postgresql://user:pass@host/db \
-e MINIO_ENDPOINT=minio:9000 \
warehouse13/app:dev
```
Access:
- Frontend: http://localhost:8000
- API: http://localhost:8000/docs
## Performance Considerations
### Resource Allocation
The unified container combines both frontend serving and API processing:
- **Memory**: Angular assets (~50MB) + Python runtime (~100MB) + working memory
- **CPU**: Primarily used for API requests; static file serving is lightweight
- **Recommended Minimum**: 384Mi memory, 350m CPU
- **Production**: 768Mi memory, 750m CPU per replica
### Scaling Strategy
Scale horizontally by increasing replicas:
```bash
# Scale to 5 replicas
kubectl scale deployment warehouse13-app --replicas=5
# Or via Helm
helm upgrade warehouse13 ./helm/warehouse13 --set app.replicas=5
```
### Caching
FastAPI serves static files efficiently with:
- ETag support
- Browser caching headers
- Gzip compression (if enabled in FastAPI config)
## Troubleshooting
### Frontend Not Loading
```bash
# Check if static files exist in container
kubectl exec -it warehouse13-app-xxx -- ls -la /app/static/
# Should see: index.html, *.js, *.css files
```
### API Not Working
```bash
# Check API health
kubectl exec -it warehouse13-app-xxx -- curl http://localhost:8000/health
# Check logs
kubectl logs warehouse13-app-xxx -f
```
### Both Frontend and API Issues
```bash
# Check if app is running
kubectl get pods -l app.kubernetes.io/component=app
# Check service
kubectl get svc warehouse13-app
# Test connectivity
kubectl port-forward svc/warehouse13-app 8000:8000
curl http://localhost:8000/health
```
## Summary
The unified architecture simplifies deployment and operations while maintaining the same functionality. All routing, caching, and API requests are handled by a single FastAPI application that serves both the Angular SPA and the REST API endpoints.

View File

@@ -1,16 +0,0 @@
apiVersion: v2
name: warehouse13
description: Warehouse13 - Enterprise Test Artifact Storage
type: application
version: 1.0.0
appVersion: "1.0.0"
keywords:
- testing
- artifacts
- storage
- datalake
maintainers:
- name: Warehouse13 Team
home: https://github.com/yourusername/warehouse13
sources:
- https://github.com/yourusername/warehouse13

View File

@@ -1,148 +0,0 @@
# Warehouse13 Helm Chart - Quick Start
## 5-Minute Deployment
### Prerequisites Check
```bash
# Verify Kubernetes cluster access
kubectl cluster-info
# Verify Helm is installed
helm version
# Create namespace
kubectl create namespace warehouse13
```
### Deploy with Defaults
```bash
# Install chart
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13
# Wait for ready
kubectl wait --for=condition=ready pod --all -n warehouse13 --timeout=5m
```
### Access Application
```bash
# In separate terminals, run:
# Terminal 1: Frontend
kubectl port-forward -n warehouse13 svc/warehouse13-frontend 4200:80
# Terminal 2: API
kubectl port-forward -n warehouse13 svc/warehouse13-api 8000:8000
# Terminal 3: MinIO Console
kubectl port-forward -n warehouse13 svc/warehouse13-minio 9001:9001
```
Then open in browser:
- **Frontend:** http://localhost:4200
- **API Docs:** http://localhost:8000/docs
- **MinIO Console:** http://localhost:9001
- Username: `minioadmin`
- Password: `minioadmin`
## Common Scenarios
### 1. Development (No Persistence)
```bash
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--values ./helm/warehouse13/values-dev.yaml
```
### 2. Production (With Ingress)
```bash
# Update values-production.yaml with your settings first
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--values ./helm/warehouse13/values-production.yaml
```
### 3. Air-Gapped (Custom Registry)
```bash
# Update values-airgapped.yaml with your registry first
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--values ./helm/warehouse13/values-airgapped.yaml
```
### 4. Custom Image Repository
```bash
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--set postgres.image.repository=myregistry.com/postgres \
--set minio.image.repository=myregistry.com/minio \
--set api.image.repository=myregistry.com/warehouse13-api \
--set frontend.image.repository=myregistry.com/warehouse13-frontend
```
## Verify Deployment
```bash
# Check pods
kubectl get pods -n warehouse13
# Check services
kubectl get svc -n warehouse13
# View logs
kubectl logs -n warehouse13 -l app.kubernetes.io/component=api --tail=50
# Check resource usage
kubectl top pods -n warehouse13
```
## Cleanup
```bash
# Uninstall release
helm uninstall warehouse13 -n warehouse13
# Delete PVCs (data will be lost!)
kubectl delete pvc -n warehouse13 --all
# Delete namespace
kubectl delete namespace warehouse13
```
## Next Steps
- **Full Documentation:** [README.md](./README.md)
- **Deployment Guide:** [../../docs/HELM-DEPLOYMENT.md](../../docs/HELM-DEPLOYMENT.md)
- **Configuration Options:** [values.yaml](./values.yaml)
- **Example Configs:** [values-dev.yaml](./values-dev.yaml), [values-production.yaml](./values-production.yaml), [values-airgapped.yaml](./values-airgapped.yaml)
## Troubleshooting
### Pods stuck in Pending
```bash
kubectl describe pod <pod-name> -n warehouse13
# Check: PVC status, node resources, storage classes
```
### Image pull errors
```bash
kubectl describe pod <pod-name> -n warehouse13
# Check: Image repository, credentials, network access
```
### Database connection errors
```bash
kubectl logs -n warehouse13 warehouse13-postgres-0
kubectl get secret -n warehouse13 warehouse13-secrets -o yaml
```
## Support
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
- Documentation: https://warehouse13.example.com/docs

View File

@@ -1,441 +0,0 @@
# Warehouse13 Helm Chart
Enterprise Test Artifact Storage - Kubernetes deployment via Helm
## Overview
This Helm chart deploys the complete Warehouse13 stack on Kubernetes:
- **PostgreSQL 15** - Metadata database
- **MinIO** - S3-compatible object storage
- **FastAPI Backend** - REST API server
- **Angular Frontend** - Web UI (nginx-served)
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- PV provisioner support (for persistent storage)
## Installation
### Quick Start
```bash
# Add the Warehouse13 chart repository (if published)
helm repo add warehouse13 https://charts.warehouse13.example.com
helm repo update
# Install with default values
helm install my-warehouse13 warehouse13/warehouse13
# Or install from local chart
helm install my-warehouse13 ./helm/warehouse13
```
### Custom Installation
```bash
# Install with custom values
helm install my-warehouse13 ./helm/warehouse13 \
--set postgres.persistence.size=20Gi \
--set minio.persistence.size=100Gi \
--set api.replicas=3
# Install in a specific namespace
helm install my-warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace
```
## Configuration
### Configurable Images
All component images can be customized via values.yaml or command-line flags:
```yaml
postgres:
image:
repository: postgres
tag: 15-alpine
pullPolicy: IfNotPresent
minio:
image:
repository: minio/minio
tag: latest
pullPolicy: IfNotPresent
api:
image:
repository: warehouse13/api
tag: latest
pullPolicy: IfNotPresent
frontend:
image:
repository: warehouse13/frontend
tag: latest
pullPolicy: IfNotPresent
```
**Example: Using custom image registry**
```bash
helm install my-warehouse13 ./helm/warehouse13 \
--set postgres.image.repository=myregistry.example.com/postgres \
--set minio.image.repository=myregistry.example.com/minio \
--set api.image.repository=myregistry.example.com/warehouse13-api \
--set frontend.image.repository=myregistry.example.com/warehouse13-frontend
```
**Example: Air-gapped deployment with specific tags**
```bash
helm install my-warehouse13 ./helm/warehouse13 \
--set postgres.image.repository=harbor.internal/library/postgres \
--set postgres.image.tag=15-alpine \
--set minio.image.repository=harbor.internal/library/minio \
--set minio.image.tag=RELEASE.2024-01-01T00-00-00Z \
--set api.image.repository=harbor.internal/warehouse13/api \
--set api.image.tag=v1.0.0 \
--set frontend.image.repository=harbor.internal/warehouse13/frontend \
--set frontend.image.tag=v1.0.0
```
### Key Parameters
| Parameter | Description | Default |
|-----------|-------------|---------|
| `global.deploymentMode` | Deployment mode (standard/airgapped) | `standard` |
| `global.storageBackend` | Storage backend (minio/s3) | `minio` |
| `postgres.persistence.enabled` | Enable PostgreSQL persistence | `true` |
| `postgres.persistence.size` | PostgreSQL PVC size | `10Gi` |
| `postgres.auth.username` | PostgreSQL username | `user` |
| `postgres.auth.password` | PostgreSQL password | `password` |
| `minio.persistence.enabled` | Enable MinIO persistence | `true` |
| `minio.persistence.size` | MinIO PVC size | `50Gi` |
| `minio.auth.rootUser` | MinIO root username | `minioadmin` |
| `minio.auth.rootPassword` | MinIO root password | `minioadmin` |
| `api.replicas` | Number of API replicas | `2` |
| `frontend.replicas` | Number of frontend replicas | `2` |
| `ingress.enabled` | Enable ingress | `false` |
| `ingress.className` | Ingress class name | `nginx` |
| `ingress.hosts` | Ingress hosts configuration | See values.yaml |
### Example Configurations
#### Production with Ingress
```yaml
# values-production.yaml
global:
deploymentMode: "standard"
storageBackend: "minio"
postgres:
persistence:
size: 50Gi
storageClass: "fast-ssd"
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
minio:
persistence:
size: 500Gi
storageClass: "bulk-storage"
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
api:
replicas: 3
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
frontend:
replicas: 3
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: warehouse13.example.com
paths:
- path: /
pathType: Prefix
backend: frontend
- path: /api
pathType: Prefix
backend: api
tls:
- secretName: warehouse13-tls
hosts:
- warehouse13.example.com
```
```bash
helm install my-warehouse13 ./helm/warehouse13 -f values-production.yaml
```
#### Air-Gapped Environment
```yaml
# values-airgapped.yaml
global:
deploymentMode: "airgapped"
storageBackend: "minio"
postgres:
image:
repository: harbor.internal.example.com/library/postgres
tag: 15-alpine
pullPolicy: IfNotPresent
minio:
image:
repository: harbor.internal.example.com/library/minio
tag: RELEASE.2024-01-01T00-00-00Z
pullPolicy: IfNotPresent
api:
image:
repository: harbor.internal.example.com/warehouse13/api
tag: v1.0.0
pullPolicy: IfNotPresent
frontend:
image:
repository: harbor.internal.example.com/warehouse13/frontend
tag: v1.0.0
pullPolicy: IfNotPresent
```
```bash
helm install my-warehouse13 ./helm/warehouse13 -f values-airgapped.yaml
```
#### Development/Testing
```yaml
# values-dev.yaml
global:
deploymentMode: "standard"
postgres:
persistence:
enabled: false # Use emptyDir for quick testing
resources:
requests:
memory: "128Mi"
cpu: "100m"
minio:
persistence:
enabled: false
resources:
requests:
memory: "256Mi"
cpu: "100m"
api:
replicas: 1
image:
tag: dev
frontend:
replicas: 1
image:
tag: dev
```
```bash
helm install my-warehouse13 ./helm/warehouse13 -f values-dev.yaml
```
## Accessing the Application
### Port Forwarding (Development)
```bash
# Access frontend
kubectl port-forward svc/warehouse13-frontend 4200:80
# Access API
kubectl port-forward svc/warehouse13-api 8000:8000
# Access MinIO console
kubectl port-forward svc/warehouse13-minio 9001:9001
# Then visit:
# - Frontend: http://localhost:4200
# - API: http://localhost:8000
# - MinIO Console: http://localhost:9001
```
### Via Ingress (Production)
If ingress is enabled:
```
https://warehouse13.example.com
```
## Upgrading
```bash
# Upgrade with new values
helm upgrade my-warehouse13 ./helm/warehouse13 \
--set api.image.tag=v2.0.0 \
--set frontend.image.tag=v2.0.0
# Upgrade with values file
helm upgrade my-warehouse13 ./helm/warehouse13 -f values-production.yaml
# Upgrade and wait for completion
helm upgrade my-warehouse13 ./helm/warehouse13 --wait --timeout 10m
```
## Uninstalling
```bash
# Uninstall the release
helm uninstall my-warehouse13
# Note: PVCs are not deleted automatically. To delete them:
kubectl delete pvc -l app.kubernetes.io/instance=my-warehouse13
```
## Backup and Restore
### PostgreSQL Backup
```bash
# Create backup
kubectl exec -it warehouse13-postgres-0 -- pg_dump -U user warehouse13 > backup.sql
# Restore
kubectl exec -i warehouse13-postgres-0 -- psql -U user warehouse13 < backup.sql
```
### MinIO Backup
```bash
# Install mc (MinIO Client)
# Configure mc alias
mc alias set w13 http://localhost:9001 minioadmin minioadmin
# Mirror bucket
mc mirror w13/artifacts ./backup/artifacts
# Restore
mc mirror ./backup/artifacts w13/artifacts
```
## Troubleshooting
### Check Pod Status
```bash
kubectl get pods -l app.kubernetes.io/name=warehouse13
```
### View Logs
```bash
# API logs
kubectl logs -l app.kubernetes.io/component=api -f
# Frontend logs
kubectl logs -l app.kubernetes.io/component=frontend -f
# PostgreSQL logs
kubectl logs warehouse13-postgres-0 -f
# MinIO logs
kubectl logs warehouse13-minio-0 -f
```
### Check Services
```bash
kubectl get svc -l app.kubernetes.io/name=warehouse13
```
### Common Issues
**Pods stuck in Pending**
- Check PVC status: `kubectl get pvc`
- Verify storage class exists: `kubectl get storageclass`
- Check node resources: `kubectl describe nodes`
**Database connection errors**
- Verify postgres pod is running: `kubectl get pod warehouse13-postgres-0`
- Check database logs: `kubectl logs warehouse13-postgres-0`
- Verify secret exists: `kubectl get secret warehouse13-secrets`
**Frontend cannot reach API**
- Check ingress configuration: `kubectl describe ingress warehouse13-ingress`
- Verify API service: `kubectl get svc warehouse13-api`
- Check API pod health: `kubectl get pods -l app.kubernetes.io/component=api`
## Security Considerations
### Secrets Management
**Default credentials are for development only!** In production:
1. **Use external secrets management:**
```yaml
# Use sealed-secrets, external-secrets, or similar
postgres:
auth:
username: "{{ .Values.externalSecrets.postgresUser }}"
password: "{{ .Values.externalSecrets.postgresPassword }}"
```
2. **Or create secrets manually:**
```bash
kubectl create secret generic warehouse13-secrets \
--from-literal=postgres-username=secure-user \
--from-literal=postgres-password=secure-password \
--from-literal=minio-root-user=secure-minio-user \
--from-literal=minio-root-password=secure-minio-password
# Then install without default secrets
helm install my-warehouse13 ./helm/warehouse13 --set createSecrets=false
```
3. **Enable TLS:**
```yaml
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
- secretName: warehouse13-tls
hosts:
- warehouse13.example.com
```
## Support
For issues and questions:
- GitHub Issues: https://github.com/yourusername/warehouse13/issues
- Documentation: https://warehouse13.example.com/docs

View File

@@ -1,131 +0,0 @@
_ _ _ _ _____
| | | | | | / |___ /
| | | | __ _ _ __ ___| |__ ___ _ _ ___ / / |_ \
| |/\| |/ _` | '__/ _ \ '_ \ / _ \| | | / __|/ / ___) |
\ /\ / (_| | | | __/ | | | (_) | |_| \__ \_/ |____/
\/ \/ \__,_|_| \___|_| |_|\___/ \__,_|___(_)
Enterprise Test Artifact Storage has been deployed!
Chart Name: {{ .Chart.Name }}
Chart Version: {{ .Chart.Version }}
App Version: {{ .Chart.AppVersion }}
Release Name: {{ .Release.Name }}
Namespace: {{ .Release.Namespace }}
---
DEPLOYMENT INFORMATION:
{{- if .Values.app.enabled }}
Application (Unified API + Frontend):
Service: warehouse13-app
Replicas: {{ .Values.app.replicas }}
Image: {{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}
Port: {{ .Values.app.service.port }}
Note: Multi-stage build includes both Angular frontend and FastAPI backend
{{- end }}
{{- if .Values.postgres.enabled }}
PostgreSQL:
Service: warehouse13-postgres
Image: {{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}
Persistence: {{ if .Values.postgres.persistence.enabled }}Enabled ({{ .Values.postgres.persistence.size }}){{ else }}Disabled (emptyDir){{ end }}
{{- end }}
{{- if .Values.minio.enabled }}
MinIO:
Service: warehouse13-minio
Image: {{ .Values.minio.image.repository }}:{{ .Values.minio.image.tag }}
Persistence: {{ if .Values.minio.persistence.enabled }}Enabled ({{ .Values.minio.persistence.size }}){{ else }}Disabled (emptyDir){{ end }}
{{- end }}
---
ACCESSING YOUR APPLICATION:
{{- if .Values.ingress.enabled }}
1. Via Ingress:
{{- range .Values.ingress.hosts }}
https://{{ .host }}
{{- end }}
{{- else }}
1. Using Port Forwarding:
# Application (Frontend + API)
kubectl port-forward -n {{ .Release.Namespace }} svc/warehouse13-app 8000:8000
Then visit:
- Frontend: http://localhost:8000
- API Docs: http://localhost:8000/docs
- Health: http://localhost:8000/health
# MinIO Console
kubectl port-forward -n {{ .Release.Namespace }} svc/warehouse13-minio 9001:9001
Then visit: http://localhost:9001
Username: {{ .Values.minio.auth.rootUser }}
Password: {{ .Values.minio.auth.rootPassword }}
2. Expose via LoadBalancer or Ingress for external access.
{{- end }}
---
CHECKING STATUS:
# View all pods
kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
# Check services
kubectl get svc -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
# View logs
kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/component=app -f
---
UPGRADING:
helm upgrade {{ .Release.Name }} warehouse13/warehouse13 \
--namespace {{ .Release.Namespace }}
---
UNINSTALLING:
helm uninstall {{ .Release.Name }} --namespace {{ .Release.Namespace }}
# Note: PVCs are retained. To delete them:
kubectl delete pvc -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }}
---
{{- if not .Values.ingress.enabled }}
⚠️ IMPORTANT: Ingress is disabled. Enable it for production use:
--set ingress.enabled=true
{{- end }}
{{- if eq .Values.postgres.auth.password "password" }}
⚠️ WARNING: Using default PostgreSQL password!
For production, set a secure password:
--set postgres.auth.password=YOUR_SECURE_PASSWORD
{{- end }}
{{- if eq .Values.minio.auth.rootPassword "minioadmin" }}
⚠️ WARNING: Using default MinIO password!
For production, set a secure password:
--set minio.auth.rootPassword=YOUR_SECURE_PASSWORD
{{- end }}
---
For more information, visit:
Documentation: https://github.com/yourusername/warehouse13
Issues: https://github.com/yourusername/warehouse13/issues
Thank you for using Warehouse13!

View File

@@ -1,99 +0,0 @@
{{- if .Values.app.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: warehouse13-app
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
app.kubernetes.io/component: app
spec:
replicas: {{ .Values.app.replicas }}
selector:
matchLabels:
{{- include "warehouse13.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: app
template:
metadata:
labels:
{{- include "warehouse13.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: app
spec:
imagePullSecrets:
- name: gitlab-dev-ns-registry-secret
serviceAccountName: {{ include "warehouse13.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: app
securityContext:
{{- toYaml .Values.securityContext | nindent 10 }}
image: "{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
ports:
- name: http
containerPort: 8000
protocol: TCP
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: database-url
- name: STORAGE_BACKEND
valueFrom:
configMapKeyRef:
name: warehouse13-config
key: STORAGE_BACKEND
- name: MINIO_ENDPOINT
valueFrom:
configMapKeyRef:
name: warehouse13-config
key: MINIO_ENDPOINT
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: minio-root-user
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: minio-root-password
- name: MINIO_BUCKET_NAME
value: "test-artifacts"
- name: MINIO_SECURE
value: "false"
- name: DEPLOYMENT_MODE
valueFrom:
configMapKeyRef:
name: warehouse13-config
key: DEPLOYMENT_MODE
resources:
{{- toYaml .Values.app.resources | nindent 10 }}
{{- if .Values.app.healthCheck.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.app.healthCheck.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.app.healthCheck.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.app.healthCheck.liveness.periodSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.app.healthCheck.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.app.healthCheck.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.app.healthCheck.readiness.periodSeconds }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}

View File

@@ -1,19 +0,0 @@
{{- if .Values.app.enabled }}
apiVersion: v1
kind: Service
metadata:
name: warehouse13-app
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
app.kubernetes.io/component: app
spec:
type: {{ .Values.app.service.type }}
ports:
- port: {{ .Values.app.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "warehouse13.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: app
{{- end }}

View File

@@ -1,10 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: warehouse13-config
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
data:
DEPLOYMENT_MODE: {{ .Values.global.deploymentMode | quote }}
STORAGE_BACKEND: {{ .Values.global.storageBackend | quote }}
MINIO_ENDPOINT: {{ printf "warehouse13-minio:%d" (.Values.minio.service.apiPort | int) | quote }}

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.imagePullSecret.name }}
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: {{
(printf "{\"auths\":{\"%s\":{\"username\":\"%s\",\"password\":\"%s\",\"email\":\"%s\",\"auth\":\"%s\"}}}"
.Values.imagePullSecret.server
.Values.imagePullSecret.username
.Values.imagePullSecret.password
.Values.imagePullSecret.email
(printf "%s" (b64enc (printf "%s:%s" .Values.imagePullSecret.username .Values.imagePullSecret.password))))
| b64enc | quote
}}

View File

@@ -1,23 +0,0 @@
{{- if .Values.minio.enabled }}
apiVersion: v1
kind: Service
metadata:
name: warehouse13-minio
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
app.kubernetes.io/component: storage
spec:
type: {{ .Values.minio.service.type }}
ports:
- port: {{ .Values.minio.service.apiPort }}
targetPort: api
protocol: TCP
name: api
- port: {{ .Values.minio.service.consolePort }}
targetPort: console
protocol: TCP
name: console
selector:
{{- include "warehouse13.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: storage
{{- end }}

View File

@@ -1,87 +0,0 @@
{{- if .Values.minio.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: warehouse13-minio
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
app.kubernetes.io/component: storage
spec:
serviceName: warehouse13-minio
replicas: 1
selector:
matchLabels:
{{- include "warehouse13.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: storage
template:
metadata:
labels:
{{- include "warehouse13.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: storage
spec:
serviceAccountName: {{ include "warehouse13.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: minio
image: "{{ .Values.minio.image.repository }}:{{ .Values.minio.image.tag }}"
imagePullPolicy: {{ .Values.minio.image.pullPolicy }}
command:
- minio
- server
- /data
- --console-address
- ":9001"
ports:
- name: api
containerPort: 9000
protocol: TCP
- name: console
containerPort: 9001
protocol: TCP
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: minio-root-user
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: minio-root-password
volumeMounts:
- name: data
mountPath: /data
resources:
{{- toYaml .Values.minio.resources | nindent 10 }}
livenessProbe:
httpGet:
path: /minio/health/live
port: api
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /minio/health/ready
port: api
initialDelaySeconds: 10
periodSeconds: 5
{{- if .Values.minio.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
{{- if .Values.minio.persistence.storageClass }}
storageClassName: {{ .Values.minio.persistence.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.minio.persistence.size }}
{{- else }}
volumes:
- name: data
emptyDir: {}
{{- end }}
{{- end }}

View File

@@ -1,19 +0,0 @@
{{- if .Values.postgres.enabled }}
apiVersion: v1
kind: Service
metadata:
name: warehouse13-postgres
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
app.kubernetes.io/component: database
spec:
type: {{ .Values.postgres.service.type }}
ports:
- port: {{ .Values.postgres.service.port }}
targetPort: postgres
protocol: TCP
name: postgres
selector:
{{- include "warehouse13.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: database
{{- end }}

View File

@@ -1,89 +0,0 @@
{{- if .Values.postgres.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: warehouse13-postgres
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
app.kubernetes.io/component: database
spec:
serviceName: warehouse13-postgres
replicas: 1
selector:
matchLabels:
{{- include "warehouse13.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: database
template:
metadata:
labels:
{{- include "warehouse13.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: database
spec:
serviceAccountName: {{ include "warehouse13.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: postgres
image: "{{ .Values.postgres.image.repository }}:{{ .Values.postgres.image.tag }}"
imagePullPolicy: {{ .Values.postgres.image.pullPolicy }}
ports:
- name: postgres
containerPort: 5432
protocol: TCP
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: postgres-username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: postgres-password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: warehouse13-secrets
key: postgres-database
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
resources:
{{- toYaml .Values.postgres.resources | nindent 10 }}
livenessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command:
- pg_isready
- -U
- $(POSTGRES_USER)
initialDelaySeconds: 10
periodSeconds: 5
{{- if .Values.postgres.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
{{- if .Values.postgres.persistence.storageClass }}
storageClassName: {{ .Values.postgres.persistence.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.postgres.persistence.size }}
{{- else }}
volumes:
- name: data
emptyDir: {}
{{- end }}
{{- end }}

View File

@@ -1,14 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: warehouse13-secrets
labels:
{{- include "warehouse13.labels" . | nindent 4 }}
type: Opaque
stringData:
postgres-username: {{ .Values.postgres.auth.username | quote }}
postgres-password: {{ .Values.postgres.auth.password | quote }}
postgres-database: {{ .Values.postgres.auth.database | quote }}
minio-root-user: {{ .Values.minio.auth.rootUser | quote }}
minio-root-password: {{ .Values.minio.auth.rootPassword | quote }}
database-url: {{ include "warehouse13.postgresUrl" . | quote }}

View File

@@ -1,83 +0,0 @@
# Warehouse13 - Air-Gapped Deployment Example
# Use this for restricted/disconnected environments
global:
deploymentMode: "airgapped"
storageBackend: "minio"
# PostgreSQL with custom registry
postgres:
enabled: true
image:
repository: harbor.internal.example.com/library/postgres
tag: 15-alpine
pullPolicy: IfNotPresent
auth:
username: warehouse13user
password: CHANGE_ME_SECURE_PASSWORD
database: warehouse13
persistence:
enabled: true
size: 20Gi
storageClass: "local-storage"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
# MinIO with custom registry
minio:
enabled: true
image:
repository: harbor.internal.example.com/library/minio
tag: RELEASE.2024-01-01T00-00-00Z
pullPolicy: IfNotPresent
auth:
rootUser: CHANGE_ME_MINIO_USER
rootPassword: CHANGE_ME_MINIO_PASSWORD
persistence:
enabled: true
size: 100Gi
storageClass: "local-storage"
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
# Application with custom registry (unified API + Frontend)
app:
enabled: true
image:
repository: harbor.internal.example.com/warehouse13/app
tag: v1.0.0
pullPolicy: IfNotPresent
replicas: 2
resources:
requests:
memory: "768Mi"
cpu: "750m"
limits:
memory: "1536Mi"
cpu: "1500m"
# Ingress disabled for air-gapped - use NodePort or port-forward
ingress:
enabled: false
# Node selector for specific nodes
nodeSelector:
environment: production
storage: local
# Tolerations for tainted nodes
tolerations:
- key: "airgapped"
operator: "Equal"
value: "true"
effect: "NoSchedule"

View File

@@ -1,69 +0,0 @@
# Warehouse13 - Development/Testing Deployment Example
# Use this for local testing or CI/CD environments
global:
deploymentMode: "standard"
storageBackend: "minio"
postgres:
enabled: true
image:
repository: postgres
tag: 15-alpine
pullPolicy: IfNotPresent
auth:
username: dev
password: dev
database: warehouse13dev
persistence:
enabled: false # Use emptyDir for faster cleanup
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "250m"
minio:
enabled: true
image:
repository: minio/minio
tag: latest
pullPolicy: IfNotPresent
auth:
rootUser: minioadmin
rootPassword: minioadmin
persistence:
enabled: false # Use emptyDir for faster cleanup
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "250m"
app:
enabled: true
image:
repository: warehouse13/app
tag: dev
pullPolicy: Always # Always pull latest dev image
replicas: 1
resources:
requests:
memory: "384Mi"
cpu: "350m"
limits:
memory: "768Mi"
cpu: "750m"
healthCheck:
enabled: true
ingress:
enabled: false # Use port-forward for dev
serviceAccount:
create: true
name: "warehouse13-dev"

View File

@@ -1,98 +0,0 @@
# Warehouse13 - Production Deployment Example
# Use this for production environments with ingress and proper resources
global:
deploymentMode: "standard"
storageBackend: "minio"
postgres:
enabled: true
image:
repository: postgres
tag: 15-alpine
pullPolicy: IfNotPresent
auth:
username: warehouse13user
password: CHANGE_ME_SECURE_PASSWORD
database: warehouse13
persistence:
enabled: true
size: 50Gi
storageClass: "fast-ssd"
resources:
requests:
memory: "1Gi"
cpu: "1000m"
limits:
memory: "2Gi"
cpu: "2000m"
minio:
enabled: true
image:
repository: minio/minio
tag: latest
pullPolicy: IfNotPresent
auth:
rootUser: CHANGE_ME_MINIO_USER
rootPassword: CHANGE_ME_MINIO_PASSWORD
persistence:
enabled: true
size: 500Gi
storageClass: "bulk-storage"
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
app:
enabled: true
image:
repository: warehouse13/app
tag: v1.0.0
pullPolicy: IfNotPresent
replicas: 3
resources:
requests:
memory: "768Mi"
cpu: "750m"
limits:
memory: "1536Mi"
cpu: "1500m"
healthCheck:
enabled: true
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
hosts:
- host: warehouse13.example.com
paths:
- path: /
pathType: Prefix
backend: app
tls:
- secretName: warehouse13-tls
hosts:
- warehouse13.example.com
# Affinity for pod distribution
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- warehouse13
topologyKey: kubernetes.io/hostname

View File

@@ -1,148 +0,0 @@
# Warehouse13 - Enterprise Test Artifact Storage
# Default values for Helm chart
# Global settings
global:
deploymentMode: "air-gapped" # standard or airgapped
storageBackend: "minio" # minio or s3
# PostgreSQL Database
postgres:
enabled: true
image:
repository: containers.global.bsf.tools/postgres
tag: 15-alpine
pullPolicy: Always
auth:
username: user
password: password
database: warehouse13
persistence:
enabled: false
size: 10Gi
storageClass: ""
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
service:
type: ClusterIP
port: 5432
# MinIO Object Storage
minio:
enabled: true
image:
repository: containers.global.bsf.tools/minio/minio
tag: latest
pullPolicy: Always
auth:
rootUser: minioadmin
rootPassword: minioadmin
persistence:
enabled: true
size: 50Gi
storageClass: ""
resources:
requests:
memory: "1Gi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
service:
type: ClusterIP
apiPort: 9000
consolePort: 9001
# Application (Unified API + Frontend)
# The application uses a multi-stage Docker build:
# - Stage 1: Builds Angular frontend
# - Stage 2: Python FastAPI backend that serves the frontend from /static
app:
enabled: true
image:
repository: registry.global.bsf.tools/esv/bsf/bsf-services/warehouse13
tag: main-7126c618
pullPolicy: Always
replicas: 1
env:
databaseUrl: "postgresql://user:password@warehouse13-postgres:5432/warehouse13"
storageBackend: "minio"
minioEndpoint: "warehouse13-minio:9000"
resources:
requests:
memory: "768Mi"
cpu: "350m"
limits:
memory: "768Mi"
cpu: "750m"
service:
type: ClusterIP
port: 8000
healthCheck:
enabled: true
liveness:
path: /health
initialDelaySeconds: 30
periodSeconds: 10
readiness:
path: /health
initialDelaySeconds: 10
periodSeconds: 5
# Ingress
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt"
hosts:
- host: warehouse13.common.global.bsf.tools
paths:
- path: /
pathType: Prefix
backend: app # All traffic goes to unified app (serves both API and frontend)
tls:
- secretName: warehouse13-tls
hosts:
- warehouse13.common.global.bsf.tools
# Service Account
serviceAccount:
create: true
annotations: {}
name: "warehouse13"
# Pod Security
podSecurityContext:
fsGroup: 2000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1000
# Node selector
nodeSelector: {}
# Tolerations
tolerations: []
# Affinity
affinity: {}
imagePullSecret:
name: gitlab-dev-ns-registry-secret
username: project_9145_bot_imagepuller
# Read only token so okay if hard coded here
password: glpat-ZV7ASvBqFoiWC9QqD5WlTG86MQp1OjVxMgk.01.0z192vpfw
server: registry.global.bsf.tools
email: botemail@global.bsf.tool

36
nginx.conf Normal file
View File

@@ -0,0 +1,36 @@
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";
}
}

View File

@@ -30,7 +30,7 @@ fi
echo "Step 1: Building Angular frontend locally..."
echo "==========================================="
./scripts/build-for-airgap.sh
./build-for-airgap.sh
echo ""
echo "Step 2: Starting Docker containers..."
@@ -46,7 +46,8 @@ echo "========================================="
echo "Services are running!"
echo "========================================="
echo ""
echo "Web UI: http://localhost:8000"
echo "Frontend: http://localhost:4200"
echo "API: http://localhost:8000"
echo "API Docs: http://localhost:8000/docs"
echo "MinIO Console: http://localhost:9001"
echo " Username: minioadmin"

106
quickstart.bat Normal file
View File

@@ -0,0 +1,106 @@
@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

View File

@@ -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 app" -ForegroundColor Yellow
Write-Host "$composeCmd logs -f api" -ForegroundColor Yellow
Write-Host " Restart services: " -NoNewline
Write-Host "$composeCmd restart" -ForegroundColor Yellow
Write-Host " Stop all: " -NoNewline

View File

@@ -41,7 +41,7 @@ echo "========================================="
echo "Services are running!"
echo "========================================="
echo ""
echo "Web UI: http://localhost:8000"
echo "API: http://localhost:8000"
echo "API Docs: http://localhost:8000/docs"
echo "MinIO Console: http://localhost:9001"
echo " Username: minioadmin"

View File

@@ -1,146 +0,0 @@
#!/usr/bin/env node
/**
* Check if npm packages are at least 2 weeks old before installation
* Usage: node scripts/check-package-age.js
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000;
function getPackageInfo(packageName, version) {
return new Promise((resolve, reject) => {
// Remove version prefixes like ^, ~, >=
let cleanVersion = version.replace(/^[\^~>=]+/, '');
// If version contains .x or wildcards, fetch all versions and find latest matching
if (cleanVersion.includes('x') || cleanVersion.includes('*')) {
const url = `https://registry.npmjs.org/${packageName}`;
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const info = JSON.parse(data);
const versions = Object.keys(info.versions || {});
// Convert version pattern to regex (e.g., "19.2.x" -> /^19\.2\.\d+$/)
const pattern = cleanVersion.replace(/\./g, '\\.').replace(/x|\*/g, '\\d+');
const regex = new RegExp(`^${pattern}$`);
// Find matching versions and get the latest
const matchingVersions = versions.filter(v => regex.test(v)).sort();
const latestMatching = matchingVersions[matchingVersions.length - 1];
if (latestMatching && info.time && info.time[latestMatching]) {
resolve({
name: packageName,
version: latestMatching,
published: info.time[latestMatching],
error: null
});
} else {
reject(new Error(`No matching version found for ${packageName}@${cleanVersion}`));
}
} catch (err) {
reject(new Error(`Failed to parse response for ${packageName}@${cleanVersion}`));
}
});
}).on('error', (err) => {
reject(err);
});
} else {
// Exact version lookup
const url = `https://registry.npmjs.org/${packageName}/${cleanVersion}`;
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const info = JSON.parse(data);
resolve({
name: packageName,
version: cleanVersion,
published: info.time || info._time,
error: null
});
} catch (err) {
reject(new Error(`Failed to parse response for ${packageName}@${cleanVersion}`));
}
});
}).on('error', (err) => {
reject(err);
});
}
});
}
async function checkPackageAge() {
const packageJsonPath = path.join(__dirname, '../frontend/package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const allDeps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
console.log('Checking package ages (must be at least 2 weeks old)...\n');
const tooNew = [];
const errors = [];
for (const [name, version] of Object.entries(allDeps)) {
try {
const info = await getPackageInfo(name, version);
const publishDate = new Date(info.published);
const age = Date.now() - publishDate.getTime();
const ageInDays = Math.floor(age / (24 * 60 * 60 * 1000));
if (age < TWO_WEEKS_MS) {
tooNew.push({
name,
version: info.version,
age: ageInDays,
published: publishDate.toISOString().split('T')[0]
});
console.log(`${name}@${info.version} - ${ageInDays} days old (published ${publishDate.toISOString().split('T')[0]})`);
} else {
console.log(`${name}@${info.version} - ${ageInDays} days old`);
}
} catch (err) {
errors.push({ name, version, error: err.message });
console.log(`⚠️ ${name}@${version} - Could not check: ${err.message}`);
}
}
console.log('\n' + '='.repeat(80));
if (tooNew.length > 0) {
console.log(`\n❌ FAILED: ${tooNew.length} package(s) are newer than 2 weeks:\n`);
tooNew.forEach(pkg => {
console.log(` - ${pkg.name}@${pkg.version} (${pkg.age} days old, published ${pkg.published})`);
});
process.exit(1);
} else if (errors.length > 0) {
console.log(`\n⚠️ WARNING: Could not verify ${errors.length} package(s)`);
process.exit(0);
} else {
console.log('\n✓ SUCCESS: All packages are at least 2 weeks old');
process.exit(0);
}
}
checkPackageAge();

View File

@@ -1,41 +0,0 @@
#!/bin/bash
# Pin npm packages to versions that are at least 2 weeks old
# This script helps ensure compliance with package age requirements
set -e
echo "========================================="
echo "Pin NPM Packages to Old Versions"
echo "========================================="
echo ""
cd frontend
echo "Step 1: Checking current package ages..."
node ../scripts/check-package-age.js || {
echo ""
echo "Some packages are too new. Recommendations:"
echo "1. Manually downgrade packages in package.json to older versions"
echo "2. Run: npm install --package-lock-only to update lock file"
echo "3. Re-run this script to verify"
exit 1
}
echo ""
echo "Step 2: Ensuring package-lock.json uses exact versions..."
if [ -f "package-lock.json" ]; then
echo "✓ package-lock.json exists"
else
echo "⚠ package-lock.json does not exist. Creating it..."
npm install --package-lock-only
fi
echo ""
echo "========================================="
echo "✓ All packages meet the 2-week age requirement"
echo "========================================="
echo ""
echo "To install these packages:"
echo " npm ci # Uses exact versions from package-lock.json"
echo ""

564
static/css/styles.css Normal file
View File

@@ -0,0 +1,564 @@
* {
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;
}
}

251
static/index.html Normal file
View File

@@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warehouse13 - Test Artifact Data Lake</title>
<link rel="stylesheet" href="/static/css/styles.css">
<script src="https://unpkg.com/lucide@latest"></script>
</head>
<body>
<div class="container">
<header>
<h1><span class="logo">[W13]</span></h1>
<div class="header-info">
<span id="deployment-mode" class="badge"></span>
<span id="storage-backend" class="badge"></span>
</div>
</header>
<nav class="tabs">
<button class="tab-button active" onclick="showTab('artifacts')">
<i data-lucide="database" style="width: 16px; height: 16px;"></i> Artifacts
</button>
<button class="tab-button" onclick="showTab('upload')">
<i data-lucide="upload" style="width: 16px; height: 16px;"></i> Upload
</button>
<button class="tab-button" onclick="showTab('query')">
<i data-lucide="search" style="width: 16px; height: 16px;"></i> Query
</button>
</nav>
<!-- Artifacts Tab -->
<div id="artifacts-tab" class="tab-content active">
<div class="toolbar">
<button onclick="loadArtifacts()" class="btn btn-primary">
<i data-lucide="refresh-cw" style="width: 16px; height: 16px;"></i> Refresh
</button>
<button id="auto-refresh-toggle" onclick="toggleAutoRefresh()" class="btn btn-success">
Auto-refresh: ON
</button>
<button onclick="generateSeedData()" class="btn btn-secondary">
<i data-lucide="sparkles" style="width: 16px; height: 16px;"></i> Generate Seed Data
</button>
<span id="artifact-count" class="count-badge"></span>
<div class="filter-inline">
<i data-lucide="search" style="width: 16px; height: 16px; color: #64748b;"></i>
<input type="text" id="filter-search" placeholder="Search..." oninput="filterTable()">
<button onclick="clearFilters()" class="btn-clear" title="Clear search">
<i data-lucide="x" style="width: 14px; height: 14px;"></i>
</button>
</div>
</div>
<div class="table-container">
<table id="artifacts-table">
<thead>
<tr>
<th class="sortable" onclick="sortTable('test_suite')">
Sim Source <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('filename')">
Artifacts <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('created_at')">
Date <span class="sort-indicator"></span>
</th>
<th class="sortable" onclick="sortTable('test_name')">
Uploaded By <span class="sort-indicator"></span>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="artifacts-tbody">
<tr>
<td colspan="5" class="loading">Loading artifacts...</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<button onclick="previousPage()" id="prev-btn" class="btn">← Previous</button>
<span id="page-info">Page 1</span>
<button onclick="nextPage()" id="next-btn" class="btn">Next →</button>
</div>
</div>
<!-- Upload Tab -->
<div id="upload-tab" class="tab-content">
<div class="upload-section">
<h2>Upload Artifact</h2>
<form id="upload-form" onsubmit="uploadArtifact(event)">
<div class="form-group">
<label for="file">File *</label>
<input type="file" id="file" name="file" required>
<small>Supported: CSV, JSON, binary files, PCAP</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="sim-source">Sim Source *</label>
<input type="text" id="sim-source" name="test_suite" placeholder="e.g., Jenkins, GitLab CI" required>
</div>
<div class="form-group">
<label for="uploaded-by">Uploaded By *</label>
<input type="text" id="uploaded-by" name="test_name" placeholder="e.g., john.doe" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="sim-source-id">SIM Source ID (for grouping)</label>
<input type="text" id="sim-source-id" name="sim_source_id" placeholder="e.g., sim_run_20251015_001">
<small>Use same ID for multiple artifacts from same source</small>
</div>
<div class="form-group">
<label for="tags">Tags (comma-separated) *</label>
<input type="text" id="tags" name="tags" placeholder="e.g., regression, smoke, critical" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="test-result">Test Result</label>
<select id="test-result" name="test_result">
<option value="">-- Select --</option>
<option value="pass">Pass</option>
<option value="fail">Fail</option>
<option value="skip">Skip</option>
<option value="error">Error</option>
</select>
</div>
<div class="form-group">
<label for="version">Version</label>
<input type="text" id="version" name="version" placeholder="e.g., v1.0.0">
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" rows="3" placeholder="Describe this artifact..."></textarea>
</div>
<div class="form-group">
<label for="test-config">Test Config (JSON)</label>
<textarea id="test-config" name="test_config" rows="4" placeholder='{"browser": "chrome", "timeout": 30}'></textarea>
</div>
<div class="form-group">
<label for="custom-metadata">Custom Metadata (JSON)</label>
<textarea id="custom-metadata" name="custom_metadata" rows="4" placeholder='{"build": "1234", "commit": "abc123"}'></textarea>
</div>
<button type="submit" class="btn btn-primary btn-large">
<i data-lucide="upload" style="width: 18px; height: 18px;"></i> Upload Artifact
</button>
</form>
<div id="upload-status"></div>
</div>
</div>
<!-- Query Tab -->
<div id="query-tab" class="tab-content">
<div class="query-section">
<h2>Query Artifacts</h2>
<form id="query-form" onsubmit="queryArtifacts(event)">
<div class="form-row">
<div class="form-group">
<label for="q-filename">Filename</label>
<input type="text" id="q-filename" placeholder="Search filename...">
</div>
<div class="form-group">
<label for="q-type">File Type</label>
<select id="q-type">
<option value="">All</option>
<option value="csv">CSV</option>
<option value="json">JSON</option>
<option value="binary">Binary</option>
<option value="pcap">PCAP</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="q-test-name">Test Name</label>
<input type="text" id="q-test-name" placeholder="Search test name...">
</div>
<div class="form-group">
<label for="q-suite">Test Suite</label>
<input type="text" id="q-suite" placeholder="e.g., integration">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="q-result">Test Result</label>
<select id="q-result">
<option value="">All</option>
<option value="pass">Pass</option>
<option value="fail">Fail</option>
<option value="skip">Skip</option>
<option value="error">Error</option>
</select>
</div>
<div class="form-group">
<label for="q-tags">Tags (comma-separated)</label>
<input type="text" id="q-tags" placeholder="e.g., regression, smoke">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="q-start-date">Start Date</label>
<input type="datetime-local" id="q-start-date">
</div>
<div class="form-group">
<label for="q-end-date">End Date</label>
<input type="datetime-local" id="q-end-date">
</div>
</div>
<button type="submit" class="btn btn-primary btn-large">
<i data-lucide="search" style="width: 18px; height: 18px;"></i> Search
</button>
<button type="button" onclick="clearQuery()" class="btn btn-secondary">
<i data-lucide="x" style="width: 18px; height: 18px;"></i> Clear
</button>
</form>
</div>
</div>
<!-- Artifact Detail Modal -->
<div id="detail-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeDetailModal()">&times;</span>
<h2>Artifact Details</h2>
<div id="detail-content"></div>
</div>
</div>
</div>
<script src="/static/js/app.js"></script>
<script>
// Initialize Lucide icons
lucide.createIcons();
</script>
</body>
</html>

592
static/js/app.js Normal file
View File

@@ -0,0 +1,592 @@
// 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 = `
<tr><td colspan="5" class="loading" style="color: #ef4444;">
Error loading artifacts: ${error.message}
</td></tr>
`;
}
}
// Display artifacts in table
function displayArtifacts(artifacts) {
const tbody = document.getElementById('artifacts-tbody');
if (artifacts.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="loading">No artifacts found. Upload some files to get started!</td></tr>';
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 => `
<tr>
<td>${artifact.sim_source_id || artifact.test_suite || '-'}</td>
<td>
<a href="#" onclick="showDetail(${artifact.id}); return false;" style="color: #60a5fa; text-decoration: none;">
${escapeHtml(artifact.filename)}
</a>
${artifact.tags && artifact.tags.length > 0 ? `<br><div style="margin-top: 5px;">${formatTags(artifact.tags)}</div>` : ''}
</td>
<td>${formatDate(artifact.created_at)}</td>
<td>${artifact.test_name || '-'}</td>
<td>
<div class="action-buttons">
<button class="icon-btn" onclick="downloadArtifact(${artifact.id}, '${escapeHtml(artifact.filename)}')" title="Download">
<i data-lucide="download" style="width: 16px; height: 16px;"></i>
</button>
<button class="icon-btn" onclick="deleteArtifact(${artifact.id})" title="Delete">
<i data-lucide="trash-2" style="width: 16px; height: 16px;"></i>
</button>
</div>
</td>
</tr>
`).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 `<span class="${className}">${result}</span>`;
}
// Format tags
function formatTags(tags) {
if (!tags || tags.length === 0) return '-';
return tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).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 = `
<div class="detail-row">
<div class="detail-label">ID</div>
<div class="detail-value">${artifact.id}</div>
</div>
<div class="detail-row">
<div class="detail-label">Filename</div>
<div class="detail-value">${escapeHtml(artifact.filename)}</div>
</div>
<div class="detail-row">
<div class="detail-label">File Type</div>
<div class="detail-value"><span class="file-type-badge">${artifact.file_type}</span></div>
</div>
<div class="detail-row">
<div class="detail-label">Size</div>
<div class="detail-value">${formatBytes(artifact.file_size)}</div>
</div>
<div class="detail-row">
<div class="detail-label">Storage Path</div>
<div class="detail-value"><code>${artifact.storage_path}</code></div>
</div>
<div class="detail-row">
<div class="detail-label">Uploaded By</div>
<div class="detail-value">${artifact.test_name || '-'}</div>
</div>
<div class="detail-row">
<div class="detail-label">Sim Source</div>
<div class="detail-value">${artifact.test_suite || '-'}</div>
</div>
<div class="detail-row">
<div class="detail-label">Test Result</div>
<div class="detail-value">${formatResult(artifact.test_result)}</div>
</div>
${artifact.test_config ? `
<div class="detail-row">
<div class="detail-label">Test Config</div>
<div class="detail-value"><pre>${JSON.stringify(artifact.test_config, null, 2)}</pre></div>
</div>
` : ''}
${artifact.custom_metadata ? `
<div class="detail-row">
<div class="detail-label">Custom Metadata</div>
<div class="detail-value"><pre>${JSON.stringify(artifact.custom_metadata, null, 2)}</pre></div>
</div>
` : ''}
${artifact.description ? `
<div class="detail-row">
<div class="detail-label">Description</div>
<div class="detail-value">${escapeHtml(artifact.description)}</div>
</div>
` : ''}
${artifact.tags && artifact.tags.length > 0 ? `
<div class="detail-row">
<div class="detail-label">Tags</div>
<div class="detail-value">${formatTags(artifact.tags)}</div>
</div>
` : ''}
<div class="detail-row">
<div class="detail-label">Version</div>
<div class="detail-value">${artifact.version || '-'}</div>
</div>
<div class="detail-row">
<div class="detail-label">Created</div>
<div class="detail-value">${formatDate(artifact.created_at)}</div>
</div>
<div class="detail-row">
<div class="detail-label">Updated</div>
<div class="detail-value">${formatDate(artifact.updated_at)}</div>
</div>
<div style="margin-top: 20px; display: flex; gap: 10px;">
<button onclick="downloadArtifact(${artifact.id}, '${escapeHtml(artifact.filename)}')" class="btn btn-primary">
<i data-lucide="download" style="width: 16px; height: 16px;"></i> Download
</button>
<button onclick="deleteArtifact(${artifact.id}); closeDetailModal();" class="btn btn-danger">
<i data-lucide="trash-2" style="width: 16px; height: 16px;"></i> Delete
</button>
</div>
`;
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();
}