diff --git a/dashboard-sqdc/.dockerignore b/dashboard-sqdc/.dockerignore new file mode 100644 index 0000000..4a32658 --- /dev/null +++ b/dashboard-sqdc/.dockerignore @@ -0,0 +1,15 @@ +node_modules +build +.git +.gitignore +*.md +.env +.vscode +.idea +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +coverage +.eslintcache diff --git a/dashboard-sqdc/.gitea/workflows/README.md b/dashboard-sqdc/.gitea/workflows/README.md new file mode 100644 index 0000000..42c5a96 --- /dev/null +++ b/dashboard-sqdc/.gitea/workflows/README.md @@ -0,0 +1,217 @@ +# Gitea CI/CD Workflow + +This directory contains the Gitea Actions workflow for building and deploying the SQDC Dashboard. + +## Workflow: build-deploy.yml + +### Triggers + +- **Push** to `main` or `dashboard` branches +- **Pull Request** to `main` or `dashboard` branches + +### Jobs + +#### 1. Build Job + +Runs on every push and pull request. + +**Steps:** +1. Checkout code +2. Set up Node.js 18 +3. Install dependencies (`npm ci`) +4. Run tests +5. Build React application +6. Login to container registry +7. Build Docker image +8. Tag image with commit SHA and `latest` +9. Push to registry: `gitea.vidoks.fr/sortifal/pfee` + +**Artifacts:** +- Docker image pushed to registry with tags: + - `gitea.vidoks.fr/sortifal/pfee:` + - `gitea.vidoks.fr/sortifal/pfee:latest` + +#### 2. Deploy Job + +Runs only on push to `main` or `dashboard` branches (not on PRs). + +**Steps:** +1. Checkout code +2. Set up kubectl +3. Configure kubectl with k3s config +4. Create registry credentials secret +5. Apply Kubernetes manifests (namespace, deployment, service, ingress) +6. Update deployment with new image +7. Wait for rollout to complete +8. Verify deployment status + +**Requirements:** +- Successful build job +- Push to protected branches only + +#### 3. Notify Job + +Runs after build and deploy jobs complete (success or failure). + +**Steps:** +1. Check deployment result +2. Display success or failure message +3. Exit with error code if deployment failed + +## Required Secrets + +Configure these in Gitea repository settings: + +| Secret | Description | +|--------|-------------| +| `KUBE_CONFIG` | Plain text kubeconfig for k3s cluster | +| `REGISTRY_URL` | Container registry URL (gitea.vidoks.fr) | +| `REGISTRY_USER` | Registry username | +| `REGISTRY_PASSWORD` | Registry password or access token | + +## Workflow Behavior + +### On Pull Request + +- Builds and tests the code +- Pushes image to registry +- **Does not deploy** to Kubernetes + +### On Push to main/dashboard + +- Builds and tests the code +- Pushes image to registry +- **Deploys** to Kubernetes cluster +- Updates running deployment with new image +- Verifies deployment success + +## Image Versioning + +Each build creates two image tags: + +1. **Commit SHA tag**: `gitea.vidoks.fr/sortifal/pfee:` + - Immutable, specific version + - Used for rollbacks + +2. **Latest tag**: `gitea.vidoks.fr/sortifal/pfee:latest` + - Points to most recent build + - Used by default in deployment + +## Monitoring + +### View Workflow Runs + +1. Go to repository on Gitea +2. Click "Actions" tab +3. Select workflow run to view logs + +### Check Deployment Status + +```bash +# View all resources +kubectl get all -n sqdc-dashboard + +# View deployment status +kubectl rollout status deployment/sqdc-dashboard -n sqdc-dashboard + +# View pod logs +kubectl logs -f deployment/sqdc-dashboard -n sqdc-dashboard +``` + +## Troubleshooting + +### Build Failures + +**Tests failing:** +```bash +# Run tests locally +npm test +``` + +**Build errors:** +```bash +# Run build locally +npm run build +``` + +### Registry Push Failures + +**Authentication errors:** +- Verify `REGISTRY_USER` and `REGISTRY_PASSWORD` are correct +- Ensure token has `write:package` permission + +**Network errors:** +- Check registry URL is accessible: `gitea.vidoks.fr` + +### Deployment Failures + +**kubectl connection errors:** +- Verify `KUBE_CONFIG` is valid and not base64 encoded +- Test locally: `kubectl get nodes` + +**Image pull errors:** +- Check registry credentials secret exists +- Verify image was pushed successfully + +**Rollout timeout:** +- Increase timeout in workflow (default: 5m) +- Check pod logs for errors + +## Manual Operations + +### Manual Deploy + +```bash +# Using the deploy script +./scripts/deploy.sh gitea.vidoks.fr sortifal/pfee + +# Or manually with kubectl +kubectl apply -f k8s/ +kubectl set image deployment/sqdc-dashboard dashboard=gitea.vidoks.fr/sortifal/pfee:latest -n sqdc-dashboard +``` + +### Rollback + +```bash +# Using the rollback script +./scripts/rollback.sh + +# Or manually +kubectl rollout undo deployment/sqdc-dashboard -n sqdc-dashboard +``` + +### Skip Workflow + +Add `[skip ci]` to commit message: + +```bash +git commit -m "docs: Update README [skip ci]" +``` + +## Customization + +### Change Deployment Conditions + +Edit the `if` condition in deploy job: + +```yaml +if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dashboard' +``` + +### Add Slack/Email Notifications + +Add steps in notify job to send alerts. + +### Add More Tests + +Add test steps in build job: + +```yaml +- name: Run linter + run: npm run lint + +- name: Run integration tests + run: npm run test:integration +``` + +For more details, see [DEPLOYMENT.md](../../DEPLOYMENT.md) and [SETUP-REGISTRY.md](../../SETUP-REGISTRY.md). diff --git a/dashboard-sqdc/.gitea/workflows/build-deploy.yml b/dashboard-sqdc/.gitea/workflows/build-deploy.yml new file mode 100644 index 0000000..0dbb408 --- /dev/null +++ b/dashboard-sqdc/.gitea/workflows/build-deploy.yml @@ -0,0 +1,109 @@ +name: Build and Deploy SQDC Dashboard + +on: + push: + branches: + - main + - dashboard + pull_request: + branches: + - main + - dashboard + +jobs: + build: + name: Build Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test -- --passWithNoTests + + - name: Build application + run: npm run build + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Container Registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u ${{ secrets.REGISTRY_USER }} --password-stdin + + - name: Build and Push Docker image + run: | + docker build -t ${{ secrets.REGISTRY_URL }}/sortifal/pfee:${{ github.sha }} . + docker tag ${{ secrets.REGISTRY_URL }}/sortifal/pfee:${{ github.sha }} ${{ secrets.REGISTRY_URL }}/sortifal/pfee:latest + docker push ${{ secrets.REGISTRY_URL }}/sortifal/pfee:${{ github.sha }} + docker push ${{ secrets.REGISTRY_URL }}/sortifal/pfee:latest + + deploy: + name: Deploy to Kubernetes + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dashboard' + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up kubectl + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + + - name: Configure kubectl + run: | + mkdir -p $HOME/.kube + echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config + + - name: Create registry credentials secret + run: | + kubectl create secret docker-registry registry-credentials \ + --docker-server=${{ secrets.REGISTRY_URL }} \ + --docker-username=${{ secrets.REGISTRY_USER }} \ + --docker-password=${{ secrets.REGISTRY_PASSWORD }} \ + -n sqdc-dashboard \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Deploy to Kubernetes + run: | + kubectl apply -f k8s/namespace.yaml + kubectl apply -f k8s/deployment.yaml + kubectl apply -f k8s/service.yaml + kubectl apply -f k8s/ingress.yaml + + - name: Update deployment with new image + run: | + kubectl set image deployment/sqdc-dashboard dashboard=${{ secrets.REGISTRY_URL }}/sortifal/pfee:${{ github.sha }} -n sqdc-dashboard + kubectl rollout status deployment/sqdc-dashboard -n sqdc-dashboard --timeout=5m + + - name: Verify deployment + run: | + kubectl get pods -n sqdc-dashboard + kubectl get svc -n sqdc-dashboard + kubectl get ingress -n sqdc-dashboard + + notify: + name: Notify Deployment Status + runs-on: ubuntu-latest + needs: [build, deploy] + if: always() + steps: + - name: Deployment Status + run: | + if [ "${{ needs.deploy.result }}" == "success" ]; then + echo "✅ Deployment successful!" + else + echo "❌ Deployment failed!" + exit 1 + fi diff --git a/dashboard-sqdc/DEPLOYMENT.md b/dashboard-sqdc/DEPLOYMENT.md new file mode 100644 index 0000000..35c4af3 --- /dev/null +++ b/dashboard-sqdc/DEPLOYMENT.md @@ -0,0 +1,446 @@ +# SQDC Dashboard - Deployment Guide + +## Table of Contents +- [Quick Start with Registry](#quick-start-with-registry) +- [Docker Deployment](#docker-deployment) +- [Kubernetes Deployment](#kubernetes-deployment) +- [Gitea CI/CD Setup](#gitea-cicd-setup) +- [Configuration](#configuration) +- [Troubleshooting](#troubleshooting) + +--- + +## Quick Start with Registry + +For a step-by-step guide to set up the container registry and CI/CD pipeline, see [SETUP-REGISTRY.md](SETUP-REGISTRY.md). + +**Quick Summary:** +- Registry: `gitea.vidoks.fr` +- Image: `gitea.vidoks.fr/sortifal/pfee:latest` +- Required Secrets: `KUBE_CONFIG`, `REGISTRY_URL`, `REGISTRY_USER`, `REGISTRY_PASSWORD` + +--- + +## Docker Deployment + +### Local Development with Docker Compose + +1. **Build and run the application:** + ```bash + docker-compose up -d + ``` + +2. **Access the application:** + - Frontend: http://localhost:8080 + - API: http://localhost:3001 + +3. **View logs:** + ```bash + docker-compose logs -f + ``` + +4. **Stop the application:** + ```bash + docker-compose down + ``` + +### Manual Docker Build + +1. **Build the image:** + ```bash + docker build -t sqdc-dashboard:latest . + ``` + +2. **Run the container:** + ```bash + docker run -d \ + --name sqdc-dashboard \ + -p 8080:80 \ + -p 3001:3001 \ + -v $(pwd)/database:/app/database \ + sqdc-dashboard:latest + ``` + +3. **Check container status:** + ```bash + docker ps + docker logs sqdc-dashboard + ``` + +--- + +## Kubernetes Deployment + +### Prerequisites + +- Kubernetes cluster (v1.20+) +- kubectl configured +- Nginx Ingress Controller installed +- (Optional) Cert-Manager for HTTPS + +### Quick Deployment + +1. **Create registry credentials secret:** + ```bash + kubectl create secret docker-registry registry-credentials \ + --docker-server=gitea.vidoks.fr \ + --docker-username= \ + --docker-password= \ + -n sqdc-dashboard + ``` + +2. **Using the deployment script:** + ```bash + ./scripts/deploy.sh gitea.vidoks.fr sortifal/pfee + ``` + +3. **Manual deployment:** + ```bash + # Apply all manifests + kubectl apply -f k8s/ + + # Or use kustomize + kubectl apply -k k8s/ + ``` + +3. **Verify deployment:** + ```bash + kubectl get all -n sqdc-dashboard + ``` + +### Detailed Steps + +#### 1. Create Namespace +```bash +kubectl apply -f k8s/namespace.yaml +``` + +#### 2. Deploy Application +```bash +kubectl apply -f k8s/deployment.yaml +``` + +#### 3. Create Service +```bash +kubectl apply -f k8s/service.yaml +``` + +#### 4. Setup Ingress +```bash +kubectl apply -f k8s/ingress.yaml +``` + +#### 5. Verify Deployment +```bash +# Check pods +kubectl get pods -n sqdc-dashboard + +# Check service +kubectl get svc -n sqdc-dashboard + +# Check ingress +kubectl get ingress -n sqdc-dashboard + +# View logs +kubectl logs -f deployment/sqdc-dashboard -n sqdc-dashboard +``` + +### Scaling + +```bash +# Scale to 3 replicas +kubectl scale deployment/sqdc-dashboard --replicas=3 -n sqdc-dashboard + +# Auto-scaling (optional) +kubectl autoscale deployment/sqdc-dashboard \ + --cpu-percent=70 \ + --min=2 \ + --max=10 \ + -n sqdc-dashboard +``` + +### Rollback + +```bash +# Using the rollback script +./scripts/rollback.sh + +# Or manually +kubectl rollout undo deployment/sqdc-dashboard -n sqdc-dashboard +kubectl rollout status deployment/sqdc-dashboard -n sqdc-dashboard +``` + +--- + +## Gitea CI/CD Setup + +### 1. Configure Gitea Actions + +Ensure Gitea Actions is enabled in your Gitea instance: +- Go to Site Administration → Configuration +- Enable Actions Runner + +### 2. Set up Secrets + +In your Gitea repository, add the following secrets: + +**Settings → Secrets → Actions** + +- `KUBE_CONFIG`: Plain text kubeconfig file for k3s + ```bash + cat ~/.kube/config + ``` + +- `REGISTRY_URL`: Container registry URL (e.g., `gitea.vidoks.fr`) + +- `REGISTRY_USER`: Registry username + +- `REGISTRY_PASSWORD`: Registry password or access token + +### 3. Configure Workflow + +The workflow file is already created at `.gitea/workflows/build-deploy.yml` + +**Workflow triggers:** +- Push to `main` or `dashboard` branch +- Pull requests to `main` or `dashboard` branch + +**Workflow steps:** +1. Build - Runs tests, builds Docker image, and pushes to container registry +2. Deploy - Pulls image from registry and deploys to Kubernetes (only on main/dashboard branch) +3. Notify - Sends deployment status + +### 4. First Deployment + +```bash +# Commit and push +git add . +git commit -m "Initial deployment setup" +git push origin main +``` + +The workflow will automatically: +- Install dependencies +- Run tests +- Build the application +- Create Docker image +- Push image to container registry (gitea.vidoks.fr) +- Pull image from registry and deploy to Kubernetes cluster + +### 5. Monitor Workflow + +- Go to your Gitea repository +- Click on "Actions" tab +- View workflow runs and logs + +--- + +## Configuration + +### Environment Variables + +**Docker Compose:** +Edit `docker-compose.yml`: +```yaml +environment: + - NODE_ENV=production + - API_PORT=3001 +``` + +**Kubernetes:** +Edit `k8s/configmap.yaml`: +```yaml +data: + NODE_ENV: "production" + API_PORT: "3001" +``` + +### Ingress Hostname + +Edit `k8s/ingress.yaml` to change the hostname: +```yaml +spec: + rules: + - host: your-domain.com # Change this +``` + +Update `/etc/hosts` for local testing: +```bash +echo "127.0.0.1 sqdc-dashboard.local" | sudo tee -a /etc/hosts +``` + +### Resource Limits + +Edit `k8s/deployment.yaml` to adjust resources: +```yaml +resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" +``` + +### Storage + +Adjust persistent volume size in `k8s/deployment.yaml`: +```yaml +resources: + requests: + storage: 1Gi # Change this +``` + +--- + +## Troubleshooting + +### Container Issues + +**Container won't start:** +```bash +docker logs sqdc-dashboard +docker inspect sqdc-dashboard +``` + +**Port already in use:** +```bash +# Find process using port +lsof -i :8080 +# Kill process +kill -9 +``` + +### Kubernetes Issues + +**Pods not running:** +```bash +kubectl describe pod -n sqdc-dashboard +kubectl logs -n sqdc-dashboard +``` + +**Image pull errors:** +```bash +# Check if image exists +docker images | grep sqdc-dashboard + +# Load image into cluster (minikube) +minikube image load sqdc-dashboard:latest + +# Or use kind +kind load docker-image sqdc-dashboard:latest +``` + +**Service not accessible:** +```bash +# Check service endpoints +kubectl get endpoints -n sqdc-dashboard + +# Port-forward for testing +kubectl port-forward svc/sqdc-dashboard-service 8080:80 -n sqdc-dashboard +``` + +**Ingress not working:** +```bash +# Check ingress controller +kubectl get pods -n ingress-nginx + +# Check ingress resource +kubectl describe ingress sqdc-dashboard-ingress -n sqdc-dashboard + +# Verify nginx ingress controller is installed +kubectl get ingressclass +``` + +### Gitea Workflow Issues + +**Workflow not triggering:** +- Check if Actions is enabled +- Verify branch names match trigger conditions +- Check workflow file syntax + +**Deployment fails:** +- Verify KUBECONFIG secret is correctly set +- Check kubectl has access to cluster +- Review workflow logs in Gitea Actions tab + +**Test failures:** +```bash +# Run tests locally +npm test + +# Skip tests (not recommended) +# Modify workflow to add: -- --passWithNoTests +``` + +--- + +## Monitoring + +### View Application Logs + +**Docker:** +```bash +docker logs -f sqdc-dashboard +``` + +**Kubernetes:** +```bash +kubectl logs -f deployment/sqdc-dashboard -n sqdc-dashboard +``` + +### Check Health Status + +**Docker:** +```bash +curl http://localhost:8080 +curl http://localhost:3001/categories +``` + +**Kubernetes:** +```bash +kubectl get pods -n sqdc-dashboard +kubectl exec -it -n sqdc-dashboard -- wget -O- http://localhost/ +``` + +--- + +## Backup and Restore + +### Backup Database + +**Docker:** +```bash +docker cp sqdc-dashboard:/app/database/sqdc.db ./backup-$(date +%Y%m%d).db +``` + +**Kubernetes:** +```bash +kubectl cp sqdc-dashboard/:/app/database/sqdc.db \ + ./backup-$(date +%Y%m%d).db \ + -n sqdc-dashboard +``` + +### Restore Database + +**Docker:** +```bash +docker cp ./backup.db sqdc-dashboard:/app/database/sqdc.db +docker restart sqdc-dashboard +``` + +**Kubernetes:** +```bash +kubectl cp ./backup.db sqdc-dashboard/:/app/database/sqdc.db \ + -n sqdc-dashboard +kubectl rollout restart deployment/sqdc-dashboard -n sqdc-dashboard +``` + +--- + +## Additional Resources + +- [Kubernetes Documentation](https://kubernetes.io/docs/) +- [Docker Documentation](https://docs.docker.com/) +- [Gitea Actions](https://docs.gitea.io/en-us/actions/) +- [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/) diff --git a/dashboard-sqdc/Dockerfile b/dashboard-sqdc/Dockerfile new file mode 100644 index 0000000..f27db3e --- /dev/null +++ b/dashboard-sqdc/Dockerfile @@ -0,0 +1,46 @@ +# Multi-stage build for optimized production image + +# Stage 1: Build the React application +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Stage 2: Production image with Nginx +FROM nginx:alpine + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Copy built application from builder stage +COPY --from=builder /app/build /usr/share/nginx/html + +# Copy database and server files for API +COPY --from=builder /app/database /app/database +COPY --from=builder /app/server.js /app/server.js +COPY --from=builder /app/node_modules /app/node_modules +COPY --from=builder /app/package.json /app/package.json + +# Install Node.js in the nginx image to run the API server +RUN apk add --no-cache nodejs npm + +# Expose ports +EXPOSE 80 3001 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 + +# Start both nginx and the API server +CMD sh -c "node /app/server.js & nginx -g 'daemon off;'" diff --git a/dashboard-sqdc/SETUP-REGISTRY.md b/dashboard-sqdc/SETUP-REGISTRY.md new file mode 100644 index 0000000..0bf1151 --- /dev/null +++ b/dashboard-sqdc/SETUP-REGISTRY.md @@ -0,0 +1,205 @@ +# Registry and CI/CD Setup Guide + +This guide walks you through setting up the container registry and CI/CD pipeline for the SQDC Dashboard. + +## Registry Information + +- **Registry URL**: `gitea.vidoks.fr` +- **Repository**: `sortifal/pfee` +- **Full Image Path**: `gitea.vidoks.fr/sortifal/pfee:latest` + +## Prerequisites + +1. Gitea account with access to `sortifal/pfee` repository +2. Kubernetes cluster (k3s) with kubectl configured +3. Registry credentials (username and password/token) + +## Step 1: Configure Gitea Secrets + +Go to your Gitea repository: **Settings → Secrets → Actions** + +Add the following secrets: + +### Required Secrets + +| Secret Name | Description | Example Value | +|------------|-------------|---------------| +| `KUBE_CONFIG` | Plain text kubeconfig for k3s | Contents of `~/.kube/config` | +| `REGISTRY_URL` | Container registry URL | `gitea.vidoks.fr` | +| `REGISTRY_USER` | Registry username | Your Gitea username | +| `REGISTRY_PASSWORD` | Registry password or token | Your Gitea password/token | + +### How to get KUBE_CONFIG + +```bash +# Display your kubeconfig +cat ~/.kube/config + +# Copy the entire output and paste it as the KUBE_CONFIG secret +``` + +### How to create a Gitea Token + +1. Go to your Gitea profile → Settings → Applications +2. Create a new token with `write:package` permission +3. Use this token as `REGISTRY_PASSWORD` + +## Step 2: Verify Workflow Configuration + +The workflow file at [.gitea/workflows/build-deploy.yml](.gitea/workflows/build-deploy.yml) is already configured with: + +- Image path: `gitea.vidoks.fr/sortifal/pfee` +- Triggers: Push to `main` or `dashboard` branches +- Build, deploy, and notify jobs + +## Step 3: Test Local Build (Optional) + +Before pushing, you can test the Docker build locally: + +```bash +# Build the image +docker build -t gitea.vidoks.fr/sortifal/pfee:test . + +# Test run locally +docker run -p 8080:80 -p 3001:3001 gitea.vidoks.fr/sortifal/pfee:test +``` + +## Step 4: Manual Registry Push (Optional) + +If you want to manually push to the registry: + +```bash +# Login to registry +docker login gitea.vidoks.fr -u + +# Build and tag +docker build -t gitea.vidoks.fr/sortifal/pfee:latest . + +# Push to registry +docker push gitea.vidoks.fr/sortifal/pfee:latest +``` + +## Step 5: Deploy to Kubernetes + +### Option A: Using the Deployment Script + +```bash +./scripts/deploy.sh gitea.vidoks.fr sortifal/pfee +``` + +### Option B: Manual Deployment + +```bash +# Create namespace +kubectl apply -f k8s/namespace.yaml + +# Create registry credentials secret +kubectl create secret docker-registry registry-credentials \ + --docker-server=gitea.vidoks.fr \ + --docker-username= \ + --docker-password= \ + -n sqdc-dashboard + +# Apply manifests +kubectl apply -f k8s/deployment.yaml +kubectl apply -f k8s/service.yaml +kubectl apply -f k8s/ingress.yaml + +# Wait for rollout +kubectl rollout status deployment/sqdc-dashboard -n sqdc-dashboard +``` + +## Step 6: Trigger CI/CD Pipeline + +Once secrets are configured, simply push to trigger the pipeline: + +```bash +git add . +git commit -m "feat: Configure CI/CD with registry" +git push origin dashboard +``` + +The workflow will: +1. Install dependencies and run tests +2. Build the React application +3. Build Docker image and push to `gitea.vidoks.fr/sortifal/pfee` +4. Deploy to Kubernetes cluster +5. Update deployment with the new image +6. Verify deployment status + +## Monitoring Deployment + +### View Workflow Logs + +1. Go to your Gitea repository +2. Click on "Actions" tab +3. Select the workflow run +4. View logs for each job (build, deploy, notify) + +### Check Kubernetes Status + +```bash +# Check pods +kubectl get pods -n sqdc-dashboard + +# Check deployment +kubectl get deployment -n sqdc-dashboard + +# Check service +kubectl get svc -n sqdc-dashboard + +# Check ingress +kubectl get ingress -n sqdc-dashboard + +# View logs +kubectl logs -f deployment/sqdc-dashboard -n sqdc-dashboard +``` + +## Troubleshooting + +### Image Pull Errors + +If pods show `ImagePullBackOff`: + +```bash +# Check if secret exists +kubectl get secret registry-credentials -n sqdc-dashboard + +# Describe the secret +kubectl describe secret registry-credentials -n sqdc-dashboard + +# Recreate the secret +kubectl delete secret registry-credentials -n sqdc-dashboard +kubectl create secret docker-registry registry-credentials \ + --docker-server=gitea.vidoks.fr \ + --docker-username= \ + --docker-password= \ + -n sqdc-dashboard +``` + +### Workflow Authentication Errors + +If the workflow fails during image push: + +1. Verify `REGISTRY_USER` and `REGISTRY_PASSWORD` secrets are correct +2. Ensure the token has `write:package` permission +3. Check registry URL matches exactly: `gitea.vidoks.fr` + +### Kubectl Connection Errors + +If deployment step fails: + +1. Verify `KUBE_CONFIG` secret contains valid kubeconfig +2. Ensure the config is in plain text (not base64 encoded) +3. Check cluster is accessible from Gitea Actions runner + +## Next Steps + +Once deployment is successful: + +1. Access the application via the ingress URL +2. Set up monitoring and alerts +3. Configure backup procedures for the database +4. Review and adjust resource limits based on usage + +For detailed documentation, see [DEPLOYMENT.md](DEPLOYMENT.md) diff --git a/dashboard-sqdc/components.json b/dashboard-sqdc/components.json new file mode 100644 index 0000000..62f1aa7 --- /dev/null +++ b/dashboard-sqdc/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/dashboard-sqdc/docker-compose.yml b/dashboard-sqdc/docker-compose.yml new file mode 100644 index 0000000..7e21edb --- /dev/null +++ b/dashboard-sqdc/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + dashboard: + build: + context: . + dockerfile: Dockerfile + container_name: sqdc-dashboard + ports: + - "8080:80" + - "3001:3001" + volumes: + # Mount database directory for persistence + - ./database:/app/database + environment: + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + default: + name: sqdc-network diff --git a/dashboard-sqdc/k8s/configmap.yaml b/dashboard-sqdc/k8s/configmap.yaml new file mode 100644 index 0000000..441f8ec --- /dev/null +++ b/dashboard-sqdc/k8s/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: sqdc-dashboard-config + namespace: sqdc-dashboard +data: + NODE_ENV: "production" + API_PORT: "3001" + # Add other environment variables as needed diff --git a/dashboard-sqdc/k8s/deployment.yaml b/dashboard-sqdc/k8s/deployment.yaml new file mode 100644 index 0000000..a6c67d8 --- /dev/null +++ b/dashboard-sqdc/k8s/deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sqdc-dashboard + namespace: sqdc-dashboard + labels: + app: sqdc-dashboard +spec: + replicas: 2 + selector: + matchLabels: + app: sqdc-dashboard + template: + metadata: + labels: + app: sqdc-dashboard + spec: + imagePullSecrets: + - name: registry-credentials + containers: + - name: dashboard + image: gitea.vidoks.fr/sortifal/pfee:latest + imagePullPolicy: Always + ports: + - containerPort: 80 + name: http + protocol: TCP + - containerPort: 3001 + name: api + protocol: TCP + env: + - name: NODE_ENV + value: "production" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 2 + volumeMounts: + - name: database + mountPath: /app/database + volumes: + - name: database + persistentVolumeClaim: + claimName: sqdc-dashboard-pvc +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: sqdc-dashboard-pvc + namespace: sqdc-dashboard +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: standard diff --git a/dashboard-sqdc/k8s/ingress.yaml b/dashboard-sqdc/k8s/ingress.yaml new file mode 100644 index 0000000..0bcabb0 --- /dev/null +++ b/dashboard-sqdc/k8s/ingress.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: sqdc-dashboard-ingress + namespace: sqdc-dashboard + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / + nginx.ingress.kubernetes.io/ssl-redirect: "false" + cert-manager.io/cluster-issuer: "letsencrypt-prod" +spec: + ingressClassName: nginx + rules: + - host: sqdc-dashboard.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: sqdc-dashboard-service + port: + number: 80 + # Uncomment for HTTPS + # tls: + # - hosts: + # - sqdc-dashboard.local + # secretName: sqdc-dashboard-tls diff --git a/dashboard-sqdc/k8s/kustomization.yaml b/dashboard-sqdc/k8s/kustomization.yaml new file mode 100644 index 0000000..395dd94 --- /dev/null +++ b/dashboard-sqdc/k8s/kustomization.yaml @@ -0,0 +1,19 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: sqdc-dashboard + +resources: + - namespace.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + - configmap.yaml + +commonLabels: + app: sqdc-dashboard + managed-by: kustomize + +images: + - name: gitea.vidoks.fr/sortifal/pfee + newTag: latest diff --git a/dashboard-sqdc/k8s/namespace.yaml b/dashboard-sqdc/k8s/namespace.yaml new file mode 100644 index 0000000..02cbd07 --- /dev/null +++ b/dashboard-sqdc/k8s/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: sqdc-dashboard + labels: + name: sqdc-dashboard + environment: production diff --git a/dashboard-sqdc/k8s/registry-secret.yaml b/dashboard-sqdc/k8s/registry-secret.yaml new file mode 100644 index 0000000..d391eff --- /dev/null +++ b/dashboard-sqdc/k8s/registry-secret.yaml @@ -0,0 +1,23 @@ +# Registry credentials secret +# This is a template - create the actual secret using: +# kubectl create secret docker-registry registry-credentials \ +# --docker-server= \ +# --docker-username= \ +# --docker-password= \ +# -n sqdc-dashboard + +# Or use this manifest (replace values): +apiVersion: v1 +kind: Secret +metadata: + name: registry-credentials + namespace: sqdc-dashboard +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: +# To generate the base64 encoded value: +# kubectl create secret docker-registry registry-credentials \ +# --docker-server=REGISTRY_URL \ +# --docker-username=REGISTRY_USER \ +# --docker-password=REGISTRY_PASSWORD \ +# --dry-run=client -o yaml | grep .dockerconfigjson diff --git a/dashboard-sqdc/k8s/service.yaml b/dashboard-sqdc/k8s/service.yaml new file mode 100644 index 0000000..b4fcfb6 --- /dev/null +++ b/dashboard-sqdc/k8s/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: sqdc-dashboard-service + namespace: sqdc-dashboard + labels: + app: sqdc-dashboard +spec: + type: ClusterIP + selector: + app: sqdc-dashboard + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP + - name: api + port: 3001 + targetPort: 3001 + protocol: TCP diff --git a/dashboard-sqdc/nginx.conf b/dashboard-sqdc/nginx.conf new file mode 100644 index 0000000..86a3b13 --- /dev/null +++ b/dashboard-sqdc/nginx.conf @@ -0,0 +1,45 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json; + + # API proxy + location /api/ { + proxy_pass http://localhost:3001/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # React app - handle client-side routing + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # Static assets with caching + location /static/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; +} diff --git a/dashboard-sqdc/package-lock.json b/dashboard-sqdc/package-lock.json index 506b3fa..0a7db7c 100644 --- a/dashboard-sqdc/package-lock.json +++ b/dashboard-sqdc/package-lock.json @@ -8,6 +8,10 @@ "name": "dashboard-sqdc", "version": "0.1.0", "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", @@ -26,13 +30,20 @@ "react-scripts": "5.0.1", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", + "tailwindcss-animate": "^1.0.7", "typescript": "^4.9.5", "web-vitals": "^2.1.4", "xlsx": "^0.18.5" }, "devDependencies": { "@types/cors": "^2.8.17", - "@types/express": "^4.17.21" + "@types/express": "^4.17.21", + "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "postcss": "^8.5.6", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^3.4.18" } }, "node_modules/@adobe/css-tools": { @@ -81,6 +92,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -730,6 +742,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1613,6 +1626,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -2460,6 +2474,44 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -3141,6 +3193,623 @@ } } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3494,6 +4163,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3892,6 +4562,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -3901,6 +4572,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4015,6 +4687,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -4068,6 +4741,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -4444,6 +5118,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4578,6 +5253,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4768,6 +5444,18 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -5550,6 +6238,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -5871,6 +6560,7 @@ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", + "peer": true, "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -5959,6 +6649,19 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -6001,6 +6704,16 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7050,6 +7763,12 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT" }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/detect-port-alt": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", @@ -7666,6 +8385,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9084,6 +9804,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-own-enumerable-property-symbols": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", @@ -10597,6 +11326,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -11494,6 +12224,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -13264,6 +13995,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14398,6 +15130,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14848,6 +15581,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14989,6 +15723,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -15013,10 +15748,58 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -15090,6 +15873,28 @@ } } }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -15459,6 +16264,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -15701,6 +16507,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -17111,11 +17918,23 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -17148,6 +17967,15 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tailwindcss/node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -17202,20 +18030,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -17646,6 +18460,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -17754,6 +18569,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17941,6 +18757,49 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18088,6 +18947,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -18159,6 +19019,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -18599,6 +19460,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/dashboard-sqdc/package.json b/dashboard-sqdc/package.json index 673bbcd..f32578d 100644 --- a/dashboard-sqdc/package.json +++ b/dashboard-sqdc/package.json @@ -3,6 +3,10 @@ "version": "0.1.0", "private": true, "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", @@ -21,13 +25,20 @@ "react-scripts": "5.0.1", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", + "tailwindcss-animate": "^1.0.7", "typescript": "^4.9.5", "web-vitals": "^2.1.4", "xlsx": "^0.18.5" }, "devDependencies": { "@types/cors": "^2.8.17", - "@types/express": "^4.17.21" + "@types/express": "^4.17.21", + "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "postcss": "^8.5.6", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^3.4.18" }, "scripts": { "start": "react-scripts start", diff --git a/dashboard-sqdc/postcss.config.js b/dashboard-sqdc/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/dashboard-sqdc/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/dashboard-sqdc/scripts/deploy.sh b/dashboard-sqdc/scripts/deploy.sh new file mode 100755 index 0000000..0c6dc49 --- /dev/null +++ b/dashboard-sqdc/scripts/deploy.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# SQDC Dashboard Deployment Script +# Usage: ./scripts/deploy.sh [registry_url] [registry_user] [registry_password] [repository_path] + +set -e + +REGISTRY_URL=${1:-"gitea.vidoks.fr"} +REGISTRY_USER=${2} +REGISTRY_PASSWORD=${3} +REPOSITORY_PATH=${4:-"sortifal/pfee"} +NAMESPACE="sqdc-dashboard" + +echo "🚀 Deploying SQDC Dashboard..." + +# Check if registry credentials are provided +if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_PASSWORD" ]; then + echo "⚠️ Warning: Registry credentials not provided. Using local image." + REGISTRY_URL="local" +fi + +# Build Docker image +echo "📦 Building Docker image..." +TIMESTAMP=$(date +%Y%m%d-%H%M%S) + +if [ "$REGISTRY_URL" != "local" ]; then + # Login to registry + echo "🔐 Logging in to registry..." + echo "$REGISTRY_PASSWORD" | docker login $REGISTRY_URL -u $REGISTRY_USER --password-stdin + + # Build and push to registry + IMAGE_NAME="$REGISTRY_URL/$REPOSITORY_PATH" + docker build -t $IMAGE_NAME:$TIMESTAMP . + docker tag $IMAGE_NAME:$TIMESTAMP $IMAGE_NAME:latest + docker push $IMAGE_NAME:$TIMESTAMP + docker push $IMAGE_NAME:latest + echo "✅ Docker image pushed: $IMAGE_NAME:$TIMESTAMP" +else + # Build local image + docker build -t sqdc-dashboard:latest . + docker tag sqdc-dashboard:latest sqdc-dashboard:$TIMESTAMP + echo "✅ Docker image built: sqdc-dashboard:$TIMESTAMP" +fi + +# Apply Kubernetes manifests +echo "☸️ Applying Kubernetes manifests..." +kubectl apply -f k8s/namespace.yaml + +# Create registry secret if credentials provided +if [ "$REGISTRY_URL" != "local" ]; then + echo "🔑 Creating registry credentials secret..." + kubectl create secret docker-registry registry-credentials \ + --docker-server=$REGISTRY_URL \ + --docker-username=$REGISTRY_USER \ + --docker-password=$REGISTRY_PASSWORD \ + -n $NAMESPACE \ + --dry-run=client -o yaml | kubectl apply -f - +fi + +kubectl apply -f k8s/deployment.yaml +kubectl apply -f k8s/service.yaml +kubectl apply -f k8s/ingress.yaml + +# Wait for rollout +echo "⏳ Waiting for deployment to complete..." +kubectl rollout status deployment/sqdc-dashboard -n $NAMESPACE --timeout=5m + +# Display deployment info +echo "" +echo "📊 Deployment Status:" +kubectl get pods -n $NAMESPACE +echo "" +kubectl get svc -n $NAMESPACE +echo "" +kubectl get ingress -n $NAMESPACE + +echo "" +echo "✅ Deployment complete!" +echo "🌐 Access the dashboard at: http://sqdc-dashboard.local" diff --git a/dashboard-sqdc/scripts/rollback.sh b/dashboard-sqdc/scripts/rollback.sh new file mode 100755 index 0000000..79b3248 --- /dev/null +++ b/dashboard-sqdc/scripts/rollback.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# SQDC Dashboard Rollback Script +# Usage: ./scripts/rollback.sh + +set -e + +NAMESPACE="sqdc-dashboard" + +echo "⏪ Rolling back SQDC Dashboard deployment..." + +kubectl rollout undo deployment/sqdc-dashboard -n $NAMESPACE + +echo "⏳ Waiting for rollback to complete..." +kubectl rollout status deployment/sqdc-dashboard -n $NAMESPACE --timeout=5m + +echo "✅ Rollback complete!" +kubectl get pods -n $NAMESPACE diff --git a/dashboard-sqdc/src/App.tsx b/dashboard-sqdc/src/App.tsx index 8f3e2eb..c903d25 100644 --- a/dashboard-sqdc/src/App.tsx +++ b/dashboard-sqdc/src/App.tsx @@ -2,7 +2,9 @@ import React, { useState } from 'react'; import { HomePage } from './pages/HomePage'; import { DetailPage } from './pages/DetailPage'; import { TrendChart, CategoryDistributionChart, StatusChart, CNQChart } from './components/Charts'; -import './App.css'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs'; +import { Card } from './components/ui/card'; +import { Shield, Target, Clock, DollarSign, Wrench, BarChart3, Home } from 'lucide-react'; type TabType = 'home' | 'security' | 'quality' | 'delays' | 'costs' | 'maintenance' | 'charts'; @@ -10,112 +12,146 @@ function App() { const [activeTab, setActiveTab] = useState('home'); return ( -
-
-
-
-

