Compare commits

...

80 Commits

Author SHA1 Message Date
Patel (US), Pratik
019208ad0e Fix typo
All checks were successful
build / docker-build (push) Successful in 58s
2025-10-17 15:14:24 -05:00
pratik
af4fd324eb Fix another typo
All checks were successful
build / docker-build (push) Successful in 1m6s
2025-10-17 15:07:35 -05:00
pratik
3d30c84497 add echo statements
All checks were successful
build / docker-build (push) Successful in 59s
2025-10-17 15:04:50 -05:00
pratik
6f8247e4fd Another typo
All checks were successful
build / docker-build (push) Successful in 58s
2025-10-17 14:57:12 -05:00
pratik
014dcb5b09 Another typo
Some checks failed
build / docker-build (push) Has been cancelled
2025-10-17 14:56:57 -05:00
pratik
2f786aa14f Fix typo -- again
All checks were successful
build / docker-build (push) Successful in 58s
2025-10-17 14:51:32 -05:00
pratik
2e9d2ca143 update ci
All checks were successful
build / docker-build (push) Successful in 1m0s
2025-10-17 14:48:03 -05:00
pratik
4f0e7013e4 Update ci 2025-10-17 14:47:05 -05:00
Patel (US), Pratik
c1df449f57 Fix buildah typo
All checks were successful
build / docker-build (push) Successful in 1m0s
2025-10-17 14:36:09 -05:00
Patel (US), Pratik
a07c3ccd9c Pull and push container
All checks were successful
build / docker-build (push) Successful in 59s
2025-10-17 14:31:05 -05:00
5fe92cde25 Merge pull request 'f/sidebar' (#6) from f/sidebar into main
All checks were successful
build / docker-build (push) Successful in 59s
Reviewed-on: #6
2025-10-17 14:02:57 -05:00
pratik
a4e4cb4c5f Merge branch 'main' into f/sidebar 2025-10-17 14:00:59 -05:00
pratik
ec5d9916ba add sidebar and various component 2025-10-17 14:00:32 -05:00
34ce3ef998 Update .gitea/workflows/docker.yml
All checks were successful
build / docker-build (push) Successful in 58s
2025-10-17 13:47:39 -05:00
d70cbdb12f Update .gitea/workflows/docker.yml
All checks were successful
build / docker-build (push) Successful in 1m7s
2025-10-17 13:32:01 -05:00
b63597345a Update .gitea/workflows/docker.yml
Some checks failed
build / docker-build (push) Failing after 9s
2025-10-17 13:14:37 -05:00
a910b8270a Merge pull request 'pipeline' (#5) from pipeline into main
Some checks failed
build / docker-build (push) Failing after 9s
Reviewed-on: mondo/SIM-Data-Platform#5
2025-10-17 13:13:14 -05:00
27afda2d70 add github actions 2025-10-17 13:04:21 -05:00
Armando Diaz
247d207e3b set deployment mode 2025-10-17 11:48:08 -05:00
Armando Diaz
a12df3306d decrese replicas 2025-10-17 11:34:28 -05:00
Armando Diaz
29eb7358df turn off postgres volume 2025-10-17 11:33:17 -05:00
Patel (US), Pratik
28daa5c078 Add in imagesecret template 2025-10-17 11:28:54 -05:00
Patel (US), Pratik
79f17c423b add in imagesecret 2025-10-17 11:28:39 -05:00
Armando Diaz
50bcd35e68 fix pull secret syntax 2025-10-17 11:16:42 -05:00
Armando Diaz
61cd5c471a add reg secret 2025-10-17 11:12:39 -05:00
Armando Diaz
e83ecf4717 fix values file 2025-10-17 11:05:07 -05:00
Armando Diaz
53b37c2e16 hard code values 2025-10-17 10:59:11 -05:00
Armando Diaz
a9bf480954 test deploy 2025-10-17 10:52:18 -05:00
Armando Diaz
4afbc53420 revert dockerfile config 2025-10-17 10:49:43 -05:00
Armando Diaz
d0594fb161 try npm ci instead 2025-10-17 10:08:45 -05:00
Armando Diaz
7a0e0c95aa update docker image 2025-10-17 09:55:41 -05:00
943cd6935b Fix package age checker to handle version ranges
- Handle version patterns like "19.2.x" and "5.x.x"
- Fetch all versions and find latest matching the pattern
- Resolve version ranges to actual version numbers before checking age

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 08:44:32 -05:00
4a270dbfe3 Add npm package age verification system
Problem: Need to ensure all npm packages are at least 2 weeks old before use

Solution:
- Created check-package-age.js script to verify package publish dates
- Added .npmrc to enforce exact version installation
- Created pin-old-versions.sh helper script
- Documented complete workflow in NPM-PACKAGE-AGE-POLICY.md

Usage:
  node scripts/check-package-age.js  # Verify all packages ≥ 2 weeks old
  npm ci                              # Install exact versions from lock file

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 08:43:38 -05:00
Armando Diaz
e08ab62a32 chart and ci tweaks 2025-10-17 08:13:54 -05:00
Armando Diaz
c7ae399615 fix values file location 2025-10-17 08:01:55 -05:00
33d06bc94d Clean up Helm directory and update documentation
- Removed deprecated chart files from helm/ root directory
- Updated all Helm documentation to reference warehouse13 chart
- Changed database name from 'datalake' to 'warehouse13' in values.yaml
- Updated helm command examples in SUMMARY.md
- Fixed migration instructions in helm/README.md
- Updated PostgreSQL backup/restore commands with correct database name

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 08:01:05 -05:00
e2e5c683e4 Fix Helm template error: update _helpers.tpl to use app instead of api
- Changed .Values.api.env.databaseUrl to .Values.app.env.databaseUrl
- This aligns with the unified architecture where api and frontend are combined into a single app
- Chart now passes helm lint successfully

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 07:55:51 -05:00
Armando Diaz
838e145598 fix release name 2025-10-16 21:20:15 -05:00
15e0f886d7 Merge pull request 'main' (#4) from main into pipeline
Reviewed-on: mondo/SIM-Data-Platform#4
2025-10-16 21:14:55 -05:00
Armando Diaz
7c50f0a59a fix ci var 2025-10-16 20:49:49 -05:00
Armando Diaz
6508363c12 test deploy 2025-10-16 18:54:18 -05:00
80242b9602 Reorganize project structure: move docs and scripts to proper directories
Changes:
- Created scripts/ directory for build and utility scripts
- Moved build-for-airgap.sh to scripts/
- Moved check-ready.sh to scripts/
- Kept quickstart scripts in root for easy access
- Moved HELM-DEPLOYMENT.md to docs/

Updated references:
- README.md: Updated link to docs/HELM-DEPLOYMENT.md
- docs/DEPLOYMENT.md: Updated paths to scripts/build-for-airgap.sh
- quickstart-airgap.sh: Updated path to scripts/build-for-airgap.sh
- scripts/check-ready.sh: Updated self-reference path
- helm/warehouse13/QUICKSTART.md: Updated HELM-DEPLOYMENT.md path
- helm/README.md: Updated HELM-DEPLOYMENT.md path

Directory structure now:
/
├── README.md (root)
├── quickstart.sh (root - easy access)
├── quickstart-airgap.sh (root - easy access)
├── docs/ (all documentation)
│   ├── API.md
│   ├── ARCHITECTURE.md
│   ├── DEPLOYMENT.md
│   ├── FEATURES.md
│   ├── FRONTEND_SETUP.md
│   ├── HELM-DEPLOYMENT.md (moved here)
│   └── SUMMARY.md
├── scripts/ (build and utility scripts)
│   ├── build-for-airgap.sh (moved here)
│   └── check-ready.sh (moved here)
└── helm/
    └── warehouse13/ (Helm chart with docs)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 17:23:03 -05:00
4641cbb3fa Update Helm chart to use unified app image (API + Frontend)
Changes:
- Replaced separate api and frontend deployments with single unified app deployment
- Updated values.yaml: Changed from api/frontend configs to single app config
- Renamed templates: api-deployment.yaml → app-deployment.yaml
- Removed frontend-deployment.yaml and frontend-service.yaml (no longer needed)
- Updated app image to warehouse13/app (multi-stage Docker build)
- Combined resource allocations: 384Mi memory, 350m CPU (up from separate totals)
- Updated all example values files (dev, production, air-gapped)
- Updated NOTES.txt to reflect single service on port 8000
- Updated ingress to route all traffic to single app service
- Added ARCHITECTURE.md documenting the unified container approach

Architecture:
The application now uses a multi-stage Docker build:
1. Stage 1: Builds Angular frontend with Node
2. Stage 2: Python FastAPI backend that serves static frontend from /static

Benefits:
- Simplified deployment (1 container instead of 2)
- Reduced resource usage (no separate nginx)
- Easier scaling (1 deployment to manage)
- Consistent versioning (frontend/backend always match)

Access pattern:
- http://localhost:8000     → Angular frontend
- http://localhost:8000/api → FastAPI REST API
- http://localhost:8000/docs → API documentation
- http://localhost:8000/health → Health check

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 17:11:39 -05:00
59001222a0 Add comprehensive Warehouse13 Helm chart with configurable images
Features:
- Complete Helm chart at helm/warehouse13/ with Warehouse13 branding
- Configurable images for all components (PostgreSQL, MinIO, API, Frontend)
- Support for 3 deployment scenarios: dev, production, air-gapped
- 14 Kubernetes templates: Deployments, StatefulSets, Services, Ingress
- Persistent storage with configurable storage classes
- Health checks for all services
- Ingress with TLS support
- Security contexts and RBAC
- Comprehensive documentation:
  - HELM-DEPLOYMENT.md (main Kubernetes guide)
  - helm/warehouse13/README.md (full chart docs)
  - helm/warehouse13/QUICKSTART.md (5-min deployment)
  - Example values files (dev, production, air-gapped)
- Updated main README.md with Helm deployment instructions
- Marked old helm chart as deprecated

All component images fully configurable via values.yaml:
- postgres:15-alpine
- minio/minio:latest
- warehouse13/api:latest
- warehouse13/frontend:latest

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 17:04:12 -05:00
Armando Diaz
5e958ac8c3 helm chart updates 2025-10-16 16:41:21 -05:00
7126c618ea Merge pull request 'f/npm' (#3) from f/npm into main
Reviewed-on: mondo/SIM-Data-Platform#3
2025-10-16 15:49:00 -05:00
Patel (US), Pratik
543617cc08 Merge remote-tracking branch 'origin/pipeline' into f/npm 2025-10-16 15:17:24 -05:00
Armando Diaz
a1151d5e89 allow test job to fail 2025-10-16 15:09:26 -05:00
Armando Diaz
10b95ec5ef fix typo 2025-10-16 15:03:05 -05:00
Armando Diaz
bf5e5c7542 build container test 2025-10-16 14:52:00 -05:00
Patel (US), Pratik
090361cf66 test npm changes 2025-10-16 14:44:08 -05:00
pratik
18e70cd445 Toggle NPM througn env file 2025-10-16 14:25:08 -05:00
a256e01444 Merge pull request 'f/app' (#2) from f/app into main
Reviewed-on: mondo/SIM-Data-Platform#2
2025-10-16 13:57:24 -05:00
pratik
5920bf1617 Update MR 2025-10-16 13:49:12 -05:00
pratik
122e3f2edc Update gitignore, combined docker for frotnend and api 2025-10-16 13:38:11 -05:00
pratik
2584e92af2 Update build process, gitignore packagelock: 2025-10-16 13:02:50 -05:00
Patel (US), Pratik
1016fee300 change ci image 2025-10-16 12:13:38 -05:00
Armando Diaz
cda0e99ce7 remove curl 2025-10-16 12:08:02 -05:00
Armando Diaz
a08b7af8ca update npm repo 2025-10-16 12:07:30 -05:00
Armando Diaz
7305cef18c update image 2025-10-16 12:06:17 -05:00
Patel (US), Pratik
450faad45c Merge branch 'lock2' 2025-10-16 12:04:58 -05:00
Patel (US), Pratik
16d1afc44b Build with BSF arti 2025-10-16 12:00:34 -05:00
Armando Diaz
a607a3f15b install curl 2025-10-16 11:51:22 -05:00
Armando Diaz
009c3261c8 update image for bsf 2025-10-16 11:47:02 -05:00
Armando Diaz
e7532f0324 test ci job 2025-10-16 11:42:26 -05:00
7ea16fe48e 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
c4d325ecd3 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
9e3af1fc07 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
bd6c2ce9c1 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
59defcefe5 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
d9c6f490f0 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
a3a2cec9cf 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
2054181228 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
6c01329f27 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
c177be326c 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
972bb50c64 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
20a4ea1655 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
0856ca5b7a 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
0e5abbbece 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
d69c209101 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
98 changed files with 7011 additions and 1889 deletions

View File

@@ -27,3 +27,9 @@ MINIO_SECURE=false
API_HOST=0.0.0.0
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/

View File

@@ -0,0 +1,26 @@
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,3 +86,10 @@ 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,164 +1,42 @@
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
# Test stage
test:
stage: test
image: python:3.11-slim
before_script:
- apt-get update && apt-get install -y gcc postgresql-client
- pip install -r requirements.txt
- pip install pytest pytest-asyncio httpx
script:
- echo "Running tests..."
- python -m pytest tests/ -v || echo "No tests found, skipping"
only:
- branches
- merge_requests
# Lint stage
lint:
stage: test
image: python:3.11-slim
before_script:
- pip install flake8 black
script:
- echo "Running linters..."
- flake8 app/ --max-line-length=120 --ignore=E203,W503 || true
- black --check app/ || true
only:
- branches
- merge_requests
allow_failure: true
# Build Docker image
build:
build_container:
stage: build
image: docker:24
services:
- docker:24-dind
image: deps.global.bsf.tools/quay.io/buildah/stable:latest
variables:
IMAGE_NAME: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- 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"
script:
- echo "Building Docker image..."
- docker build -t $IMAGE_TAG -t $LATEST_TAG .
- docker push $IMAGE_TAG
- docker push $LATEST_TAG
only:
- main
- master
- develop
- tags
# - 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 to development
deploy:dev:
deploy_helm_charts:
stage: deploy
image: alpine/helm:latest
before_script:
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl
- mv kubectl /usr/local/bin/
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG_DEV" | base64 -d > ~/.kube/config
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:
- echo "Deploying to development environment..."
- kubectl config use-context $CONTEXT
- echo "Deploy - $CI_REGISTRY_NAME - $CI_COMMIT_REF_SLUG - $CI_COMMIT_SHORT_SHA"
- |
helm upgrade --install datalake-dev ./helm \
--namespace datalake-dev \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set ingress.enabled=true \
--set ingress.hosts[0].host=datalake-dev.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--wait \
--timeout 5m
environment:
name: development
url: https://datalake-dev.example.com
only:
- develop
when: manual
# Deploy to staging
deploy:staging:
stage: deploy
image: alpine/helm:latest
before_script:
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl
- mv kubectl /usr/local/bin/
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG_STAGING" | base64 -d > ~/.kube/config
script:
- echo "Deploying to staging environment..."
- |
helm upgrade --install datalake-staging ./helm \
--namespace datalake-staging \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set ingress.enabled=true \
--set ingress.hosts[0].host=datalake-staging.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set resources.requests.cpu=1000m \
--set resources.requests.memory=1Gi \
--wait \
--timeout 5m
environment:
name: staging
url: https://datalake-staging.example.com
only:
- main
- master
when: manual
# Deploy to production
deploy:prod:
stage: deploy
image: alpine/helm:latest
before_script:
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl
- mv kubectl /usr/local/bin/
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG_PROD" | base64 -d > ~/.kube/config
script:
- echo "Deploying to production environment..."
- |
helm upgrade --install datalake ./helm \
--namespace datalake-prod \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set replicaCount=3 \
--set ingress.enabled=true \
--set ingress.hosts[0].host=datalake.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set resources.requests.cpu=2000m \
--set resources.requests.memory=2Gi \
--set autoscaling.enabled=true \
--set autoscaling.minReplicas=3 \
--set autoscaling.maxReplicas=10 \
--wait \
--timeout 10m
environment:
name: production
url: https://datalake.example.com
only:
- tags
when: manual
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

164
.gitlab-ci.yml.old Normal file
View File

@@ -0,0 +1,164 @@
stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
LATEST_TAG: $CI_REGISTRY_IMAGE:latest
# Test stage
test:
stage: test
image: python:3.11-slim
before_script:
- apt-get update && apt-get install -y gcc postgresql-client
- pip install -r requirements.txt
- pip install pytest pytest-asyncio httpx
script:
- echo "Running tests..."
- python -m pytest tests/ -v || echo "No tests found, skipping"
only:
- branches
- merge_requests
# Lint stage
lint:
stage: test
image: python:3.11-slim
before_script:
- pip install flake8 black
script:
- echo "Running linters..."
- flake8 app/ --max-line-length=120 --ignore=E203,W503 || true
- black --check app/ || true
only:
- branches
- merge_requests
allow_failure: true
# Build Docker image
build:
stage: build
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- echo "Building Docker image..."
- docker build -t $IMAGE_TAG -t $LATEST_TAG .
- docker push $IMAGE_TAG
- docker push $LATEST_TAG
only:
- main
- master
- develop
- tags
# Deploy to development
deploy:dev:
stage: deploy
image: alpine/helm:latest
before_script:
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl
- mv kubectl /usr/local/bin/
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG_DEV" | base64 -d > ~/.kube/config
script:
- echo "Deploying to development environment..."
- |
helm upgrade --install datalake-dev ./helm \
--namespace datalake-dev \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set ingress.enabled=true \
--set ingress.hosts[0].host=datalake-dev.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--wait \
--timeout 5m
environment:
name: development
url: https://datalake-dev.example.com
only:
- develop
when: manual
# Deploy to staging
deploy:staging:
stage: deploy
image: alpine/helm:latest
before_script:
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl
- mv kubectl /usr/local/bin/
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG_STAGING" | base64 -d > ~/.kube/config
script:
- echo "Deploying to staging environment..."
- |
helm upgrade --install datalake-staging ./helm \
--namespace datalake-staging \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set ingress.enabled=true \
--set ingress.hosts[0].host=datalake-staging.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set resources.requests.cpu=1000m \
--set resources.requests.memory=1Gi \
--wait \
--timeout 5m
environment:
name: staging
url: https://datalake-staging.example.com
only:
- main
- master
when: manual
# Deploy to production
deploy:prod:
stage: deploy
image: alpine/helm:latest
before_script:
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl
- mv kubectl /usr/local/bin/
- mkdir -p ~/.kube
- echo "$KUBE_CONFIG_PROD" | base64 -d > ~/.kube/config
script:
- echo "Deploying to production environment..."
- |
helm upgrade --install datalake ./helm \
--namespace datalake-prod \
--create-namespace \
--set image.repository=$CI_REGISTRY_IMAGE \
--set image.tag=$CI_COMMIT_SHORT_SHA \
--set replicaCount=3 \
--set ingress.enabled=true \
--set ingress.hosts[0].host=datalake.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set resources.requests.cpu=2000m \
--set resources.requests.memory=2Gi \
--set autoscaling.enabled=true \
--set autoscaling.minReplicas=3 \
--set autoscaling.maxReplicas=10 \
--wait \
--timeout 10m
environment:
name: production
url: https://datalake.example.com
only:
- tags
when: manual

View File

@@ -1,465 +0,0 @@
# Deployment Guide
This guide covers deploying the Test Artifact Data Lake in various environments.
## Table of Contents
- [Local Development](#local-development)
- [Docker Compose](#docker-compose)
- [Kubernetes/Helm](#kuberneteshelm)
- [AWS Deployment](#aws-deployment)
- [Self-Hosted Deployment](#self-hosted-deployment)
- [GitLab CI/CD](#gitlab-cicd)
---
## Local Development
### Prerequisites
- Python 3.11+
- PostgreSQL 15+
- MinIO or AWS S3 access
### Steps
1. **Create virtual environment:**
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
2. **Install dependencies:**
```bash
pip install -r requirements.txt
```
3. **Set up PostgreSQL:**
```bash
createdb datalake
```
4. **Configure environment:**
```bash
cp .env.example .env
# Edit .env with your configuration
```
5. **Run the application:**
```bash
python -m uvicorn app.main:app --reload
```
---
## Docker Compose
### Quick Start
1. **Start all services:**
```bash
docker-compose up -d
```
2. **Check logs:**
```bash
docker-compose logs -f api
```
3. **Stop services:**
```bash
docker-compose down
```
### Services Included
- PostgreSQL (port 5432)
- MinIO (port 9000, console 9001)
- API (port 8000)
### Customization
Edit `docker-compose.yml` to:
- Change port mappings
- Adjust resource limits
- Add environment variables
- Configure volumes
---
## Kubernetes/Helm
### Prerequisites
- Kubernetes cluster (1.24+)
- Helm 3.x
- kubectl configured
### Installation
1. **Add dependencies (if using PostgreSQL/MinIO from Bitnami):**
```bash
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
```
2. **Install with default values:**
```bash
helm install datalake ./helm \
--namespace datalake \
--create-namespace
```
3. **Custom installation:**
```bash
helm install datalake ./helm \
--namespace datalake \
--create-namespace \
--set image.repository=your-registry/datalake \
--set image.tag=1.0.0 \
--set ingress.enabled=true \
--set ingress.hosts[0].host=datalake.yourdomain.com
```
### Configuration Options
**Image:**
```bash
--set image.repository=your-registry/datalake
--set image.tag=1.0.0
--set image.pullPolicy=Always
```
**Resources:**
```bash
--set resources.requests.cpu=1000m
--set resources.requests.memory=1Gi
--set resources.limits.cpu=2000m
--set resources.limits.memory=2Gi
```
**Autoscaling:**
```bash
--set autoscaling.enabled=true
--set autoscaling.minReplicas=3
--set autoscaling.maxReplicas=10
--set autoscaling.targetCPUUtilizationPercentage=80
```
**Ingress:**
```bash
--set ingress.enabled=true
--set ingress.className=nginx
--set ingress.hosts[0].host=datalake.example.com
--set ingress.hosts[0].paths[0].path=/
--set ingress.hosts[0].paths[0].pathType=Prefix
```
### Upgrade
```bash
helm upgrade datalake ./helm \
--namespace datalake \
--set image.tag=1.1.0
```
### Uninstall
```bash
helm uninstall datalake --namespace datalake
```
---
## AWS Deployment
### Using AWS S3 Storage
1. **Create S3 bucket:**
```bash
aws s3 mb s3://your-test-artifacts-bucket
```
2. **Create IAM user with S3 access:**
```bash
aws iam create-user --user-name datalake-service
aws iam attach-user-policy --user-name datalake-service \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
```
3. **Generate access keys:**
```bash
aws iam create-access-key --user-name datalake-service
```
4. **Deploy with Helm:**
```bash
helm install datalake ./helm \
--namespace datalake \
--create-namespace \
--set config.storageBackend=s3 \
--set aws.enabled=true \
--set aws.accessKeyId=YOUR_ACCESS_KEY \
--set aws.secretAccessKey=YOUR_SECRET_KEY \
--set aws.region=us-east-1 \
--set aws.bucketName=your-test-artifacts-bucket \
--set minio.enabled=false
```
### Using EKS
1. **Create EKS cluster:**
```bash
eksctl create cluster \
--name datalake-cluster \
--region us-east-1 \
--nodegroup-name standard-workers \
--node-type t3.medium \
--nodes 3
```
2. **Configure kubectl:**
```bash
aws eks update-kubeconfig --name datalake-cluster --region us-east-1
```
3. **Deploy application:**
```bash
helm install datalake ./helm \
--namespace datalake \
--create-namespace \
--set config.storageBackend=s3
```
### Using RDS for PostgreSQL
```bash
helm install datalake ./helm \
--namespace datalake \
--create-namespace \
--set postgresql.enabled=false \
--set config.databaseUrl="postgresql://user:pass@your-rds-endpoint:5432/datalake"
```
---
## Self-Hosted Deployment
### Using MinIO
1. **Deploy MinIO:**
```bash
helm install minio bitnami/minio \
--namespace datalake \
--create-namespace \
--set auth.rootUser=admin \
--set auth.rootPassword=adminpassword \
--set persistence.size=100Gi
```
2. **Deploy application:**
```bash
helm install datalake ./helm \
--namespace datalake \
--set config.storageBackend=minio \
--set minio.enabled=false \
--set minio.endpoint=minio:9000 \
--set minio.accessKey=admin \
--set minio.secretKey=adminpassword
```
### On-Premise Kubernetes
1. **Prepare persistent volumes:**
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: datalake-postgres-pv
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /data/postgres
```
2. **Deploy with local storage:**
```bash
helm install datalake ./helm \
--namespace datalake \
--create-namespace \
--set postgresql.persistence.storageClass=local-storage \
--set minio.persistence.storageClass=local-storage
```
---
## GitLab CI/CD
### Setup
1. **Configure GitLab variables:**
Go to Settings → CI/CD → Variables and add:
| Variable | Description | Protected | Masked |
|----------|-------------|-----------|---------|
| `CI_REGISTRY_USER` | Docker registry username | No | No |
| `CI_REGISTRY_PASSWORD` | Docker registry password | No | Yes |
| `KUBE_CONFIG_DEV` | Base64 kubeconfig for dev | No | Yes |
| `KUBE_CONFIG_STAGING` | Base64 kubeconfig for staging | Yes | Yes |
| `KUBE_CONFIG_PROD` | Base64 kubeconfig for prod | Yes | Yes |
2. **Encode kubeconfig:**
```bash
cat ~/.kube/config | base64 -w 0
```
### Pipeline Stages
1. **Test**: Runs on all branches and MRs
2. **Build**: Builds Docker image on main/develop/tags
3. **Deploy**: Manual deployment to dev/staging/prod
### Deployment Flow
**Development:**
```bash
git push origin develop
# Manually trigger deploy:dev job in GitLab
```
**Staging:**
```bash
git push origin main
# Manually trigger deploy:staging job in GitLab
```
**Production:**
```bash
git tag v1.0.0
git push origin v1.0.0
# Manually trigger deploy:prod job in GitLab
```
### Customizing Pipeline
Edit `.gitlab-ci.yml` to:
- Add more test stages
- Change deployment namespaces
- Adjust Helm values per environment
- Add security scanning
- Configure rollback procedures
---
## Monitoring
### Health Checks
```bash
# Kubernetes
kubectl get pods -n datalake
kubectl logs -f -n datalake deployment/datalake
# Direct
curl http://localhost:8000/health
```
### Metrics
Add Prometheus monitoring:
```bash
helm install datalake ./helm \
--set metrics.enabled=true \
--set serviceMonitor.enabled=true
```
---
## Backup and Recovery
### Database Backup
```bash
# PostgreSQL
kubectl exec -n datalake deployment/datalake-postgresql -- \
pg_dump -U user datalake > backup.sql
# Restore
kubectl exec -i -n datalake deployment/datalake-postgresql -- \
psql -U user datalake < backup.sql
```
### Storage Backup
**S3:**
```bash
aws s3 sync s3://your-bucket s3://backup-bucket
```
**MinIO:**
```bash
mc mirror minio/test-artifacts backup/test-artifacts
```
---
## Troubleshooting
### Pod Not Starting
```bash
kubectl describe pod -n datalake <pod-name>
kubectl logs -n datalake <pod-name>
```
### Database Connection Issues
```bash
kubectl exec -it -n datalake deployment/datalake -- \
psql $DATABASE_URL
```
### Storage Issues
```bash
# Check MinIO
kubectl port-forward -n datalake svc/minio 9000:9000
# Access http://localhost:9000
```
---
## Security Considerations
1. **Use secrets management:**
- Kubernetes Secrets
- AWS Secrets Manager
- HashiCorp Vault
2. **Enable TLS:**
- Configure ingress with TLS certificates
- Use cert-manager for automatic certificates
3. **Network policies:**
- Restrict pod-to-pod communication
- Limit external access
4. **RBAC:**
- Configure Kubernetes RBAC
- Limit service account permissions
---
## Performance Tuning
### Database
- Increase connection pool size
- Add database indexes
- Configure autovacuum
### API
- Increase replica count
- Configure horizontal pod autoscaling
- Adjust resource requests/limits
### Storage
- Use CDN for frequently accessed files
- Configure S3 Transfer Acceleration
- Optimize MinIO deployment

View File

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

View File

@@ -1,4 +1,4 @@
# Obsidian
# Warehouse13
**Enterprise Test Artifact Storage**
@@ -36,7 +36,7 @@ A lightweight, cloud-native API for storing and querying test artifacts includin
## Quick Start
### One-Command Setup
### Standard Deployment (Internet Access)
**Linux/macOS:**
```bash
@@ -48,11 +48,20 @@ 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:**
```bash
./quickstart-airgap.sh
```
This script:
1. Builds Angular locally (where npm works)
2. Packages pre-built files into Docker
3. Starts all services
See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for detailed instructions.
### Manual Setup with Docker Compose
1. Clone the repository:
@@ -205,35 +214,54 @@ MINIO_BUCKET_NAME=test-artifacts
### Kubernetes with Helm
1. Build and push Docker image:
**Quick Start:**
```bash
docker build -t your-registry/datalake:latest .
docker push your-registry/datalake:latest
helm install warehouse13 ./helm/warehouse13 --namespace warehouse13 --create-namespace
```
2. Install with Helm:
**Production Deployment:**
```bash
helm install datalake ./helm \
--set image.repository=your-registry/datalake \
--set image.tag=latest \
--namespace datalake \
--create-namespace
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./helm/warehouse13/values-production.yaml
```
3. Access the API:
**Air-Gapped Deployment:**
```bash
kubectl port-forward -n datalake svc/datalake 8000:8000
helm install warehouse13 ./helm/warehouse13 \
--namespace warehouse13 \
--create-namespace \
--values ./helm/warehouse13/values-airgapped.yaml
```
**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
Edit `helm/values.yaml` to customize:
- Replica count
- Resource limits
- Storage backend (S3 vs MinIO)
- Ingress settings
- PostgreSQL settings
- Autoscaling
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
### GitLab CI/CD

View File

@@ -1,4 +1,4 @@
from fastapi import FastAPI
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
# Create FastAPI app
app = FastAPI(
title="Obsidian",
title="Warehouse13",
description="Enterprise Test Artifact Storage - API for storing and querying test artifacts including CSV, JSON, binary files, and packet captures",
version="1.0.0",
docs_url="/docs",
@@ -39,10 +39,8 @@ app.add_middleware(
app.include_router(artifacts_router)
app.include_router(seed_router)
# Mount static files
# Static directory setup
static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static")
if os.path.exists(static_dir):
app.mount("/static", StaticFiles(directory=static_dir), name="static")
@app.on_event("startup")
@@ -59,7 +57,7 @@ async def startup_event():
async def api_root():
"""API root endpoint"""
return {
"message": "Obsidian - Enterprise Test Artifact Storage",
"message": "Warehouse13 - Enterprise Test Artifact Storage",
"version": "1.0.0",
"docs": "/docs",
"deployment_mode": settings.deployment_mode,
@@ -75,7 +73,31 @@ async def ui_root():
return FileResponse(index_path)
else:
return {
"message": "Obsidian - Enterprise Test Artifact Storage",
"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
}
# 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.",

View File

@@ -1,5 +1,7 @@
version: '3.8'
name: warehouse13
services:
postgres:
image: postgres:15-alpine
@@ -34,8 +36,12 @@ services:
timeout: 5s
retries: 5
api:
build: .
app:
container_name: warehouse13-app
build:
context: .
args:
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
ports:
- "8000:8000"
environment:

View File

140
docs/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,140 @@
# Deployment Options
This project supports two deployment strategies for the Angular frontend, depending on your environment's network access.
## Option 1: Standard Build (Internet Access Required)
Use the standard `Dockerfile.frontend` which builds the Angular app inside Docker.
**Requirements:**
- Internet access to npm registry
- Docker build environment
**Usage:**
```bash
./quickstart.sh
# or
docker-compose up -d --build
```
This uses `Dockerfile.frontend` which:
1. Installs npm dependencies in Docker
2. Builds Angular app in Docker
3. Serves with nginx
---
## Option 2: Pre-built Deployment (Air-Gapped/Restricted Environments) ⭐ RECOMMENDED
Use `Dockerfile.frontend.prebuilt` for environments with restricted npm access.
**Requirements:**
- Node.js 18+ installed locally (on a machine with npm access)
- npm installed locally
- No internet required during Docker build
**Note:** This project uses Angular 17 with webpack bundler (not Vite) for better compatibility with restricted npm environments.
**Usage:**
### Quick Start (Recommended)
```bash
./quickstart-airgap.sh
```
This script will:
1. Build the Angular app locally
2. Start all Docker containers
3. Verify the deployment
### Manual Steps
### Step 1: Build Angular app locally
**IMPORTANT:** You MUST run this step BEFORE `docker-compose up`!
```bash
# Option A: Use the helper script
./scripts/build-for-airgap.sh
# Option B: Build manually
cd frontend
npm install # Only needed once or when dependencies change
npm run build:prod
cd ..
```
This creates `frontend/dist/frontend/browser/` which Docker will copy.
### Step 2: Update docker-compose.yml
Edit `docker-compose.yml` and change the frontend dockerfile:
```yaml
frontend:
build:
context: .
dockerfile: Dockerfile.frontend.prebuilt # <-- Change this line
ports:
- "4200:80"
depends_on:
- api
```
### Step 3: Build and deploy
```bash
docker-compose up -d --build
```
This uses `Dockerfile.frontend.prebuilt` which:
1. Copies pre-built Angular files from `frontend/dist/`
2. Serves with nginx
3. No npm/node required in Docker
---
## Troubleshooting
### Build Tool Package Issues
If you see errors about missing packages like:
```
Cannot find package "vite"
Cannot find package "esbuild"
Cannot find package "rollup"
```
**Solution:** This project uses Angular 17 with webpack bundler specifically to avoid these issues. If you still encounter package access problems in your restricted environment, use Option 2 (Pre-built) deployment above, which eliminates all npm dependencies in Docker.
### Custom NPM Registry
For both options, you can use a custom npm registry:
```bash
# Set in .env file
NPM_REGISTRY=http://your-npm-proxy:8081/repository/npm-proxy/
# Or inline
NPM_REGISTRY=http://your-proxy ./quickstart.sh
```
---
## Recommendation
- **Development/Cloud**: Use Option 1 (standard)
- **Air-gapped/Enterprise**: Use Option 2 (pre-built) ⭐ **RECOMMENDED**
- **CI/CD**: Use Option 2 for faster, more reliable builds
- **Restricted npm access**: Use Option 2 (pre-built) ⭐ **REQUIRED**
---
## Build Strategy for Restricted Environments
**This project uses Angular 17 with webpack** instead of Angular 19 with Vite specifically for better compatibility with restricted npm environments. Webpack has fewer platform-specific binary dependencies than Vite.
If you encounter any package access errors during builds:
- `Cannot find package "vite"`
- `Cannot find package "rollup"`
- `Cannot find package "esbuild"`
- Any platform-specific binary errors
**Solution:** Use Option 2 (Pre-built) deployment. This completely avoids npm installation in Docker and eliminates all build tool dependency issues.

517
docs/HELM-DEPLOYMENT.md Normal file
View File

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

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

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

17
frontend/.editorconfig Normal file
View File

@@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

43
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
__screenshots__/
# System files
.DS_Store
Thumbs.db

178
frontend/README.md Normal file
View File

@@ -0,0 +1,178 @@
# Warehouse13 Frontend - Angular Application
Modern Angular application for the Warehouse13 Test Artifact Data Lake.
## Features
- **Angular 20** with standalone components
- **TypeScript** for type safety
- **Reactive Forms** for upload and query functionality
- **RxJS** for reactive programming
- **Auto-refresh** artifacts every 5 seconds
- **Client-side sorting and filtering**
- **Dark theme** UI
- **Responsive design**
## Development
### Prerequisites
- Node.js 24.x or higher
- npm 11.x or higher
- Backend API running on port 8000
### Installation
```bash
cd frontend
npm install
```
### Run Development Server
```bash
npm start
```
The application will be available at `http://localhost:4200/`
The development server includes a proxy configuration that forwards `/api` requests to `http://localhost:8000`.
### Build for Production
```bash
npm run build:prod
```
Build artifacts will be in the `dist/frontend/browser` directory.
## Project Structure
```
src/
├── app/
│ ├── components/
│ │ ├── artifacts-list/ # Main artifacts table with sorting, filtering, auto-refresh
│ │ ├── upload-form/ # Reactive form for uploading artifacts
│ │ └── query-form/ # Advanced query interface
│ ├── models/
│ │ └── artifact.model.ts # TypeScript interfaces for type safety
│ ├── services/
│ │ └── artifact.ts # HTTP service for API calls
│ ├── app.ts # Main app component with routing
│ ├── app.config.ts # Application configuration
│ └── app.routes.ts # Route definitions
├── styles.css # Global dark theme styles
└── main.ts # Application bootstrap
## Key Components
### ArtifactsListComponent
- Displays artifacts in a sortable, filterable table
- Auto-refreshes every 5 seconds (toggleable)
- Client-side search across all fields
- Download and delete actions
- Detail modal for full artifact information
- Tags displayed as inline badges
- SIM source grouping support
### UploadFormComponent
- Reactive form with validation
- File upload with drag-and-drop support
- Required fields: File, Sim Source, Uploaded By, Tags
- Optional fields: SIM Source ID (for grouping), Test Result, Version, Description, Test Config, Custom Metadata
- JSON validation for config fields
- Real-time upload status feedback
### QueryFormComponent
- Advanced search with multiple filter criteria
- Filter by: filename, file type, test name, test suite, test result, SIM source ID, tags, date range
- Results emitted to artifacts list
## API Integration
The frontend communicates with the FastAPI backend through the `ArtifactService`:
- `GET /api/v1/artifacts/` - List all artifacts
- `GET /api/v1/artifacts/:id` - Get single artifact
- `POST /api/v1/artifacts/upload` - Upload new artifact
- `POST /api/v1/artifacts/query` - Query with filters
- `GET /api/v1/artifacts/:id/download` - Download artifact file
- `DELETE /api/v1/artifacts/:id` - Delete artifact
- `POST /api/v1/seed/generate/:count` - Generate seed data
## Configuration
### Proxy Configuration (`proxy.conf.json`)
```json
{
"/api": {
"target": "http://localhost:8000",
"secure": false,
"changeOrigin": true
}
}
```
This proxies all `/api` requests to the backend during development.
## Styling
The application uses a custom dark theme with:
- Dark blue/slate color palette
- Gradient headers
- Responsive design
- Smooth transitions and hover effects
- Tag badges for categorization
- Result badges for test statuses
## Browser Support
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
## Development Tips
1. **Hot Reload**: Changes to TypeScript files automatically trigger recompilation
2. **Type Safety**: Use TypeScript interfaces in `models/` for all API responses
3. **State Management**: Currently using component-level state; consider NgRx for complex state
4. **Testing**: Run `npm test` for unit tests (Jasmine/Karma)
## Deployment
For production deployment, build the application and serve the `dist/frontend/browser` directory with your web server (nginx, Apache, etc.).
Example nginx configuration:
```nginx
server {
listen 80;
server_name your-domain.com;
root /path/to/dist/frontend/browser;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8000;
}
}
```
## Future Enhancements
- [ ] Add NgRx for state management
- [ ] Implement WebSocket for real-time updates
- [ ] Add Angular Material components
- [ ] Unit and E2E tests
- [ ] PWA support
- [ ] Drag-and-drop file upload
- [ ] Bulk operations
- [ ] Export to CSV/JSON
## License
Same as parent project

100
frontend/angular.json Normal file
View File

@@ -0,0 +1,100 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"frontend": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/frontend",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
},
"configurations": {
"production": {
"optimization" : {
"fonts": false
},
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "frontend:build:production"
},
"development": {
"buildTarget": "frontend:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}

83
frontend/package.json Normal file
View File

@@ -0,0 +1,83 @@
{
"name": "frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build",
"build:prod": "ng build --configuration production",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"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",
"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",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.9.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "5.x.x",
"undici-types": "7.12.0",
"node-releases": "2.0.21",
"node-gyp": "11.4.2",
"tar": "7.4.3",
"minizlib": "3.0.2",
"immutable": "5.1.3",
"exponential-backoff": "3.1.2",
"emoji-regex": "10.5.0",
"electron-to-chromium": "1.5.221",
"caniuse-lite": "1.0.30001743"
},
"resolutions": {
"undici-types": "7.12.0",
"node-releases": "2.0.21",
"node-gyp": "11.4.2",
"tar": "7.4.3",
"minizlib": "3.0.2",
"immutable": "5.1.3",
"exponential-backoff": "3.1.2",
"emoji-regex": "10.5.0",
"electron-to-chromium": "1.5.221",
"caniuse-lite": "1.0.30001743"
},
"overrides": {
"undici-types": "7.12.0",
"node-releases": "2.0.21",
"node-gyp": "11.4.2",
"tar": "7.4.3",
"minizlib": "3.0.2",
"immutable": "5.1.3",
"exponential-backoff": "3.1.2",
"emoji-regex": "10.5.0",
"electron-to-chromium": "1.5.221",
"caniuse-lite": "1.0.30001743"
}
}

7
frontend/proxy.conf.json Normal file
View File

@@ -0,0 +1,7 @@
{
"/api": {
"target": "http://localhost:8000",
"secure": false,
"changeOrigin": true
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient()
]
};

134
frontend/src/app/app.css Normal file
View File

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

342
frontend/src/app/app.html Normal file
View File

@@ -0,0 +1,342 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * Delete the template below * * * * * * * * * -->
<!-- * * * * * * * to get started with your project! * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<style>
:host {
--bright-blue: oklch(51.01% 0.274 263.83);
--electric-violet: oklch(53.18% 0.28 296.97);
--french-violet: oklch(47.66% 0.246 305.88);
--vivid-pink: oklch(69.02% 0.277 332.77);
--hot-red: oklch(61.42% 0.238 15.34);
--orange-red: oklch(63.32% 0.24 31.68);
--gray-900: oklch(19.37% 0.006 300.98);
--gray-700: oklch(36.98% 0.014 302.71);
--gray-400: oklch(70.9% 0.015 304.04);
--red-to-pink-to-purple-vertical-gradient: linear-gradient(
180deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--red-to-pink-to-purple-horizontal-gradient: linear-gradient(
90deg,
var(--orange-red) 0%,
var(--vivid-pink) 50%,
var(--electric-violet) 100%
);
--pill-accent: var(--bright-blue);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 {
font-size: 3.125rem;
color: var(--gray-900);
font-weight: 500;
line-height: 100%;
letter-spacing: -0.125rem;
margin: 0;
font-family: "Inter Tight", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol";
}
p {
margin: 0;
color: var(--gray-700);
}
main {
width: 100%;
min-height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
box-sizing: inherit;
position: relative;
}
.angular-logo {
max-width: 9.2rem;
}
.content {
display: flex;
justify-content: space-around;
width: 100%;
max-width: 700px;
margin-bottom: 3rem;
}
.content h1 {
margin-top: 1.75rem;
}
.content p {
margin-top: 1.5rem;
}
.divider {
width: 1px;
background: var(--red-to-pink-to-purple-vertical-gradient);
margin-inline: 0.5rem;
}
.pill-group {
display: flex;
flex-direction: column;
align-items: start;
flex-wrap: wrap;
gap: 1.25rem;
}
.pill {
display: flex;
align-items: center;
--pill-accent: var(--bright-blue);
background: color-mix(in srgb, var(--pill-accent) 5%, transparent);
color: var(--pill-accent);
padding-inline: 0.75rem;
padding-block: 0.375rem;
border-radius: 2.75rem;
border: 0;
transition: background 0.3s ease;
font-family: var(--inter-font);
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.4rem;
letter-spacing: -0.00875rem;
text-decoration: none;
white-space: nowrap;
}
.pill:hover {
background: color-mix(in srgb, var(--pill-accent) 15%, transparent);
}
.pill-group .pill:nth-child(6n + 1) {
--pill-accent: var(--bright-blue);
}
.pill-group .pill:nth-child(6n + 2) {
--pill-accent: var(--electric-violet);
}
.pill-group .pill:nth-child(6n + 3) {
--pill-accent: var(--french-violet);
}
.pill-group .pill:nth-child(6n + 4),
.pill-group .pill:nth-child(6n + 5),
.pill-group .pill:nth-child(6n + 6) {
--pill-accent: var(--hot-red);
}
.pill-group svg {
margin-inline-start: 0.25rem;
}
.social-links {
display: flex;
align-items: center;
gap: 0.73rem;
margin-top: 1.5rem;
}
.social-links path {
transition: fill 0.3s ease;
fill: var(--gray-400);
}
.social-links a:hover svg path {
fill: var(--gray-900);
}
@media screen and (max-width: 650px) {
.content {
flex-direction: column;
width: max-content;
}
.divider {
height: 1px;
width: 100%;
background: var(--red-to-pink-to-purple-horizontal-gradient);
margin-block: 1.5rem;
}
}
</style>
<main class="main">
<div class="content">
<div class="left-side">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 982 239"
fill="none"
class="angular-logo"
>
<g clip-path="url(#a)">
<path
fill="url(#b)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
<path
fill="url(#c)"
d="M388.676 191.625h30.849L363.31 31.828h-35.758l-56.215 159.797h30.848l13.174-39.356h60.061l13.256 39.356Zm-65.461-62.675 21.602-64.311h1.227l21.602 64.311h-44.431Zm126.831-7.527v70.202h-28.23V71.839h27.002v20.374h1.392c2.782-6.71 7.2-12.028 13.255-15.956 6.056-3.927 13.584-5.89 22.503-5.89 8.264 0 15.465 1.8 21.684 5.318 6.137 3.518 10.964 8.673 14.319 15.382 3.437 6.71 5.074 14.81 4.992 24.383v76.175h-28.23v-71.92c0-8.019-2.046-14.237-6.219-18.819-4.173-4.5-9.819-6.791-17.102-6.791-4.91 0-9.328 1.063-13.174 3.272-3.846 2.128-6.792 5.237-9.001 9.328-2.046 4.009-3.191 8.918-3.191 14.728ZM589.233 239c-10.147 0-18.82-1.391-26.103-4.091-7.282-2.7-13.092-6.382-17.511-10.964-4.418-4.582-7.528-9.655-9.164-15.219l25.448-6.136c1.145 2.372 2.782 4.663 4.991 6.954 2.209 2.291 5.155 4.255 8.837 5.81 3.683 1.554 8.428 2.291 14.074 2.291 8.019 0 14.647-1.964 19.884-5.81 5.237-3.845 7.856-10.227 7.856-19.064v-22.665h-1.391c-1.473 2.946-3.601 5.892-6.383 9.001-2.782 3.109-6.464 5.645-10.965 7.691-4.582 2.046-10.228 3.109-17.101 3.109-9.165 0-17.511-2.209-25.039-6.545-7.446-4.337-13.42-10.883-17.757-19.474-4.418-8.673-6.628-19.473-6.628-32.565 0-13.091 2.21-24.301 6.628-33.383 4.419-9.082 10.311-15.955 17.839-20.7 7.528-4.746 15.874-7.037 25.039-7.037 7.037 0 12.846 1.145 17.347 3.518 4.582 2.373 8.182 5.236 10.883 8.51 2.7 3.272 4.746 6.382 6.137 9.327h1.554v-19.8h27.821v121.749c0 10.228-2.454 18.737-7.364 25.447-4.91 6.709-11.538 11.7-20.048 15.055-8.509 3.355-18.165 4.991-28.884 4.991Zm.245-71.266c5.974 0 11.047-1.473 15.302-4.337 4.173-2.945 7.446-7.118 9.573-12.519 2.21-5.482 3.274-12.027 3.274-19.637 0-7.609-1.064-14.155-3.274-19.8-2.127-5.646-5.318-10.064-9.491-13.255-4.174-3.11-9.329-4.746-15.384-4.746s-11.537 1.636-15.792 4.91c-4.173 3.272-7.365 7.772-9.492 13.418-2.128 5.727-3.191 12.191-3.191 19.392 0 7.2 1.063 13.745 3.273 19.228 2.127 5.482 5.318 9.736 9.573 12.764 4.174 3.027 9.41 4.582 15.629 4.582Zm141.56-26.51V71.839h28.23v119.786h-27.412v-21.273h-1.227c-2.7 6.709-7.119 12.191-13.338 16.446-6.137 4.255-13.747 6.382-22.748 6.382-7.855 0-14.81-1.718-20.783-5.237-5.974-3.518-10.72-8.591-14.075-15.382-3.355-6.709-5.073-14.891-5.073-24.464V71.839h28.312v71.921c0 7.609 2.046 13.664 6.219 18.083 4.173 4.5 9.655 6.709 16.365 6.709 4.173 0 8.183-.982 12.111-3.028 3.927-2.045 7.118-5.072 9.655-9.082 2.537-4.091 3.764-9.164 3.764-15.218Zm65.707-109.395v159.796h-28.23V31.828h28.23Zm44.841 162.169c-7.61 0-14.402-1.391-20.457-4.091-6.055-2.7-10.883-6.791-14.32-12.109-3.518-5.319-5.237-11.946-5.237-19.801 0-6.791 1.228-12.355 3.765-16.773 2.536-4.419 5.891-7.937 10.228-10.637 4.337-2.618 9.164-4.664 14.647-6.055 5.4-1.391 11.046-2.373 16.856-3.027 7.037-.737 12.683-1.391 17.102-1.964 4.337-.573 7.528-1.555 9.574-2.782 1.963-1.309 3.027-3.273 3.027-5.973v-.491c0-5.891-1.718-10.391-5.237-13.664-3.518-3.191-8.51-4.828-15.056-4.828-6.955 0-12.356 1.473-16.447 4.5-4.009 3.028-6.71 6.546-8.183 10.719l-26.348-3.764c2.046-7.282 5.483-13.336 10.31-18.328 4.746-4.909 10.638-8.59 17.511-11.045 6.955-2.455 14.565-3.682 22.912-3.682 5.809 0 11.537.654 17.265 2.045s10.965 3.6 15.711 6.71c4.746 3.109 8.51 7.282 11.455 12.6 2.864 5.318 4.337 11.946 4.337 19.883v80.184h-27.166v-16.446h-.9c-1.719 3.355-4.092 6.464-7.201 9.328-3.109 2.864-6.955 5.237-11.619 6.955-4.828 1.718-10.229 2.536-16.529 2.536Zm7.364-20.701c5.646 0 10.556-1.145 14.729-3.354 4.173-2.291 7.364-5.237 9.655-9.001 2.292-3.763 3.355-7.854 3.355-12.273v-14.155c-.9.737-2.373 1.391-4.5 2.046-2.128.654-4.419 1.145-7.037 1.636-2.619.491-5.155.9-7.692 1.227-2.537.328-4.746.655-6.628.901-4.173.572-8.019 1.472-11.292 2.781-3.355 1.31-5.973 3.11-7.855 5.401-1.964 2.291-2.864 5.318-2.864 8.918 0 5.237 1.882 9.164 5.728 11.782 3.682 2.782 8.51 4.091 14.401 4.091Zm64.643 18.328V71.839h27.412v19.965h1.227c2.21-6.955 5.974-12.274 11.292-16.038 5.319-3.763 11.456-5.645 18.329-5.645 1.555 0 3.355.082 5.237.163 1.964.164 3.601.328 4.91.573v25.938c-1.227-.41-3.109-.819-5.646-1.146a58.814 58.814 0 0 0-7.446-.49c-5.155 0-9.738 1.145-13.829 3.354-4.091 2.209-7.282 5.236-9.655 9.164-2.373 3.927-3.519 8.427-3.519 13.5v70.448h-28.312ZM222.077 39.192l-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"
/>
</g>
<defs>
<radialGradient
id="c"
cx="0"
cy="0"
r="1"
gradientTransform="rotate(118.122 171.182 60.81) scale(205.794)"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF41F8" />
<stop offset=".707" stop-color="#FF41F8" stop-opacity=".5" />
<stop offset="1" stop-color="#FF41F8" stop-opacity="0" />
</radialGradient>
<linearGradient
id="b"
x1="0"
x2="982"
y1="192"
y2="192"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#F0060B" />
<stop offset="0" stop-color="#F0070C" />
<stop offset=".526" stop-color="#CC26D5" />
<stop offset="1" stop-color="#7702FF" />
</linearGradient>
<clipPath id="a"><path fill="#fff" d="M0 0h982v239H0z" /></clipPath>
</defs>
</svg>
<h1>Hello, {{ title() }}</h1>
<p>Congratulations! Your app is running. 🎉</p>
</div>
<div class="divider" role="separator" aria-label="Divider"></div>
<div class="right-side">
<div class="pill-group">
@for (item of [
{ title: 'Explore the Docs', link: 'https://angular.dev' },
{ title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
{ title: 'Prompt and best practices for AI', link: 'https://angular.dev/ai/develop-with-ai'},
{ title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
{ title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
{ title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
]; track item.title) {
<a
class="pill"
[href]="item.link"
target="_blank"
rel="noopener"
>
<span>{{ item.title }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
height="14"
viewBox="0 -960 960 960"
width="14"
fill="currentColor"
>
<path
d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z"
/>
</svg>
</a>
}
</div>
<div class="social-links">
<a
href="https://github.com/angular/angular"
aria-label="Github"
target="_blank"
rel="noopener"
>
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Github"
>
<path
d="M12.3047 0C5.50634 0 0 5.50942 0 12.3047C0 17.7423 3.52529 22.3535 8.41332 23.9787C9.02856 24.0946 9.25414 23.7142 9.25414 23.3871C9.25414 23.0949 9.24389 22.3207 9.23876 21.2953C5.81601 22.0377 5.09414 19.6444 5.09414 19.6444C4.53427 18.2243 3.72524 17.8449 3.72524 17.8449C2.61064 17.082 3.81137 17.0973 3.81137 17.0973C5.04697 17.1835 5.69604 18.3647 5.69604 18.3647C6.79321 20.2463 8.57636 19.7029 9.27978 19.3881C9.39052 18.5924 9.70736 18.0499 10.0591 17.7423C7.32641 17.4347 4.45429 16.3765 4.45429 11.6618C4.45429 10.3185 4.9311 9.22133 5.72065 8.36C5.58222 8.04931 5.16694 6.79833 5.82831 5.10337C5.82831 5.10337 6.85883 4.77319 9.2121 6.36459C10.1965 6.09082 11.2424 5.95546 12.2883 5.94931C13.3342 5.95546 14.3801 6.09082 15.3644 6.36459C17.7023 4.77319 18.7328 5.10337 18.7328 5.10337C19.3942 6.79833 18.9789 8.04931 18.8559 8.36C19.6403 9.22133 20.1171 10.3185 20.1171 11.6618C20.1171 16.3888 17.2409 17.4296 14.5031 17.7321C14.9338 18.1012 15.3337 18.8559 15.3337 20.0084C15.3337 21.6552 15.3183 22.978 15.3183 23.3779C15.3183 23.7009 15.5336 24.0854 16.1642 23.9623C21.0871 22.3484 24.6094 17.7341 24.6094 12.3047C24.6094 5.50942 19.0999 0 12.3047 0Z"
/>
</svg>
</a>
<a
href="https://twitter.com/angular"
aria-label="Twitter"
target="_blank"
rel="noopener"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Twitter"
>
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
/>
</svg>
</a>
<a
href="https://www.youtube.com/channel/UCbn1OgGei-DV7aSRo_HaAiw"
aria-label="Youtube"
target="_blank"
rel="noopener"
>
<svg
width="29"
height="20"
viewBox="0 0 29 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
alt="Youtube"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M27.4896 1.52422C27.9301 1.96749 28.2463 2.51866 28.4068 3.12258C29.0004 5.35161 29.0004 10 29.0004 10C29.0004 10 29.0004 14.6484 28.4068 16.8774C28.2463 17.4813 27.9301 18.0325 27.4896 18.4758C27.0492 18.9191 26.5 19.2389 25.8972 19.4032C23.6778 20 14.8068 20 14.8068 20C14.8068 20 5.93586 20 3.71651 19.4032C3.11363 19.2389 2.56449 18.9191 2.12405 18.4758C1.68361 18.0325 1.36732 17.4813 1.20683 16.8774C0.613281 14.6484 0.613281 10 0.613281 10C0.613281 10 0.613281 5.35161 1.20683 3.12258C1.36732 2.51866 1.68361 1.96749 2.12405 1.52422C2.56449 1.08095 3.11363 0.76113 3.71651 0.596774C5.93586 0 14.8068 0 14.8068 0C14.8068 0 23.6778 0 25.8972 0.596774C26.5 0.76113 27.0492 1.08095 27.4896 1.52422ZM19.3229 10L11.9036 5.77905V14.221L19.3229 10Z"
/>
</svg>
</a>
</div>
</div>
</div>
</main>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<router-outlet />

View File

@@ -0,0 +1,15 @@
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 }
];

View File

@@ -0,0 +1,23 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend');
});
});

52
frontend/src/app/app.ts Normal file
View File

@@ -0,0 +1,52 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { ArtifactService } from './services/artifact';
import { NavSidebarComponent } from './components/nav-sidebar/nav-sidebar';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, NavSidebarComponent],
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="header-info">
<span class="badge">{{ deploymentMode }}</span>
<span class="badge">{{ storageBackend }}</span>
</div>
</header>
<div class="content-area">
<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) {}
ngOnInit() {
this.artifactService.getApiInfo().subscribe({
next: (info) => {
this.deploymentMode = `Mode: ${info.deployment_mode}`;
this.storageBackend = `Storage: ${info.storage_backend}`;
},
error: (err) => console.error('Failed to load API info:', err)
});
}
onSidebarToggle(isCollapsed: boolean) {
this.isSidebarCollapsed = isCollapsed;
}
}

View File

@@ -0,0 +1,190 @@
<div class="artifacts-container">
<div class="toolbar">
<button (click)="loadArtifacts()" class="btn btn-primary">
<span class="material-icons md-16">refresh</span> Refresh
</button>
<button (click)="toggleAutoRefresh()"
[class.btn-success]="autoRefreshEnabled"
[class.btn-secondary]="!autoRefreshEnabled"
class="btn">
Auto-refresh: {{ autoRefreshEnabled ? 'ON' : 'OFF' }}
</button>
<button (click)="generateSeedData()" class="btn btn-secondary">
<span class="material-icons md-16">auto_awesome</span> Generate Seed Data
</button>
<span class="count-badge">{{ filteredArtifacts.length }} artifacts</span>
<div class="filter-inline">
<span class="material-icons md-16 search-icon">search</span>
<input
type="text"
[(ngModel)]="searchTerm"
(input)="onSearch()"
placeholder="Search..."
class="search-input">
<button (click)="clearSearch()" class="btn-clear" *ngIf="searchTerm">
<span class="material-icons md-16">close</span>
</button>
</div>
</div>
<div class="error-message" *ngIf="error">{{ error }}</div>
<div class="table-container">
<table class="artifacts-table">
<thead>
<tr>
<th class="sortable" (click)="sortTable('sim_source_id')">
Sim Source
<span class="sort-indicator" *ngIf="sortColumn === 'sim_source_id'">
{{ sortDirection === 'asc' ? '↑' : '↓' }}
</span>
</th>
<th class="sortable" (click)="sortTable('filename')">
Artifacts
<span class="sort-indicator" *ngIf="sortColumn === 'filename'">
{{ sortDirection === 'asc' ? '↑' : '↓' }}
</span>
</th>
<th class="sortable" (click)="sortTable('created_at')">
Date
<span class="sort-indicator" *ngIf="sortColumn === 'created_at'">
{{ sortDirection === 'asc' ? '↑' : '↓' }}
</span>
</th>
<th class="sortable" (click)="sortTable('test_name')">
Uploaded By
<span class="sort-indicator" *ngIf="sortColumn === 'test_name'">
{{ sortDirection === 'asc' ? '↑' : '↓' }}
</span>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngIf="loading">
<td colspan="5" class="loading">Loading artifacts...</td>
</tr>
<tr *ngIf="!loading && filteredArtifacts.length === 0">
<td colspan="5" class="loading">No artifacts found. Upload some files to get started!</td>
</tr>
<tr *ngFor="let artifact of filteredArtifacts" (click)="showDetail(artifact)" class="clickable">
<td>{{ artifact.sim_source_id || artifact.test_suite || '-' }}</td>
<td>
<a href="javascript:void(0)" class="artifact-link">{{ artifact.filename }}</a>
<div class="tags" *ngIf="artifact.tags && artifact.tags.length > 0">
<span class="tag" *ngFor="let tag of artifact.tags">{{ tag }}</span>
</div>
</td>
<td>{{ formatDate(artifact.created_at) }}</td>
<td>{{ artifact.test_name || '-' }}</td>
<td>
<div class="action-buttons">
<button (click)="downloadArtifact(artifact, $event)" class="icon-btn" title="Download">
<span class="material-icons md-16">download</span>
</button>
<button (click)="deleteArtifact(artifact, $event)" class="icon-btn danger" title="Delete">
<span class="material-icons md-16">delete</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<button (click)="previousPage()" [disabled]="currentPage === 1" class="btn">← Previous</button>
<span class="page-info">Page {{ currentPage }}</span>
<button (click)="nextPage()" [disabled]="filteredArtifacts.length < pageSize" class="btn">Next →</button>
</div>
</div>
<!-- Detail Modal -->
<div class="modal" *ngIf="selectedArtifact" (click)="closeDetail()">
<div class="modal-content" (click)="$event.stopPropagation()">
<span class="close" (click)="closeDetail()">&times;</span>
<h2>Artifact Details</h2>
<div class="detail-content">
<div class="detail-row">
<div class="detail-label">ID</div>
<div class="detail-value">{{ selectedArtifact.id }}</div>
</div>
<div class="detail-row">
<div class="detail-label">Filename</div>
<div class="detail-value">{{ selectedArtifact.filename }}</div>
</div>
<div class="detail-row">
<div class="detail-label">File Type</div>
<div class="detail-value"><span class="file-type-badge">{{ selectedArtifact.file_type }}</span></div>
</div>
<div class="detail-row">
<div class="detail-label">Size</div>
<div class="detail-value">{{ formatBytes(selectedArtifact.file_size) }}</div>
</div>
<div class="detail-row">
<div class="detail-label">Storage Path</div>
<div class="detail-value"><code>{{ selectedArtifact.storage_path }}</code></div>
</div>
<div class="detail-row">
<div class="detail-label">Uploaded By</div>
<div class="detail-value">{{ selectedArtifact.test_name || '-' }}</div>
</div>
<div class="detail-row">
<div class="detail-label">Sim Source</div>
<div class="detail-value">{{ selectedArtifact.test_suite || '-' }}</div>
</div>
<div class="detail-row" *ngIf="selectedArtifact.sim_source_id">
<div class="detail-label">SIM Source ID</div>
<div class="detail-value">{{ selectedArtifact.sim_source_id }}</div>
</div>
<div class="detail-row" *ngIf="selectedArtifact.test_result">
<div class="detail-label">Test Result</div>
<div class="detail-value">
<span class="result-badge result-{{ selectedArtifact.test_result }}">
{{ selectedArtifact.test_result }}
</span>
</div>
</div>
<div class="detail-row" *ngIf="selectedArtifact.test_config">
<div class="detail-label">Test Config</div>
<div class="detail-value"><pre>{{ selectedArtifact.test_config | json }}</pre></div>
</div>
<div class="detail-row" *ngIf="selectedArtifact.custom_metadata">
<div class="detail-label">Custom Metadata</div>
<div class="detail-value"><pre>{{ selectedArtifact.custom_metadata | json }}</pre></div>
</div>
<div class="detail-row" *ngIf="selectedArtifact.description">
<div class="detail-label">Description</div>
<div class="detail-value">{{ selectedArtifact.description }}</div>
</div>
<div class="detail-row" *ngIf="selectedArtifact.tags && selectedArtifact.tags.length > 0">
<div class="detail-label">Tags</div>
<div class="detail-value">
<span class="tag" *ngFor="let tag of selectedArtifact.tags">{{ tag }}</span>
</div>
</div>
<div class="detail-row">
<div class="detail-label">Version</div>
<div class="detail-value">{{ selectedArtifact.version || '-' }}</div>
</div>
<div class="detail-row">
<div class="detail-label">Created</div>
<div class="detail-value">{{ formatDate(selectedArtifact.created_at) }}</div>
</div>
<div class="detail-row">
<div class="detail-label">Updated</div>
<div class="detail-value">{{ formatDate(selectedArtifact.updated_at) }}</div>
</div>
<div class="modal-actions">
<button (click)="downloadArtifact(selectedArtifact, $event)" class="btn btn-primary">
<span class="material-icons md-16">download</span> Download
</button>
<button (click)="deleteArtifact(selectedArtifact, $event); closeDetail()" class="btn btn-danger">
<span class="material-icons md-16">delete</span> Delete
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ArtifactsList } from './artifacts-list';
describe('ArtifactsList', () => {
let component: ArtifactsList;
let fixture: ComponentFixture<ArtifactsList>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ArtifactsList]
})
.compileComponents();
fixture = TestBed.createComponent(ArtifactsList);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,235 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ArtifactService } from '../../services/artifact';
import { Artifact } from '../../models/artifact.model';
import { interval, Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
@Component({
selector: 'app-artifacts-list',
standalone: true,
imports: [CommonModule, FormsModule],
templateUrl: './artifacts-list.html',
styleUrls: ['./artifacts-list.css']
})
export class ArtifactsListComponent implements OnInit, OnDestroy {
artifacts: Artifact[] = [];
filteredArtifacts: Artifact[] = [];
selectedArtifact: Artifact | null = null;
searchTerm: string = '';
// Pagination
currentPage: number = 1;
pageSize: number = 25;
// Auto-refresh
autoRefreshEnabled: boolean = true;
private refreshSubscription?: Subscription;
private readonly REFRESH_INTERVAL = 5000; // 5 seconds
// Sorting
sortColumn: string | null = null;
sortDirection: 'asc' | 'desc' = 'asc';
loading: boolean = false;
error: string | null = null;
constructor(private artifactService: ArtifactService) {}
ngOnInit() {
this.loadArtifacts();
this.startAutoRefresh();
}
ngOnDestroy() {
this.stopAutoRefresh();
}
loadArtifacts() {
this.loading = true;
this.error = null;
const offset = (this.currentPage - 1) * this.pageSize;
this.artifactService.listArtifacts(this.pageSize, offset).subscribe({
next: (artifacts) => {
this.artifacts = artifacts;
this.applyFilter();
this.loading = false;
},
error: (err) => {
this.error = 'Failed to load artifacts: ' + err.message;
this.loading = false;
}
});
}
applyFilter() {
if (!this.searchTerm) {
this.filteredArtifacts = [...this.artifacts];
} else {
const term = this.searchTerm.toLowerCase();
this.filteredArtifacts = this.artifacts.filter(artifact =>
artifact.filename.toLowerCase().includes(term) ||
(artifact.test_name && artifact.test_name.toLowerCase().includes(term)) ||
(artifact.test_suite && artifact.test_suite.toLowerCase().includes(term)) ||
(artifact.sim_source_id && artifact.sim_source_id.toLowerCase().includes(term))
);
}
if (this.sortColumn) {
this.applySorting();
}
}
onSearch() {
this.applyFilter();
}
clearSearch() {
this.searchTerm = '';
this.applyFilter();
}
sortTable(column: string) {
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
this.applySorting();
}
applySorting() {
if (!this.sortColumn) return;
this.filteredArtifacts.sort((a, b) => {
const aVal = (a as any)[this.sortColumn!] || '';
const bVal = (b as any)[this.sortColumn!] || '';
let comparison = 0;
if (this.sortColumn === 'created_at') {
comparison = new Date(aVal).getTime() - new Date(bVal).getTime();
} else {
comparison = String(aVal).localeCompare(String(bVal));
}
return this.sortDirection === 'asc' ? comparison : -comparison;
});
}
previousPage() {
if (this.currentPage > 1) {
this.currentPage--;
this.loadArtifacts();
}
}
nextPage() {
this.currentPage++;
this.loadArtifacts();
}
toggleAutoRefresh() {
this.autoRefreshEnabled = !this.autoRefreshEnabled;
if (this.autoRefreshEnabled) {
this.startAutoRefresh();
} else {
this.stopAutoRefresh();
}
}
private startAutoRefresh() {
if (!this.autoRefreshEnabled) return;
this.refreshSubscription = interval(this.REFRESH_INTERVAL)
.pipe(switchMap(() => this.artifactService.listArtifacts(this.pageSize, (this.currentPage - 1) * this.pageSize)))
.subscribe({
next: (artifacts) => {
this.artifacts = artifacts;
this.applyFilter();
},
error: (err) => console.error('Auto-refresh error:', err)
});
}
private stopAutoRefresh() {
if (this.refreshSubscription) {
this.refreshSubscription.unsubscribe();
}
}
showDetail(artifact: Artifact) {
this.selectedArtifact = artifact;
}
closeDetail() {
this.selectedArtifact = null;
}
downloadArtifact(artifact: Artifact, event: Event) {
event.stopPropagation();
this.artifactService.downloadArtifact(artifact.id).subscribe({
next: (blob) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = artifact.filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
},
error: (err) => alert('Download failed: ' + err.message)
});
}
deleteArtifact(artifact: Artifact, event: Event) {
event.stopPropagation();
if (!confirm(`Are you sure you want to delete ${artifact.filename}? This cannot be undone.`)) {
return;
}
this.artifactService.deleteArtifact(artifact.id).subscribe({
next: () => {
this.loadArtifacts();
if (this.selectedArtifact?.id === artifact.id) {
this.closeDetail();
}
},
error: (err) => alert('Delete failed: ' + err.message)
});
}
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;
}
this.artifactService.generateSeedData(num).subscribe({
next: (result) => {
alert(result.message);
this.loadArtifacts();
},
error: (err) => alert('Generation failed: ' + err.message)
});
}
formatBytes(bytes: number): string {
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];
}
formatDate(dateString: string): string {
return new Date(dateString).toLocaleString();
}
}

View File

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

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

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

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

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

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

@@ -0,0 +1,102 @@
<div class="query-section">
<h2>Query Artifacts</h2>
<form [formGroup]="queryForm" (ngSubmit)="onSubmit()">
<div class="form-row">
<div class="form-group">
<label for="q-filename">Filename</label>
<input
type="text"
id="q-filename"
formControlName="filename"
placeholder="Search filename...">
</div>
<div class="form-group">
<label for="q-type">File Type</label>
<select id="q-type" formControlName="file_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"
formControlName="test_name"
placeholder="Search test name...">
</div>
<div class="form-group">
<label for="q-suite">Test Suite</label>
<input
type="text"
id="q-suite"
formControlName="test_suite"
placeholder="e.g., integration">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="q-sim-source-id">SIM Source ID</label>
<input
type="text"
id="q-sim-source-id"
formControlName="sim_source_id"
placeholder="e.g., sim_run_abc123">
</div>
<div class="form-group">
<label for="q-result">Test Result</label>
<select id="q-result" formControlName="test_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>
<div class="form-row">
<div class="form-group">
<label for="q-tags">Tags (comma-separated)</label>
<input
type="text"
id="q-tags"
formControlName="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"
formControlName="start_date">
</div>
<div class="form-group">
<label for="q-end-date">End Date</label>
<input
type="datetime-local"
id="q-end-date"
formControlName="end_date">
</div>
</div>
<div class="button-group">
<button type="submit" class="btn btn-primary btn-large">
<span class="material-icons md-18">search</span> Search
</button>
<button type="button" (click)="clearForm()" class="btn btn-secondary">
<span class="material-icons md-18">close</span> Clear
</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QueryForm } from './query-form';
describe('QueryForm', () => {
let component: QueryForm;
let fixture: ComponentFixture<QueryForm>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [QueryForm]
})
.compileComponents();
fixture = TestBed.createComponent(QueryForm);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,83 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
import { ArtifactService } from '../../services/artifact';
import { Artifact, ArtifactQuery } from '../../models/artifact.model';
@Component({
selector: 'app-query-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './query-form.html',
styleUrls: ['./query-form.css']
})
export class QueryFormComponent {
queryForm: FormGroup;
@Output() resultsFound = new EventEmitter<Artifact[]>();
constructor(
private fb: FormBuilder,
private artifactService: ArtifactService
) {
this.queryForm = this.fb.group({
filename: [''],
file_type: [''],
test_name: [''],
test_suite: [''],
test_result: [''],
sim_source_id: [''],
tags: [''],
start_date: [''],
end_date: ['']
});
}
onSubmit() {
const query: ArtifactQuery = {
limit: 100,
offset: 0
};
if (this.queryForm.value.filename) {
query.filename = this.queryForm.value.filename;
}
if (this.queryForm.value.file_type) {
query.file_type = this.queryForm.value.file_type;
}
if (this.queryForm.value.test_name) {
query.test_name = this.queryForm.value.test_name;
}
if (this.queryForm.value.test_suite) {
query.test_suite = this.queryForm.value.test_suite;
}
if (this.queryForm.value.test_result) {
query.test_result = this.queryForm.value.test_result;
}
if (this.queryForm.value.sim_source_id) {
query.sim_source_id = this.queryForm.value.sim_source_id;
}
if (this.queryForm.value.tags) {
query.tags = this.queryForm.value.tags
.split(',')
.map((t: string) => t.trim())
.filter((t: string) => t);
}
if (this.queryForm.value.start_date) {
query.start_date = new Date(this.queryForm.value.start_date).toISOString();
}
if (this.queryForm.value.end_date) {
query.end_date = new Date(this.queryForm.value.end_date).toISOString();
}
this.artifactService.queryArtifacts(query).subscribe({
next: (artifacts) => {
this.resultsFound.emit(artifacts);
},
error: (err) => alert('Query failed: ' + err.message)
});
}
clearForm() {
this.queryForm.reset();
}
}

View File

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

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

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

View File

@@ -0,0 +1,109 @@
<div class="upload-section">
<h2>Upload Artifact</h2>
<form [formGroup]="uploadForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="file">File *</label>
<input type="file" id="file" (change)="onFileSelected($event)" 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"
formControlName="sim_source"
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"
formControlName="uploaded_by"
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"
formControlName="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"
formControlName="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" formControlName="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"
formControlName="version"
placeholder="e.g., v1.0.0">
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
id="description"
formControlName="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"
formControlName="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"
formControlName="custom_metadata"
rows="4"
placeholder='{"build": "1234", "commit": "abc123"}'></textarea>
</div>
<button type="submit" class="btn btn-primary btn-large" [disabled]="uploading">
<span class="material-icons md-18">upload</span>
{{ uploading ? 'Uploading...' : 'Upload Artifact' }}
</button>
</form>
<div *ngIf="uploadStatus" class="upload-status" [class.success]="uploadStatus.success" [class.error]="!uploadStatus.success">
{{ uploadStatus.message }}
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UploadForm } from './upload-form';
describe('UploadForm', () => {
let component: UploadForm;
let fixture: ComponentFixture<UploadForm>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UploadForm]
})
.compileComponents();
fixture = TestBed.createComponent(UploadForm);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,132 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ArtifactService } from '../../services/artifact';
@Component({
selector: 'app-upload-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './upload-form.html',
styleUrls: ['./upload-form.css']
})
export class UploadFormComponent {
uploadForm: FormGroup;
selectedFile: File | null = null;
uploading: boolean = false;
uploadStatus: { message: string, success: boolean } | null = null;
constructor(
private fb: FormBuilder,
private artifactService: ArtifactService
) {
this.uploadForm = this.fb.group({
file: [null, Validators.required],
sim_source: ['', Validators.required],
uploaded_by: ['', Validators.required],
sim_source_id: [''],
tags: ['', Validators.required],
test_result: [''],
version: [''],
description: [''],
test_config: [''],
custom_metadata: ['']
});
}
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.selectedFile = input.files[0];
this.uploadForm.patchValue({ file: this.selectedFile });
}
}
onSubmit() {
if (!this.uploadForm.valid || !this.selectedFile) {
this.showStatus('Please fill in all required fields and select a file', false);
return;
}
// Validate JSON fields
const testConfig = this.uploadForm.value.test_config;
const customMetadata = this.uploadForm.value.custom_metadata;
if (testConfig) {
try {
JSON.parse(testConfig);
} catch (e) {
this.showStatus('Invalid Test Config JSON', false);
return;
}
}
if (customMetadata) {
try {
JSON.parse(customMetadata);
} catch (e) {
this.showStatus('Invalid Custom Metadata JSON', false);
return;
}
}
const formData = new FormData();
formData.append('file', this.selectedFile);
formData.append('test_suite', this.uploadForm.value.sim_source);
formData.append('test_name', this.uploadForm.value.uploaded_by);
if (this.uploadForm.value.sim_source_id) {
formData.append('sim_source_id', this.uploadForm.value.sim_source_id);
}
// Parse and append tags as JSON array
if (this.uploadForm.value.tags) {
const tagsArray = this.uploadForm.value.tags
.split(',')
.map((t: string) => t.trim())
.filter((t: string) => t);
formData.append('tags', JSON.stringify(tagsArray));
}
if (this.uploadForm.value.test_result) {
formData.append('test_result', this.uploadForm.value.test_result);
}
if (this.uploadForm.value.version) {
formData.append('version', this.uploadForm.value.version);
}
if (this.uploadForm.value.description) {
formData.append('description', this.uploadForm.value.description);
}
if (testConfig) {
formData.append('test_config', testConfig);
}
if (customMetadata) {
formData.append('custom_metadata', customMetadata);
}
this.uploading = true;
this.artifactService.uploadArtifact(formData).subscribe({
next: (artifact) => {
this.showStatus(`Successfully uploaded: ${artifact.filename}`, true);
this.uploadForm.reset();
this.selectedFile = null;
this.uploading = false;
},
error: (err) => {
this.showStatus('Upload failed: ' + err.error?.detail || err.message, false);
this.uploading = false;
}
});
}
private showStatus(message: string, success: boolean) {
this.uploadStatus = { message, success };
setTimeout(() => {
this.uploadStatus = null;
}, 5000);
}
}

View File

@@ -0,0 +1,40 @@
export interface Artifact {
id: number;
filename: string;
file_type: string;
file_size: number;
storage_path: string;
content_type: string | null;
test_name: string | null;
test_suite: string | null;
test_config: any;
test_result: string | null;
sim_source_id: string | null;
custom_metadata: any;
description: string | null;
tags: string[] | null;
created_at: string;
updated_at: string;
version: string | null;
parent_id: number | null;
}
export interface ArtifactQuery {
filename?: string;
file_type?: string;
test_name?: string;
test_suite?: string;
test_result?: string;
sim_source_id?: string;
tags?: string[];
start_date?: string;
end_date?: string;
limit?: number;
offset?: number;
}
export interface ApiInfo {
deployment_mode: string;
storage_backend: string;
version: string;
}

View File

@@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Artifact, ArtifactQuery, ApiInfo } from '../models/artifact.model';
@Injectable({
providedIn: 'root'
})
export class ArtifactService {
private apiUrl = '/api/v1/artifacts';
private baseUrl = '/api';
constructor(private http: HttpClient) {}
getApiInfo(): Observable<ApiInfo> {
return this.http.get<ApiInfo>(this.baseUrl);
}
listArtifacts(limit: number = 100, offset: number = 0): Observable<Artifact[]> {
const params = new HttpParams()
.set('limit', limit.toString())
.set('offset', offset.toString());
return this.http.get<Artifact[]>(this.apiUrl + '/', { params });
}
getArtifact(id: number): Observable<Artifact> {
return this.http.get<Artifact>(`${this.apiUrl}/${id}`);
}
queryArtifacts(query: ArtifactQuery): Observable<Artifact[]> {
return this.http.post<Artifact[]>(`${this.apiUrl}/query`, query);
}
uploadArtifact(formData: FormData): Observable<Artifact> {
return this.http.post<Artifact>(`${this.apiUrl}/upload`, formData);
}
downloadArtifact(id: number): Observable<Blob> {
return this.http.get(`${this.apiUrl}/${id}/download`, {
responseType: 'blob'
});
}
deleteArtifact(id: number): Observable<any> {
return this.http.delete(`${this.apiUrl}/${id}`);
}
generateSeedData(count: number): Observable<any> {
return this.http.post(`/api/v1/seed/generate/${count}`, {});
}
}

14
frontend/src/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
</html>

6
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View File

@@ -33,6 +33,21 @@ header {
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 {
@@ -524,6 +539,49 @@ code {
stroke: currentColor;
}
/* Artifact link styles - softer blue */
.artifact-link {
color: #93c5fd;
text-decoration: none;
transition: color 0.3s;
}
.artifact-link:hover {
color: #bfdbfe;
text-decoration: underline;
}
/* Clickable row cursor */
tr.clickable {
cursor: pointer;
}
/* Search icon color */
.search-icon {
color: #64748b;
}
/* Material Icons */
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 20px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
vertical-align: middle;
}
.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; }
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;

View File

@@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}

34
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"target": "ES2022",
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,14 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.ts"
]
}

View File

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

42
helm/README.md Normal file
View File

@@ -0,0 +1,42 @@
# 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,111 +0,0 @@
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

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

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

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

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

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

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

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

441
helm/warehouse13/README.md Normal file
View File

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

@@ -0,0 +1,131 @@
_ _ _ _ _____
| | | | | | / |___ /
| | | | __ _ _ __ ___| |__ ___ _ _ ___ / / |_ \
| |/\| |/ _` | '__/ _ \ '_ \ / _ \| | | / __|/ / ___) |
\ /\ / (_| | | | __/ | | | (_) | |_| \__ \_/ |____/
\/ \/ \__,_|_| \___|_| |_|\___/ \__,_|___(_)
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,14 +1,14 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "datalake.name" -}}
{{- define "warehouse13.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "datalake.fullname" -}}
{{- define "warehouse13.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 "datalake.chart" -}}
{{- define "warehouse13.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "datalake.labels" -}}
helm.sh/chart: {{ include "datalake.chart" . }}
{{ include "datalake.selectorLabels" . }}
{{- define "warehouse13.labels" -}}
helm.sh/chart: {{ include "warehouse13.chart" . }}
{{ include "warehouse13.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
@@ -43,18 +43,29 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
{{/*
Selector labels
*/}}
{{- define "datalake.selectorLabels" -}}
app.kubernetes.io/name: {{ include "datalake.name" . }}
{{- define "warehouse13.selectorLabels" -}}
app.kubernetes.io/name: {{ include "warehouse13.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "datalake.serviceAccountName" -}}
{{- define "warehouse13.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "datalake.fullname" .) .Values.serviceAccount.name }}
{{- default (include "warehouse13.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,99 @@
{{- 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

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

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

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

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

View File

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

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

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

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

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

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

View File

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

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

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

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

74
quickstart-airgap.sh Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
set -e
echo "========================================="
echo "Warehouse13 - Air-Gapped Quick Start"
echo "========================================="
echo ""
echo "This script is for restricted/air-gapped environments"
echo "where npm packages cannot be downloaded during Docker build."
echo ""
# Check if we're in the right directory
if [ ! -f "docker-compose.yml" ]; then
echo "Error: Must run from project root directory"
exit 1
fi
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo "Error: Docker is not installed. Please install Docker first."
exit 1
fi
# Check if Docker Compose is installed
if ! command -v docker-compose &> /dev/null; then
echo "Error: Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
echo "Step 1: Building Angular frontend locally..."
echo "==========================================="
./scripts/build-for-airgap.sh
echo ""
echo "Step 2: Starting Docker containers..."
echo "==========================================="
docker-compose up -d --build
echo ""
echo "Step 3: Waiting for services to be ready..."
sleep 15
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: docker-compose logs -f"
echo "To stop: docker-compose down"
echo ""
echo "========================================="
echo "Testing the API..."
echo "========================================="
# Wait a bit more for API to be fully ready
sleep 5
# Test health endpoint
if curl -s http://localhost:8000/health | grep -q "healthy"; then
echo "✓ API is healthy!"
echo ""
echo "========================================="
echo "Setup complete! 🚀"
echo "========================================="
else
echo "⚠ API is not responding yet. Please wait a moment and check http://localhost:8000/health"
fi

View File

@@ -1,106 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo =========================================
echo Obsidian - 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

@@ -1,7 +1,7 @@
# Test Artifact Data Lake - Quick Start (PowerShell)
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host "Obsidian - Quick Start" -ForegroundColor Cyan
Write-Host "Warehouse13 - Quick Start" -ForegroundColor Cyan
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host ""
@@ -121,7 +121,7 @@ Write-Host "Useful Commands:" -ForegroundColor Cyan
Write-Host " Generate seed data: " -NoNewline
Write-Host "Use the 'Generate Seed Data' button in the UI" -ForegroundColor Yellow
Write-Host " View logs: " -NoNewline
Write-Host "$composeCmd logs -f api" -ForegroundColor Yellow
Write-Host "$composeCmd logs -f app" -ForegroundColor Yellow
Write-Host " Restart services: " -NoNewline
Write-Host "$composeCmd restart" -ForegroundColor Yellow
Write-Host " Stop all: " -NoNewline

View File

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

View File

@@ -0,0 +1,70 @@
#!/bin/bash
set -e
echo "========================================="
echo "Building Angular for Air-Gapped Deployment"
echo "========================================="
echo ""
# Check if we're in the right directory
if [ ! -f "docker-compose.yml" ]; then
echo "Error: Must run from project root directory"
exit 1
fi
# Check if node is installed
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed. Please install Node.js 24+ first."
exit 1
fi
# Check if npm is installed
if ! command -v npm &> /dev/null; then
echo "Error: npm is not installed. Please install npm first."
exit 1
fi
echo "Step 1/3: Installing dependencies..."
cd frontend
npm install
echo ""
echo "Step 2/3: Building Angular production bundle..."
npm run build:prod
echo ""
echo "Step 3/3: Copying to static directory..."
if [ -d "dist/frontend/browser" ]; then
echo "✓ Build successful!"
echo "✓ Output: frontend/dist/frontend/browser"
# Copy to static directory for local FastAPI serving
cd ..
rm -rf static/*
cp -r frontend/dist/frontend/browser/* static/
echo "✓ Copied to static/ directory"
ls -lh static/ | head -10
else
echo "✗ Build failed - output directory not found"
exit 1
fi
echo ""
echo "========================================="
echo "Build Complete!"
echo "========================================="
echo ""
echo "The Angular app has been built and copied to static/"
echo "You can now:"
echo ""
echo "1. Run locally with FastAPI:"
echo " uvicorn app.main:app --reload"
echo " Access at: http://localhost:8000"
echo ""
echo "2. Deploy with Docker:"
echo " docker-compose up -d --build"
echo " (Docker will rebuild Angular during build)"
echo ""
echo "========================================="

146
scripts/check-package-age.js Executable file
View File

@@ -0,0 +1,146 @@
#!/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();

62
scripts/check-ready.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
echo "========================================="
echo "Warehouse13 - Readiness Check"
echo "========================================="
echo ""
errors=0
# Check if pre-built files exist
if [ -d "frontend/dist/frontend/browser" ]; then
echo "✓ Pre-built Angular files found"
echo " Location: frontend/dist/frontend/browser"
file_count=$(ls -1 frontend/dist/frontend/browser | wc -l)
echo " Files: $file_count"
else
echo "✗ ERROR: Pre-built Angular files NOT found!"
echo " Expected: frontend/dist/frontend/browser"
echo ""
echo " You need to build the Angular app first:"
echo " Run: ./scripts/build-for-airgap.sh"
echo " OR: cd frontend && npm install && npm run build:prod"
echo ""
errors=$((errors + 1))
fi
echo ""
# Check if docker-compose.yml is configured for pre-built
if grep -q "Dockerfile.frontend.prebuilt" docker-compose.yml; then
echo "✓ docker-compose.yml configured for pre-built deployment"
else
echo "⚠ WARNING: docker-compose.yml may not be configured for pre-built deployment"
echo " Current frontend dockerfile:"
grep "dockerfile:" docker-compose.yml | grep -A 1 "frontend:" | tail -1
echo ""
echo " For air-gapped deployment, it should be: Dockerfile.frontend.prebuilt"
fi
echo ""
# Check if Docker is running
if docker info &> /dev/null; then
echo "✓ Docker is running"
else
echo "✗ ERROR: Docker is not running or not accessible"
errors=$((errors + 1))
fi
echo ""
echo "========================================="
if [ $errors -eq 0 ]; then
echo "✓ Ready to deploy!"
echo ""
echo "Run: docker-compose up -d --build"
echo "Or: ./quickstart-airgap.sh"
exit 0
else
echo "✗ NOT ready - please fix the errors above"
exit 1
fi

41
scripts/pin-old-versions.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/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 ""

View File

@@ -1,251 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Obsidian - 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>◆ Obsidian</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>

View File

@@ -1,592 +0,0 @@
// API Base URL
const API_BASE = '/api/v1';
// Pagination
let currentPage = 1;
let pageSize = 25;
let totalArtifacts = 0;
// Auto-refresh
let autoRefreshEnabled = true;
let autoRefreshInterval = null;
const REFRESH_INTERVAL_MS = 5000; // 5 seconds
// Sorting and filtering
let allArtifacts = []; // Store all artifacts for client-side sorting/filtering
let currentSortColumn = null;
let currentSortDirection = 'asc';
// Load API info on page load
window.addEventListener('DOMContentLoaded', () => {
loadApiInfo();
loadArtifacts();
startAutoRefresh();
});
// Load API information
async function loadApiInfo() {
try {
const response = await fetch('/api');
const data = await response.json();
document.getElementById('deployment-mode').textContent = `Mode: ${data.deployment_mode}`;
document.getElementById('storage-backend').textContent = `Storage: ${data.storage_backend}`;
} catch (error) {
console.error('Error loading API info:', error);
}
}
// Load artifacts
async function loadArtifacts(limit = pageSize, offset = 0) {
try {
const response = await fetch(`${API_BASE}/artifacts/?limit=${limit}&offset=${offset}`);
const artifacts = await response.json();
allArtifacts = artifacts; // Store for sorting/filtering
displayArtifacts(artifacts);
updatePagination(artifacts.length);
} catch (error) {
console.error('Error loading artifacts:', error);
document.getElementById('artifacts-tbody').innerHTML = `
<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();
}