📊 Dashboard SQDC

-

Indicateurs de Performance - Sécurité, Qualité, Délais, Coûts

-
-
- {new Date().toLocaleDateString('fr-FR', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' - })} +
+ {/* Header */} +
+
+
+
+

SQDC Dashboard

+

+ Safety • Quality • Delivery • Cost • Maintenance +

+
+
+ {new Date().toLocaleDateString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric' + })} +
- + {/* Navigation */} +
+
+ setActiveTab(value as TabType)}> + + + + Overview + + + + Safety + + + + Quality + + + + Delivery + + + + Cost + + + + Maintenance + + + + Analytics + + -
- {activeTab === 'home' && ( - - )} - - {activeTab === 'security' && ( - - )} - - {activeTab === 'quality' && ( - - )} - - {activeTab === 'delays' && ( - - )} - - {activeTab === 'costs' && ( - - )} - - {activeTab === 'maintenance' && ( - - )} - - {activeTab === 'charts' && ( -
-
-

📈 Analyses et Tendances

-

Vue globale des tendances et des analyses comparatives

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

Analytics & Trends

+

Comprehensive performance analysis

+
+
+ + + + + + + + + + + + +
+
+
+ +
+
+ + {/* Footer */} +
+
+
+

SQDC Dashboard - Real-time Performance Monitoring

+

Last updated: {new Date().toLocaleTimeString('en-US', { + hour: '2-digit', + minute: '2-digit' + })}

- )} - - -
-

Dashboard SQDC | Dernière mise à jour: {new Date().toLocaleTimeString('fr-FR')} | Données en temps réel

+
); diff --git a/dashboard-sqdc/src/components/ChartModal.tsx b/dashboard-sqdc/src/components/ChartModal.tsx index c62fa32..b537b84 100644 --- a/dashboard-sqdc/src/components/ChartModal.tsx +++ b/dashboard-sqdc/src/components/ChartModal.tsx @@ -1,7 +1,14 @@ import React from 'react'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js'; import { Line } from 'react-chartjs-2'; -import '../styles/ChartModal.css'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { LineChart } from 'lucide-react'; ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); @@ -13,13 +20,12 @@ interface ChartModalProps { } export const ChartModal: React.FC = ({ isOpen, kpi, measurements, onClose }) => { - if (!isOpen || !kpi) return null; + if (!kpi) return null; - // Préparer les données pour le graphique const labels = measurements .map(m => new Date(m.measurement_date).toLocaleDateString('fr-FR')) .reverse(); - + const values = measurements .map(m => m.value) .reverse(); @@ -30,13 +36,13 @@ export const ChartModal: React.FC = ({ isOpen, kpi, measurement { label: kpi.name, data: values, - borderColor: '#6496ff', - backgroundColor: 'rgba(100, 150, 255, 0.1)', + borderColor: '#60a5fa', + backgroundColor: 'rgba(96, 165, 250, 0.1)', tension: 0.4, fill: true, pointRadius: 4, - pointBackgroundColor: '#6496ff', - pointBorderColor: '#fff', + pointBackgroundColor: '#60a5fa', + pointBorderColor: '#1e293b', pointBorderWidth: 2, }, ], @@ -49,11 +55,12 @@ export const ChartModal: React.FC = ({ isOpen, kpi, measurement legend: { display: true, position: 'top' as const, + labels: { + color: '#cbd5e1', + }, }, title: { - display: true, - text: `Graphique Complet: ${kpi.name}`, - font: { size: 16, weight: 'bold' as const }, + display: false, }, }, scales: { @@ -62,31 +69,46 @@ export const ChartModal: React.FC = ({ isOpen, kpi, measurement title: { display: true, text: kpi.unit, + color: '#cbd5e1', + }, + ticks: { + color: '#94a3b8', + }, + grid: { + color: 'rgba(148, 163, 184, 0.1)', }, }, x: { display: true, + ticks: { + color: '#94a3b8', + }, + grid: { + color: 'rgba(148, 163, 184, 0.1)', + }, }, }, }; return ( -
-
e.stopPropagation()}> -
-

📈 {kpi.name}

- -
-
+ + + + + + {kpi.name} + + + Nombre de mesures: {measurements.length} | Période: {labels[0]} à {labels[labels.length - 1]} + + + +
-
-

Nombre de mesures: {measurements.length}

-

Période: {labels[0]} à {labels[labels.length - 1]}

-
-
-
+ + ); }; diff --git a/dashboard-sqdc/src/components/Charts.tsx b/dashboard-sqdc/src/components/Charts.tsx index 3f894a6..dde340b 100644 --- a/dashboard-sqdc/src/components/Charts.tsx +++ b/dashboard-sqdc/src/components/Charts.tsx @@ -12,7 +12,6 @@ import { Legend } from 'chart.js'; import { kpiData, getCategoryColor } from '../data/kpiData'; -import '../styles/Charts.css'; ChartJS.register( CategoryScale, @@ -25,60 +24,107 @@ ChartJS.register( Legend ); +const commonOptions = { + plugins: { + legend: { + labels: { + color: '#cbd5e1', + }, + }, + }, + scales: { + y: { + ticks: { + color: '#94a3b8', + }, + grid: { + color: 'rgba(148, 163, 184, 0.1)', + }, + }, + x: { + ticks: { + color: '#94a3b8', + }, + grid: { + color: 'rgba(148, 163, 184, 0.1)', + }, + }, + }, +}; + export const TrendChart: React.FC = () => { const data = { - labels: ['Semaine 1', 'Semaine 2', 'Semaine 3', 'Semaine 4'], + labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'], datasets: [ { - label: 'Sécurité (%)', + label: 'Safety (%)', data: [94, 95, 96, 96], - borderColor: getCategoryColor('security'), - backgroundColor: 'rgba(231, 76, 60, 0.1)', + borderColor: '#ef4444', + backgroundColor: 'rgba(239, 68, 68, 0.1)', tension: 0.4 }, { - label: 'Qualité (%)', + label: 'Quality (%)', data: [75, 76, 77, 78.5], - borderColor: getCategoryColor('quality'), - backgroundColor: 'rgba(52, 152, 219, 0.1)', + borderColor: '#3b82f6', + backgroundColor: 'rgba(59, 130, 246, 0.1)', tension: 0.4 }, { - label: 'Délais (%)', + label: 'Delivery (%)', data: [97, 97.5, 97.8, 98], - borderColor: getCategoryColor('delays'), - backgroundColor: 'rgba(243, 156, 18, 0.1)', + borderColor: '#f59e0b', + backgroundColor: 'rgba(245, 158, 11, 0.1)', tension: 0.4 }, { - label: 'Coûts (%)', + label: 'Cost (%)', data: [92, 91, 90.5, 89], - borderColor: getCategoryColor('costs'), - backgroundColor: 'rgba(39, 174, 96, 0.1)', + borderColor: '#10b981', + backgroundColor: 'rgba(16, 185, 129, 0.1)', tension: 0.4 } ] }; return ( -
-

📈 Tendances SQDC

- +
+

SQDC Trends

+
+ +
); }; export const CategoryDistributionChart: React.FC = () => { const categoryCounts = { - 'Sécurité': kpiData.security.length, - 'Qualité': kpiData.quality.length, - 'Délais': kpiData.delays.length, - 'Coûts': kpiData.costs.length, + 'Safety': kpiData.security.length, + 'Quality': kpiData.quality.length, + 'Delivery': kpiData.delays.length, + 'Cost': kpiData.costs.length, 'Maintenance': kpiData.maintenance.length }; @@ -87,40 +133,41 @@ export const CategoryDistributionChart: React.FC = () => { datasets: [{ data: Object.values(categoryCounts), backgroundColor: [ - getCategoryColor('security'), - getCategoryColor('quality'), - getCategoryColor('delays'), - getCategoryColor('costs'), - getCategoryColor('maintenance') + '#ef4444', + '#3b82f6', + '#f59e0b', + '#10b981', + '#8b5cf6' ], - borderColor: 'white', + borderColor: '#1e293b', borderWidth: 2 }] }; return ( -
-

📊 Répartition KPI par Catégorie

- +
+

KPI Distribution by Category

+
+ +
); }; export const StatusChart: React.FC = () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const allKPIs = [ - ...kpiData.security, - ...kpiData.quality, - ...kpiData.delays, - ...kpiData.costs, - ...kpiData.maintenance - ]; - - const categories = ['Sécurité', 'Qualité', 'Délais', 'Coûts', 'Maintenance']; + const categories = ['Safety', 'Quality', 'Delivery', 'Cost', 'Maintenance']; const categoryKPIs = [ kpiData.security, kpiData.quality, @@ -133,65 +180,95 @@ export const StatusChart: React.FC = () => { labels: categories, datasets: [ { - label: 'Bon', + label: 'On Target', data: categoryKPIs.map(cat => cat.filter(k => k.status === 'good').length), - backgroundColor: '#27ae60' + backgroundColor: '#10b981' }, { - label: 'À améliorer', + label: 'Warning', data: categoryKPIs.map(cat => cat.filter(k => k.status === 'warning').length), - backgroundColor: '#f39c12' + backgroundColor: '#f59e0b' }, { - label: 'Critique', + label: 'Critical', data: categoryKPIs.map(cat => cat.filter(k => k.status === 'critical').length), - backgroundColor: '#e74c3c' + backgroundColor: '#ef4444' } ] }; return ( -
-

📊 État des KPI par Catégorie

- +
+

KPI Status by Category

+
+ +
); }; export const CNQChart: React.FC = () => { const data = { - labels: ['Rebuts', 'Retouches', 'Retours Clients'], + labels: ['Scrap', 'Rework', 'Returns'], datasets: [{ - label: 'Coût (€)', + label: 'Cost (€)', data: [8500, 7200, 2800], backgroundColor: [ - '#e74c3c', - '#f39c12', - '#3498db' + '#ef4444', + '#f59e0b', + '#3b82f6' ], - borderColor: 'white', + borderColor: '#1e293b', borderWidth: 2 }] }; return ( -
-

💔 Coûts des Non-Qualité

- +
+

Non-Quality Costs

+
+ +
); }; diff --git a/dashboard-sqdc/src/components/ExportModal.tsx b/dashboard-sqdc/src/components/ExportModal.tsx index 33d4420..1c2f42d 100644 --- a/dashboard-sqdc/src/components/ExportModal.tsx +++ b/dashboard-sqdc/src/components/ExportModal.tsx @@ -1,5 +1,14 @@ import React, { useState } from 'react'; -import '../styles/ExportModal.css'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { Calendar, FileDown } from 'lucide-react'; interface ExportModalProps { isOpen: boolean; @@ -11,8 +20,6 @@ interface ExportModalProps { export const ExportModal: React.FC = ({ isOpen, kpiName, onExport, onClose }) => { const [selectedRange, setSelectedRange] = useState(null); - if (!isOpen) return null; - const handleExport = () => { if (selectedRange !== null) { onExport(selectedRange); @@ -21,83 +28,68 @@ export const ExportModal: React.FC = ({ isOpen, kpiName, onExp } }; + const ranges = [ + { value: 7, label: 'Dernière semaine', sublabel: '7 jours', icon: Calendar }, + { value: 30, label: 'Dernier mois', sublabel: '30 jours', icon: Calendar }, + { value: 365, label: 'Cette année', sublabel: '365 jours', icon: Calendar }, + { value: -1, label: 'Toutes les données', sublabel: 'Sans limite', icon: FileDown }, + ]; + return ( -
-
e.stopPropagation()}> -
-

💾 Exporter {kpiName}

- + + + + + + Exporter {kpiName} + + + Sélectionnez la plage de données à exporter en Excel + + + +
+ {ranges.map((range) => { + const Icon = range.icon; + return ( + + ); + })}
-
-

- Sélectionnez la plage de données à exporter: -

- -
- - - - - - - -
-
- -
- - -
-
-
+ + + + + ); }; diff --git a/dashboard-sqdc/src/components/KPICard.tsx b/dashboard-sqdc/src/components/KPICard.tsx index 51e0dc1..21954cb 100644 --- a/dashboard-sqdc/src/components/KPICard.tsx +++ b/dashboard-sqdc/src/components/KPICard.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import '../styles/KPICard.css'; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from './ui/card'; +import { Badge } from './ui/badge'; +import { TrendingUp, Minus } from 'lucide-react'; interface KPICardProps { kpi: any; @@ -7,54 +9,70 @@ interface KPICardProps { } export const KPICard: React.FC = ({ kpi, color }) => { - const getStatusIcon = () => { + const getStatusVariant = () => { switch (kpi.status) { case 'good': - return '✓'; + return 'success'; case 'warning': - return '⚠'; + return 'warning'; case 'critical': - return '🔴'; + return 'destructive'; default: - return '•'; + return 'default'; + } + }; + + const getStatusLabel = () => { + switch (kpi.status) { + case 'good': + return 'Bon'; + case 'warning': + return 'Attention'; + case 'critical': + return 'Critique'; + default: + return 'N/A'; } }; const getTrendIcon = () => { - if (!kpi.latest) return '•'; - - // Déterminer la tendance basée sur les données de la dernière semaine - return '📊'; // Placeholder pour l'instant - }; - - const getStatusClass = () => { - return `status-${kpi.status}`; + if (!kpi.latest) return ; + return ; }; return ( -
-
-

{kpi.name}

- {getTrendIcon()} -
- -
-
{kpi.value}
-
{kpi.unit}
-
+ + +
+ + {kpi.name} + +
+ {getTrendIcon()} +
+
+
-
- - {getStatusIcon()} {kpi.status?.charAt(0).toUpperCase() + kpi.status?.slice(1) || 'N/A'} - + +
+
{kpi.value}
+
{kpi.unit}
+
+

+ {kpi.description} +

+
+ + + + {getStatusLabel()} + {kpi.target && ( - + Obj: {kpi.target} {kpi.unit} )} -
- -

{kpi.description}

-
+ + ); }; diff --git a/dashboard-sqdc/src/components/RangeChartModal.tsx b/dashboard-sqdc/src/components/RangeChartModal.tsx index 26d03ea..e19cd5d 100644 --- a/dashboard-sqdc/src/components/RangeChartModal.tsx +++ b/dashboard-sqdc/src/components/RangeChartModal.tsx @@ -1,7 +1,15 @@ import React, { useState } from 'react'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js'; import { Line } from 'react-chartjs-2'; -import '../styles/RangeChartModal.css'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from './ui/dialog'; +import { Button } from './ui/button'; +import { LineChart } from 'lucide-react'; ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); @@ -22,16 +30,14 @@ export const RangeChartModal: React.FC = ({ }) => { const [selectedRange, setSelectedRange] = useState(30); - if (!isOpen || !kpi) return null; + if (!kpi) return null; - // Obtenir les mesures pour la plage sélectionnée const filteredMeasurements = getMeasurementsForRange(selectedRange); - // Préparer les données pour le graphique const labels = filteredMeasurements .map(m => new Date(m.measurement_date).toLocaleDateString('fr-FR')) .reverse(); - + const values = filteredMeasurements .map(m => m.value) .reverse(); @@ -42,13 +48,13 @@ export const RangeChartModal: React.FC = ({ { label: kpi.name, data: values, - borderColor: '#6496ff', - backgroundColor: 'rgba(100, 150, 255, 0.1)', + borderColor: '#60a5fa', + backgroundColor: 'rgba(96, 165, 250, 0.1)', tension: 0.4, fill: true, pointRadius: 4, - pointBackgroundColor: '#6496ff', - pointBorderColor: '#fff', + pointBackgroundColor: '#60a5fa', + pointBorderColor: '#1e293b', pointBorderWidth: 2, }, ], @@ -61,11 +67,12 @@ export const RangeChartModal: React.FC = ({ legend: { display: true, position: 'top' as const, + labels: { + color: '#cbd5e1', + }, }, title: { - display: true, - text: `Graphique Complet: ${kpi.name}`, - font: { size: 16, weight: 'bold' as const }, + display: false, }, }, scales: { @@ -74,86 +81,67 @@ export const RangeChartModal: React.FC = ({ title: { display: true, text: kpi.unit, + color: '#cbd5e1', + }, + ticks: { + color: '#94a3b8', + }, + grid: { + color: 'rgba(148, 163, 184, 0.1)', }, }, x: { display: true, + ticks: { + color: '#94a3b8', + }, + grid: { + color: 'rgba(148, 163, 184, 0.1)', + }, }, }, }; + const ranges = [ + { value: 7, label: 'Semaine' }, + { value: 30, label: 'Mois' }, + { value: 90, label: 'Trimestre' }, + { value: 365, label: 'Année' }, + { value: -1, label: 'Tout' }, + ]; + return ( -
-
e.stopPropagation()}> -
-

📈 {kpi.name}

- + + + + + + {kpi.name} + + + Mesures: {filteredMeasurements.length} | Période: {labels[0]} à {labels[labels.length - 1]} + + + +
+ {ranges.map((range) => ( + + ))}
-
- - - - - -
- -
+
- -
-

Mesures: {filteredMeasurements.length}

-

Période: {labels[0]} à {labels[labels.length - 1]}

-
-
-
+ + ); }; diff --git a/dashboard-sqdc/src/components/ui/badge.tsx b/dashboard-sqdc/src/components/ui/badge.tsx new file mode 100644 index 0000000..10a4f7f --- /dev/null +++ b/dashboard-sqdc/src/components/ui/badge.tsx @@ -0,0 +1,40 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../../lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-red-600 text-white hover:bg-red-700", + outline: "text-foreground", + success: + "border-transparent bg-emerald-600 text-white hover:bg-emerald-700", + warning: + "border-transparent bg-amber-500 text-gray-900 hover:bg-amber-600", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/dashboard-sqdc/src/components/ui/button.tsx b/dashboard-sqdc/src/components/ui/button.tsx new file mode 100644 index 0000000..2c1f020 --- /dev/null +++ b/dashboard-sqdc/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "../../lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/dashboard-sqdc/src/components/ui/card.tsx b/dashboard-sqdc/src/components/ui/card.tsx new file mode 100644 index 0000000..c1b6fbb --- /dev/null +++ b/dashboard-sqdc/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "../../lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/dashboard-sqdc/src/components/ui/dialog.tsx b/dashboard-sqdc/src/components/ui/dialog.tsx new file mode 100644 index 0000000..266c901 --- /dev/null +++ b/dashboard-sqdc/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "../../lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/dashboard-sqdc/src/components/ui/table.tsx b/dashboard-sqdc/src/components/ui/table.tsx new file mode 100644 index 0000000..6d7543b --- /dev/null +++ b/dashboard-sqdc/src/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "../../lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/dashboard-sqdc/src/components/ui/tabs.tsx b/dashboard-sqdc/src/components/ui/tabs.tsx new file mode 100644 index 0000000..f5087f9 --- /dev/null +++ b/dashboard-sqdc/src/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "../../lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/dashboard-sqdc/src/index.css b/dashboard-sqdc/src/index.css index dfe5191..b957500 100644 --- a/dashboard-sqdc/src/index.css +++ b/dashboard-sqdc/src/index.css @@ -1,25 +1,52 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 224 71% 4%; + --foreground: 213 31% 91%; + --card: 224 71% 4%; + --card-foreground: 213 31% 91%; + --popover: 224 71% 4%; + --popover-foreground: 213 31% 91%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 222.2 47.4% 11.2%; + --secondary-foreground: 210 40% 98%; + --muted: 223 47% 11%; + --muted-foreground: 215.4 16.3% 56.9%; + --accent: 216 34% 17%; + --accent-foreground: 210 40% 98%; + --destructive: 0 63% 31%; + --destructive-foreground: 210 40% 98%; + --border: 216 34% 17%; + --input: 216 34% 17%; + --ring: 216 34% 17%; + --radius: 0.5rem; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } } -html, body, #root { - height: 100%; - width: 100%; -} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background: #f5f7fa; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + html, body, #root { + height: 100%; + width: 100%; + } } diff --git a/dashboard-sqdc/src/lib/utils.ts b/dashboard-sqdc/src/lib/utils.ts new file mode 100644 index 0000000..d084cca --- /dev/null +++ b/dashboard-sqdc/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/dashboard-sqdc/src/pages/DetailPage.tsx b/dashboard-sqdc/src/pages/DetailPage.tsx index f6358a0..cf60754 100644 --- a/dashboard-sqdc/src/pages/DetailPage.tsx +++ b/dashboard-sqdc/src/pages/DetailPage.tsx @@ -1,11 +1,15 @@ import React, { useState, useEffect } from 'react'; import * as XLSX from 'xlsx'; -import { getCategoryColor, getCategoryName, getCategoryEmoji } from '../data/kpiData'; +import { getCategoryColor, getCategoryName } from '../data/kpiData'; import { useSQLiteDatabase } from '../database/useSQLiteDatabase'; import { ChartModal } from '../components/ChartModal'; import { ExportModal } from '../components/ExportModal'; import { RangeChartModal } from '../components/RangeChartModal'; -import '../styles/DetailPage.css'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; +import { Button } from '../components/ui/button'; +import { Badge } from '../components/ui/badge'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../components/ui/table'; +import { Download, LineChart } from 'lucide-react'; interface DetailPageProps { category: 'security' | 'quality' | 'delays' | 'costs' | 'maintenance'; @@ -29,10 +33,8 @@ export const DetailPage: React.FC = ({ category }) => { const [showExportModal, setShowExportModal] = useState(false); const [showChartRangeModal, setShowChartRangeModal] = useState(false); - // Obtenir les KPI de cette catégorie const categoryKPIs = db.kpis.filter(kpi => kpi.category_id === categoryId); - // Charger les mesures quand un KPI est sélectionné useEffect(() => { if (!selectedKPIId) { if (categoryKPIs.length > 0) { @@ -53,23 +55,19 @@ export const DetailPage: React.FC = ({ category }) => { const selectedKPI = categoryKPIs.find(k => k.id === selectedKPIId); - // Export to Excel const exportToExcel = (kpi: any, measurements: any[]) => { if (!kpi || measurements.length === 0) return; - // Préparer les données const data = measurements.map(m => ({ Date: new Date(m.measurement_date).toLocaleString('fr-FR'), Valeur: m.value, Statut: m.status, })); - // Créer un classeur Excel const worksheet = XLSX.utils.json_to_sheet(data); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'Mesures'); - // Ajouter une feuille de résumé const summary = [ ['KPI', kpi.name], ['Unité', kpi.unit], @@ -78,167 +76,206 @@ export const DetailPage: React.FC = ({ category }) => { ['Nombre de mesures', measurements.length], ['Date d\'export', new Date().toLocaleString('fr-FR')], ]; - + const summarySheet = XLSX.utils.aoa_to_sheet(summary); XLSX.utils.book_append_sheet(workbook, summarySheet, 'Résumé'); - // Télécharger const filename = `${kpi.name.replace(/\s+/g, '_')}_${new Date().toISOString().split('T')[0]}.xlsx`; XLSX.writeFile(workbook, filename); }; - // Handle export with date range selection const handleExportClick = (days: number) => { if (!selectedKPI) return; - + let measurementsToExport = selectedKPIMeasurements; - - // Si l'utilisateur a sélectionné une plage spécifique (pas -1 pour tous) + if (days > 0 && days !== 365) { measurementsToExport = selectedKPIMeasurements.filter((m: any) => { const daysAgo = (Date.now() - new Date(m.measurement_date).getTime()) / (1000 * 60 * 60 * 24); return daysAgo <= days; }); } - + exportToExcel(selectedKPI, measurementsToExport); }; - // Get measurements for chart by date range const getMeasurementsForDateRange = (days: number) => { if (days === -1) return selectedKPIMeasurements; - + return selectedKPIMeasurements.filter((m: any) => { const daysAgo = (Date.now() - new Date(m.measurement_date).getTime()) / (1000 * 60 * 60 * 24); return daysAgo <= days; }); }; + const getStatusBadge = (status: string) => { + switch (status) { + case 'good': + return On Target; + case 'warning': + return Warning; + case 'critical': + return Critical; + default: + return N/A; + } + }; + return ( -
-
-

- {getCategoryEmoji(category)} {getCategoryName(category)} +
+ {/* Header */} +
+

+ {getCategoryName(category)}

-

Analyse détaillée des indicateurs

+

Detailed metric analysis and performance tracking

-
-
-

Sélectionner un KPI

- -
- - -
- -
- {categoryKPIs.map(kpi => ( - - ))} -
-
+ + Exporter + + +
+
+ {categoryKPIs.map(kpi => ( + + ))} +
+ + + + {/* KPI Details */} {selectedKPI && ( -
-
-

{selectedKPI.name}

-
-
-
Unité
-
{selectedKPI.unit}
+
+ {/* KPI Info */} + + + {selectedKPI.name} + + +
+
+
Unité
+
{selectedKPI.unit}
+
+
+
Cible
+
{selectedKPI.target}
+
+
+
Fréquence
+
{selectedKPI.frequency || 'N/A'}
+
-
-
Cible
-
{selectedKPI.target}
-
-
-
Fréquence
-
{selectedKPI.frequency || 'N/A'}
-
-
-
-
-

Description

-

{selectedKPI.description}

-

Formule

-

{selectedKPI.formula}

-
+
+

Description

+

{selectedKPI.description}

+
+
+

Formule

+

{selectedKPI.formula}

+
+ + + + {/* Statistics */} {selectedKPIStats && ( -
-

Statistiques (30 derniers jours)

-
-
-
Moyenne
-
{selectedKPIStats.avg?.toFixed(2) || 'N/A'}
+ + + Statistiques (30 derniers jours) + + +
+
+
{selectedKPIStats.avg?.toFixed(2) || 'N/A'}
+
Moyenne
+
+
+
{selectedKPIStats.min?.toFixed(2) || 'N/A'}
+
Min
+
+
+
{selectedKPIStats.max?.toFixed(2) || 'N/A'}
+
Max
+
+
+
{selectedKPIStats.count || 0}
+
Mesures
+
-
-
Min
-
{selectedKPIStats.min?.toFixed(2) || 'N/A'}
-
-
-
Max
-
{selectedKPIStats.max?.toFixed(2) || 'N/A'}
-
-
-
Mesures
-
{selectedKPIStats.count || 0}
-
-
-
+ + )} -
-

Dernières mesures ({selectedKPIMeasurements.length})

- - - - - - - - - - {selectedKPIMeasurements.slice(-10).reverse().map((m: any, idx: number) => ( - - - - - - ))} - -
DateValeurStatut
{new Date(m.measurement_date).toLocaleString('fr-FR')}{m.value} - - {m.status === 'good' ? '✓ Bon' : m.status === 'warning' ? '⚠️ Attention' : '🔴 Critique'} - -
-
+ {/* Measurements Table */} + + + Dernières mesures ({selectedKPIMeasurements.length}) + + + + + + Date + Valeur + Statut + + + + {selectedKPIMeasurements.slice(-10).reverse().map((m: any, idx: number) => ( + + {new Date(m.measurement_date).toLocaleString('fr-FR')} + {m.value} + {getStatusBadge(m.status)} + + ))} + +
+
+
)}
- { const db = useSQLiteDatabase(); @@ -12,7 +15,6 @@ export const HomePage: React.FC = () => { const [avgPerformance, setAvgPerformance] = useState(0); const [criticalAlerts, setCriticalAlerts] = useState([]); - // Convertir la plage de temps en nombre de jours const getDaysFromTimeRange = (range: 'today' | 'week' | 'last7' | 'month' | 'year'): number => { switch (range) { case 'today': @@ -34,7 +36,6 @@ export const HomePage: React.FC = () => { } }; - // Charger les données quand les KPI changent ou que la plage change useEffect(() => { if (db.loading || db.kpis.length === 0) return; @@ -43,10 +44,9 @@ export const HomePage: React.FC = () => { const kpisWithStatus: any[] = []; const alertList: any[] = []; - // Charger les mesures pour chaque KPI for (const kpi of db.kpis) { const measurements = await db.getMeasurementsForKPI(kpi.id, days); - + let status = 'good'; let value = 0; @@ -55,7 +55,7 @@ export const HomePage: React.FC = () => { value = Math.round((values.reduce((a: number, b: number) => a + b, 0) / values.length) * 100) / 100; const tolerance = kpi.target! * 0.1; - + if ([2, 4, 5, 6, 13, 16].includes(kpi.id)) { if (value > kpi.target! + tolerance * 2) status = 'critical'; else if (value > kpi.target! + tolerance) status = 'warning'; @@ -74,7 +74,6 @@ export const HomePage: React.FC = () => { kpisWithStatus.push({ ...kpi, status, value }); } - // Calculer les statistiques const statCounts = { total: kpisWithStatus.length, good: kpisWithStatus.filter(k => k.status === 'good').length, @@ -82,15 +81,13 @@ export const HomePage: React.FC = () => { critical: kpisWithStatus.filter(k => k.status === 'critical').length }; - // Grouper par catégorie const topKPIsMap: any = {}; [1, 2, 3, 4, 5].forEach(catId => { topKPIsMap[catId] = kpisWithStatus .filter(k => k.category_id === catId) - .slice(0, 2); + .slice(0, 1); }); - // Performance globale const performance = Math.round( ((statCounts.good * 100 + statCounts.warning * 50) / (statCounts.total * 100)) * 100 ); @@ -102,7 +99,7 @@ export const HomePage: React.FC = () => { if (a.status === 'critical' && b.status !== 'critical') return -1; if (a.status !== 'critical' && b.status === 'critical') return 1; return 0; - }).slice(0, 4)); + }).slice(0, 3)); }; fetchData(); @@ -110,138 +107,194 @@ export const HomePage: React.FC = () => { if (db.loading) { return ( -
-
-

⏳ Chargement des données...

-
+
+ + +
+
+

Loading data...

+
+
+
); } if (db.error) { return ( -
-
-

❌ {db.error}

-

Assurez-vous que le serveur API est lancé: npm run server

-
+
+ + +
+ +

{db.error}

+

+ Please ensure the API server is running: npm run server +

+
+
+
); } + const timeRangeOptions = [ + { value: 'today', label: 'Today' }, + { value: 'week', label: 'Week' }, + { value: 'last7', label: 'Last 7D' }, + { value: 'month', label: 'Month' }, + { value: 'year', label: 'Year' }, + ] as const; + return ( -
-
-
-

📊 Dashboard SQDC

-
- - - - - +
+
+ {/* Compact Header */} +
+
+

Performance Overview

+

Real-time SQDC Metrics

+
+
+ {timeRangeOptions.map(option => ( + + ))}
- {criticalAlerts.length > 0 && ( -
-

🚨 Alertes ({criticalAlerts.length})

-
- {criticalAlerts.map(alert => ( -
-
- {alert.status === 'critical' ? '🔴' : '⚠️'} + {/* Compact Stats - Single Row */} +
+ + +
+ + Total +
+
{stats.total}
+
+
+ + + +
+ + On Target +
+
{stats.good}
+
+
+ + + +
+ + Warning +
+
{stats.warning}
+
+
+ + + +
+ + Critical +
+
{stats.critical}
+
+
+ + + +
+ + Score +
+
{avgPerformance}%
+
+
+
+ + {/* Main Content Grid */} +
+ {/* Left Column - Categories */} +
+ {[1, 2, 3, 4, 5].map(catId => { + const categoryKPIs = topKPIs[catId] || []; + const category = db.categories.find(c => c.id === catId); + + if (!category || categoryKPIs.length === 0) return null; + + return ( +
+
+
+

+ {category.name} +

-
-
{alert.name}
-
{alert.value} {alert.unit}
+
+ {categoryKPIs.map((kpi: any) => ( + + ))}
- ))} -
+ ); + })}
- )} -
-
-
-
-

📊 Statistiques

-
-
-
-
{stats.total}
-
KPI Total
-
-
-
{stats.good}
-
✅ Bon
-
-
-
{stats.warning}
-
⚠️ Attention
-
-
-
{stats.critical}
-
🔴 Critique
-
-
-
{avgPerformance}%
-
Performance
-
+ {/* Right Column - Alerts */} +
+ + + + + Alerts + {criticalAlerts.length > 0 && ( + + {criticalAlerts.length} + + )} + + + + {criticalAlerts.length === 0 ? ( +
+ +

All metrics on target

+
+ ) : ( + criticalAlerts.map(alert => ( +
+ + {alert.status === 'critical' ? 'Critical' : 'Warning'} + +
{alert.name}
+
+ {alert.value} {alert.unit} +
+
+ )) + )} +
+
- - {[1, 2, 3, 4, 5].map(catId => { - const categoryKPIs = topKPIs[catId] || []; - const category = db.categories.find(c => c.id === catId); - - if (!category) return null; - - return ( -
-
-

- {category.emoji} {category.name} -

-
-
- {categoryKPIs.map((kpi: any) => ( - - ))} -
-
- ); - })}
); diff --git a/dashboard-sqdc/tailwind.config.js b/dashboard-sqdc/tailwind.config.js new file mode 100644 index 0000000..62ed56e --- /dev/null +++ b/dashboard-sqdc/tailwind.config.js @@ -0,0 +1,77 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: [ + './pages/**/*.{ts,tsx}', + './components/**/*.{ts,tsx}', + './app/**/*.{ts,tsx}', + './src/**/*.{ts,tsx}', + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} diff --git a/les formules mathematique sont bonne mais sont ma... - les formules mathematique sont bonne mais sont ma....csv b/les formules mathematique sont bonne mais sont ma... - les formules mathematique sont bonne mais sont ma....csv deleted file mode 100644 index cb88896..0000000 --- a/les formules mathematique sont bonne mais sont ma... - les formules mathematique sont bonne mais sont ma....csv +++ /dev/null @@ -1,22 +0,0 @@ -Catégorie,KPI (Indicateur),Objectif,Formule de Calcul Ajustée -SÉCURITÉ,Taux de Fréquence (TF),Mesurer la fréquence des accidents avec arrêt.,(Nombre d’Accidents avec Arret/Nombre d’Heures Travaillees)×1000000 -SÉCURITÉ,Nombre d'Incidents/Near Miss,Évaluer la culture de sécurité et la proactivité.,Compte des rapports d’incidents (sans blessure/dommage) -SÉCURITÉ,Taux de Conformité aux Audits,Mesurer le respect des procédures de sécurité.,(Points de Controle Conformites/Total Points de Controle)×100 -QUALITÉ,Taux de Rebut (Scrap Rate),Mesurer le pourcentage d'unités jetées (irréparables).,(Nombre d’Unites Rebutees/Nombre Total d’Unites Produites)×100 -QUALITÉ,Taux de Retouche (Rework Rate),Mesurer le pourcentage d'unités nécessitant une reprise.,(Nombre d’Unites Retouchees/Nombre Total d’Unites Produites)×100 -QUALITÉ,Nombre de Défauts par Unité (DPU),Mesurer le nombre moyen de défauts par produit.,Nombre Total de Defauts Trouves/Nombre Total d’Unites Inspectees -QUALITÉ,Taux de Retours Clients,Mesurer l'impact de la non-qualité chez le client.,(Nombre d’Unites Retournees/Nombre Total d’Unites Vendues)×100 -QUALITÉ,Taux de rendement synthétique (TRS),,Nb pièces bonnes x Temps cycle / Temps d’ouverture -DÉLAIS / LIVRAISON,Efficacité Globale de l'Équipement (OEE),"Mesurer l'efficacité combinée (Disponibilité, Performance, Qualité).",Disponibilite×Performance×Qualite -DÉLAIS / LIVRAISON,Taux de Respect du Plan (Schedule Adherence),Mesurer la capacité à atteindre le volume planifié.,(Quantite Reellement Produite/Quantite Planifiee)×100 -DÉLAIS / LIVRAISON,"Temps de Cycle (Cycle Time, TC)",Mesurer le temps nécessaire pour assembler une unité.,Temps Total de Production/Nombre Total d’Unites Produites -DÉLAIS / LIVRAISON,Tack Time (TT),,Temps de production / Nombre de pièces demandées -DÉLAIS / LIVRAISON,Temps d'Arrêt Imprévu (Downtime),Mesurer le temps d'arrêt non planifié de la ligne.,Somme des Periodes d’Arret Non Planifie -COÛT,Coût par Unité (CPU),Mesurer l'efficacité des coûts de production.,Cout Total de Production/Nombre Total d’Unites Produites -COÛT,Productivité de la Main-d'œuvre,Mesurer l'efficacité de l'équipe d'assemblage.,Nombre d’Unites Produites/Total Heures Main-d’œuvre Directe -COÛT,Coût des Non-Qualité (CNQ),"Mesurer le coût des défauts (retouche, rebut, retours).",Cout des Rebuts+Cout des Retouches -MAINTENANCE,Temps Moyen Entre Pannes (MTBF),Mesurer la fiabilité des équipements.,Temps Total de Fonctionnement/Nombre Total de Pannes -MAINTENANCE,Temps Moyen de Réparation (MTTR),Mesurer la rapidité des interventions de maintenance.,Temps Total de Reparation/Nombre Total de Pannes -MAINTENANCE,Ratio Maintenance Préventive / Corrective,Évaluer la stratégie de maintenance (proactif vs réactif).,Heures MP/Heures MC -MAINTENANCE,Taux d'Achèvement du Plan Préventif,Mesurer le respect des programmes d'entretien.,(Taches MP Terminees/Taches MP Planifiees)×100 -MAINTENANCE,Coût de Maintenance par Unité Produite,Relier les dépenses de maintenance à la production.,Couts Totaux de Maintenance/Nombre Total d’Unites Produites \ No newline at end of file