diff --git a/k8s/vertex/k8s-aks/airqo-vertex/values-prod.yaml b/k8s/vertex/k8s-aks/airqo-vertex/values-prod.yaml index 8af974f43f..578a8ef7fc 100644 --- a/k8s/vertex/k8s-aks/airqo-vertex/values-prod.yaml +++ b/k8s/vertex/k8s-aks/airqo-vertex/values-prod.yaml @@ -1,7 +1,7 @@ replicaCount: 2 image: repository: airqoacr.azurecr.io/airqo-vertex - tag: prod-41d702ea-1782208434 + tag: prod-0c766198-1782669793 pullPolicy: IfNotPresent imagePullSecrets: [] nameOverride: '' diff --git a/k8s/vertex/k8s-aks/airqo-vertex/values-stage.yaml b/k8s/vertex/k8s-aks/airqo-vertex/values-stage.yaml index f60b224157..8fcea337cc 100644 --- a/k8s/vertex/k8s-aks/airqo-vertex/values-stage.yaml +++ b/k8s/vertex/k8s-aks/airqo-vertex/values-stage.yaml @@ -1,7 +1,7 @@ replicaCount: 2 image: repository: airqoacr.azurecr.io/airqo-stage-vertex - tag: stage-f7402cd5-1782647967 + tag: stage-7b984e41-1782670087 pullPolicy: IfNotPresent imagePullSecrets: [] nameOverride: '' diff --git a/src/internal-docs/.env.example b/src/internal-docs/.env.example deleted file mode 100644 index fcadf84287..0000000000 --- a/src/internal-docs/.env.example +++ /dev/null @@ -1,43 +0,0 @@ -# ------------------------------------------------------------------------- -# BookStack Environment Configuration -# ------------------------------------------------------------------------- -# Copy this file to `.env` before running `docker compose up -d`. -# Do not commit the `.env` file to version control. -# ------------------------------------------------------------------------- - -# The base URL of your BookStack installation. -# Must include the protocol (http:// or https://) -APP_URL=http://localhost:6875 - -# The name of the application (Shows in the header and emails) -APP_NAME="Mariana Deep" - -# Disable public access to enforce authentication for all pages -# Set to 'true' if you want anonymous users to read documentation -APP_PUBLIC=false - -# Application Key (Required for sessions and encryption) -# Generate a new one for production using: docker run --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:latest appkey -APP_KEY= - -# ------------------------------------------------------------------------- -# Database Configuration -# ------------------------------------------------------------------------- -# Ensure these match the values used in docker-compose.yml. -DB_HOST=bookstack_db -DB_DATABASE=bookstackapp -DB_USER=bookstack -DB_PASSWORD=your_secure_db_password -DB_ROOT_PASSWORD=your_secure_root_password - -# ------------------------------------------------------------------------- -# Mail Settings (Optional but recommended for user invitations/resets) -# ------------------------------------------------------------------------- -# MAIL_DRIVER=smtp -# MAIL_HOST=smtp.mailtrap.io -# MAIL_PORT=2525 -# MAIL_USERNAME=null -# MAIL_PASSWORD=null -# MAIL_ENCRYPTION=null -# MAIL_FROM=docs@yourdomain.com -# MAIL_FROM_NAME="Internal Docs" diff --git a/src/internal-docs/.gitignore b/src/internal-docs/.gitignore deleted file mode 100644 index 41813ebc80..0000000000 --- a/src/internal-docs/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Ignore environment variables file -.env - -# Ignore Docker volumes for BookStack application state (uploads, themes, etc.) -config/ - -# Ignore Docker volumes for MariaDB database files -db_data/ -db_data_v2/ diff --git a/src/internal-docs/README.md b/src/internal-docs/README.md deleted file mode 100644 index 4b8fa284e6..0000000000 --- a/src/internal-docs/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Mariana Deep - Internal Documentation - -This repository contains the containerized environment and Kubernetes Helm charts for **Mariana Deep**, AirQo's internal documentation platform powered by [BookStack](https://www.bookstackapp.com/). - -## Table of Contents -- [Local Setup & Testing](#local-setup--testing) -- [Kubernetes Deployment](#kubernetes-deployment) -- [Creating & Editing Documentation](#creating--editing-documentation) -- [Information Architecture](#information-architecture-product-focused) -- [Contributing Guidelines](#contributing-guidelines) -- [Security & Data Persistence](#security--data-persistence) - -## Local Setup & Testing - -To spin up the documentation platform locally for testing or local development: - -1. **Configure the Environment** - Duplicate the provided example environment file: - - *macOS / Linux:* - ```bash - cp .env.example .env - ``` - *Windows (Command Prompt / PowerShell):* - ```cmd - copy .env.example .env - ``` - -2. **Start the Containers** - Use Docker Compose to build and start the environment in the background: - ```bash - docker compose up -d - ``` - -3. **Access the Application** - Wait approximately 20 seconds for database migrations to complete. - Open your browser and navigate to `http://localhost:6875`. - *(Default credentials on first run: `admin@admin.com` / `password`)* - -## Kubernetes Deployment - -The project uses Helm charts located in the `k8s/` directory to deploy to our production cluster. - -### Environments -- **Production**: Accessible at `https://platform.airqo.net/mariana` - -### Deployment Commands -To deploy or upgrade the cluster, run the following Helm commands from the root of this project: - -**Production:** -```bash -helm upgrade --install airqo-internal-docs k8s/ -f k8s/values-prod.yaml -n production -``` - -## Creating & Editing Documentation - -BookStack features an intuitive WYSIWYG editor, making it incredibly easy for any technical contributor to document features, APIs, and runbooks without fighting markdown syntax (though Markdown is fully supported!). - -### How to Create Content -1. **Navigate to the appropriate Book**: Look at the Information Architecture below to find exactly where your document belongs. -2. **Add a Chapter or Page**: Click the "New Page" or "New Chapter" button in the right sidebar menu. -3. **Use the Editor**: Write your documentation. You can drag-and-drop images directly into the editor. If you prefer raw markdown, you can switch the editor type in your user settings. -4. **Link Internal Content**: Use the "Link" button to easily search and interlink to other existing internal documents. - -### Best Practices for Documentation -- **Keep it Concise**: Use bullet points, clear headings, and avoid massive walls of text. Scannability is key. -- **Keep it Updated**: The Boy Scout Rule applies—if you change a system's behavior, update its documentation in the exact same sprint. -- **Use Code Blocks**: When documenting CLI commands, scripts, or JSON payloads, use properly formatted code blocks. -- **Tagging**: Use tags on your pages (e.g., `api`, `deprecated`, `v2`) to make them highly searchable across the platform. - -## Information Architecture (Product-Focused) - -We organize all of our documentation using a **Product-focused approach**. Whether it's a mobile app, a microservice, or a data pipeline, every major initiative is treated as a Product. Strictly adhere to the following hierarchy: - -1. **Shelves (High-Level Domains)**: Groups related products under a larger domain or suite. - *(Examples: AirQo Platform, Data Pipelines, Mobile Apps, Infrastructure)* -2. **Books (The Products)**: Represents a specific Product, Microservice, or distinct Tool. - *(Examples: Netmanager, Beacon, AirQo API, Ingestion Pipeline)* -3. **Chapters (Major Components)**: Groups related documentation topics within a specific Product. - *(Examples: Architecture, API Reference, Deployment Guides, Runbooks)* -4. **Pages (Specific Documents)**: The granular, individual pieces of documentation. - *(Examples: Authentication Flow Diagram, GET /v1/devices, How to run the calibration script)* - -**Example Flow:** -`Shelves (AirQo Platform) -> Book (Netmanager) -> Chapter (API Reference) -> Page (Authentication Endpoint)` - -## Contributing Guidelines - -We encourage all engineers to actively contribute to Mariana Deep. If you see something wrong, fix it! - -- **Typos and Minor Fixes**: Fix them immediately using the BookStack web interface. No review needed. -- **Major Architectural Documentation**: Draft the documentation in a Page and request a review (via Slack or email link) from the Lead Engineer of that specific product before finalizing. -- **Infrastructure Changes**: If you are modifying the Helm charts or Docker Compose configurations in this repository, please create a feature branch, test locally using Docker Compose, and open a standard Pull Request on GitHub. - -## Security & Data Persistence - -- **Public Access**: Explicitly disabled (`APP_PUBLIC=false`). Unauthenticated users cannot view any pages and will be immediately redirected to the login screen. -- **User Management**: Open registration is disabled. Administrators manually provision access. -- **Data Persistence**: In Kubernetes, Persistent Volume Claims (PVCs) ensure uploaded images and database state survive pod restarts. Locally, BookStack configuration and uploads are stored in the bind-mounted `./config` directory, while database data persists in the Docker-managed `bookstack_db_data` volume. diff --git a/src/internal-docs/docker-compose.yml b/src/internal-docs/docker-compose.yml deleted file mode 100644 index c07c985d64..0000000000 --- a/src/internal-docs/docker-compose.yml +++ /dev/null @@ -1,52 +0,0 @@ -version: "3.8" - -services: - bookstack: - image: lscr.io/linuxserver/bookstack:24.05.2 - container_name: bookstack - environment: - - PUID=1000 - - PGID=1000 - - TZ=UTC - - APP_NAME=${APP_NAME:-"Mariana Deep"} - - APP_URL=${APP_URL:-http://localhost:6875} - - APP_KEY=${APP_KEY:-} - - DB_HOST=bookstack_db - - DB_PORT=3306 - - DB_USER=${DB_USER:-bookstack} - - DB_PASS=${DB_PASSWORD:-bookstack_secure_password} - - DB_DATABASE=${DB_DATABASE:-bookstackapp} - # Security & Access Control phase settings: - - APP_PUBLIC=${APP_PUBLIC:-false} # Disable public viewing by default - volumes: - - ./config:/config - ports: - - "6875:80" - restart: unless-stopped - depends_on: - - bookstack_db - networks: - - internal-docs-network - - bookstack_db: - image: mariadb:10.6 - container_name: bookstack_db - environment: - - TZ=UTC - - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-root_secure_password} - - MYSQL_DATABASE=${DB_DATABASE:-bookstackapp} - - MYSQL_USER=${DB_USER:-bookstack} - - MYSQL_PASSWORD=${DB_PASSWORD:-bookstack_secure_password} - volumes: - - bookstack_db_data:/var/lib/mysql - restart: unless-stopped - networks: - - internal-docs-network - -networks: - internal-docs-network: - name: internal-docs-network - driver: bridge - -volumes: - bookstack_db_data: diff --git a/src/internal-docs/k8s/Chart.yaml b/src/internal-docs/k8s/Chart.yaml deleted file mode 100644 index 1210342f67..0000000000 --- a/src/internal-docs/k8s/Chart.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v2 -appVersion: "24.05.2" -description: AirQo Internal Docs (Mariana Deep) Helm Chart -name: airqo-internal-docs -home: https://airqo.net -version: 0.1.0 -maintainers: - - name: AirQo - email: support@airqo.net - url: https://airqo.net diff --git a/src/internal-docs/k8s/templates/_helpers.tpl b/src/internal-docs/k8s/templates/_helpers.tpl deleted file mode 100644 index 75f1964af3..0000000000 --- a/src/internal-docs/k8s/templates/_helpers.tpl +++ /dev/null @@ -1,49 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "internal-docs.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -*/}} -{{- define "internal-docs.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "internal-docs.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "internal-docs.labels" -}} -helm.sh/chart: {{ include "internal-docs.chart" . }} -{{ include "internal-docs.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "internal-docs.selectorLabels" -}} -app.kubernetes.io/name: {{ include "internal-docs.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} diff --git a/src/internal-docs/k8s/templates/configmap.yaml b/src/internal-docs/k8s/templates/configmap.yaml deleted file mode 100644 index ee4e549c61..0000000000 --- a/src/internal-docs/k8s/templates/configmap.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Values.app.configmap }} - namespace: {{ .Values.app.namespace }} - labels: - {{- include "internal-docs.labels" . | nindent 4 }} -data: - # BookStack Specific - APP_NAME: {{ .Values.env.APP_NAME | quote }} - APP_PUBLIC: {{ .Values.env.APP_PUBLIC | quote }} - - # Shared Database Configuration (Used by BookStack container) - DB_USER: {{ .Values.env.DB_USER | quote }} - DB_DATABASE: {{ .Values.env.DB_DATABASE | quote }} - - # MariaDB Specific (Used by MariaDB container) - MYSQL_USER: {{ .Values.env.DB_USER | quote }} - MYSQL_DATABASE: {{ .Values.env.DB_DATABASE | quote }} diff --git a/src/internal-docs/k8s/templates/deployment-app.yaml b/src/internal-docs/k8s/templates/deployment-app.yaml deleted file mode 100644 index e41a9f4441..0000000000 --- a/src/internal-docs/k8s/templates/deployment-app.yaml +++ /dev/null @@ -1,60 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.app.name }} - namespace: {{ .Values.app.namespace }} - labels: - {{- include "internal-docs.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - app: {{ .Values.app.label }} - template: - metadata: - labels: - app: {{ .Values.app.label }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: {{ .Values.app.label }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.targetPort }} - protocol: TCP - env: - - name: DB_HOST - value: {{ .Values.dbApp.name }} - - name: APP_URL - value: {{ .Values.app.url | quote }} - envFrom: - - configMapRef: - name: {{ .Values.app.configmap }} - - secretRef: - name: {{ .Values.app.configmap }}-secret - volumeMounts: - - name: bookstack-config - mountPath: /config - resources: - {{- toYaml .Values.resources | nindent 12 }} - volumes: - - name: bookstack-config - persistentVolumeClaim: - claimName: {{ .Values.app.name }}-pvc - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/src/internal-docs/k8s/templates/deployment-db.yaml b/src/internal-docs/k8s/templates/deployment-db.yaml deleted file mode 100644 index 9060bedd09..0000000000 --- a/src/internal-docs/k8s/templates/deployment-db.yaml +++ /dev/null @@ -1,75 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Values.dbApp.name }} - namespace: {{ .Values.app.namespace }} - labels: - {{- include "internal-docs.labels" . | nindent 4 }} -spec: - replicas: 1 - selector: - matchLabels: - app: {{ .Values.dbApp.label }} - template: - metadata: - labels: - app: {{ .Values.dbApp.label }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: {{ .Values.dbApp.label }} - image: "{{ .Values.dbImage.repository }}:{{ .Values.dbImage.tag }}" - imagePullPolicy: {{ .Values.dbImage.pullPolicy }} - ports: - - name: mysql - containerPort: {{ .Values.dbService.targetPort }} - protocol: TCP - envFrom: - - configMapRef: - name: {{ .Values.app.configmap }} - - secretRef: - name: {{ .Values.app.configmap }}-secret - volumeMounts: - - name: mariadb-config - mountPath: /var/lib/mysql - livenessProbe: - exec: - command: - - sh - - -c - - mysqladmin ping -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - failureThreshold: 3 - readinessProbe: - exec: - command: - - sh - - -c - - mysql -h 127.0.0.1 -uroot -p"$MYSQL_ROOT_PASSWORD" -e 'SELECT 1' - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 3 - failureThreshold: 2 - resources: - {{- toYaml .Values.dbResources | nindent 12 }} - volumes: - - name: mariadb-config - persistentVolumeClaim: - claimName: {{ .Values.dbApp.name }}-pvc - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/src/internal-docs/k8s/templates/hpa.yaml b/src/internal-docs/k8s/templates/hpa.yaml deleted file mode 100644 index 9803305c14..0000000000 --- a/src/internal-docs/k8s/templates/hpa.yaml +++ /dev/null @@ -1,23 +0,0 @@ -{{- if .Values.autoscaling }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ .Values.app.name }} - namespace: {{ .Values.app.namespace }} - labels: - {{- include "internal-docs.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ .Values.app.name }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} -{{- end }} diff --git a/src/internal-docs/k8s/templates/pvc.yaml b/src/internal-docs/k8s/templates/pvc.yaml deleted file mode 100644 index 7139a8a19a..0000000000 --- a/src/internal-docs/k8s/templates/pvc.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ .Values.app.name }}-pvc - namespace: {{ .Values.app.namespace }} -spec: - accessModes: - - ReadWriteOnce - {{- if .Values.persistence.storageClass }} - storageClassName: {{ .Values.persistence.storageClass }} - {{- end }} - resources: - requests: - storage: {{ .Values.persistence.size }} ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ .Values.dbApp.name }}-pvc - namespace: {{ .Values.app.namespace }} -spec: - accessModes: - - ReadWriteOnce - {{- if .Values.dbPersistence.storageClass }} - storageClassName: {{ .Values.dbPersistence.storageClass }} - {{- end }} - resources: - requests: - storage: {{ .Values.dbPersistence.size }} diff --git a/src/internal-docs/k8s/templates/secret.yaml b/src/internal-docs/k8s/templates/secret.yaml deleted file mode 100644 index c2e4d895aa..0000000000 --- a/src/internal-docs/k8s/templates/secret.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Values.app.configmap }}-secret - namespace: {{ .Values.app.namespace }} - labels: - {{- include "internal-docs.labels" . | nindent 4 }} -type: Opaque -stringData: - APP_KEY: {{ required "env.APP_KEY must be set (inject via CI/CD/Secrets)" .Values.env.APP_KEY | quote }} - DB_PASS: {{ required "env.DB_PASSWORD must be set" .Values.env.DB_PASSWORD | quote }} - DB_PASSWORD: {{ .Values.env.DB_PASSWORD | quote }} - MYSQL_PASSWORD: {{ .Values.env.DB_PASSWORD | quote }} - MYSQL_ROOT_PASSWORD: {{ required "env.MYSQL_ROOT_PASSWORD must be set" .Values.env.MYSQL_ROOT_PASSWORD | quote }} diff --git a/src/internal-docs/k8s/templates/service-app.yaml b/src/internal-docs/k8s/templates/service-app.yaml deleted file mode 100644 index 594d4a3ccb..0000000000 --- a/src/internal-docs/k8s/templates/service-app.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ .Values.app.name }} - namespace: {{ .Values.app.namespace }} - labels: - {{- include "internal-docs.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: {{ .Values.service.targetPort }} - protocol: TCP - name: http - {{- if eq .Values.service.type "NodePort" }} - nodePort: {{ .Values.service.nodePort }} - {{- end }} - selector: - app: {{ .Values.app.label }} diff --git a/src/internal-docs/k8s/templates/service-db.yaml b/src/internal-docs/k8s/templates/service-db.yaml deleted file mode 100644 index 034b9a85b1..0000000000 --- a/src/internal-docs/k8s/templates/service-db.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ .Values.dbApp.name }} - namespace: {{ .Values.app.namespace }} - labels: - {{- include "internal-docs.labels" . | nindent 4 }} -spec: - type: {{ .Values.dbService.type }} - ports: - - port: {{ .Values.dbService.port }} - targetPort: {{ .Values.dbService.targetPort }} - protocol: TCP - name: mysql - selector: - app: {{ .Values.dbApp.label }} diff --git a/src/internal-docs/k8s/values-prod.yaml b/src/internal-docs/k8s/values-prod.yaml deleted file mode 100644 index 0593a9bbdc..0000000000 --- a/src/internal-docs/k8s/values-prod.yaml +++ /dev/null @@ -1,91 +0,0 @@ -replicaCount: 2 -image: - repository: lscr.io/linuxserver/bookstack - tag: 24.05.2 - pullPolicy: IfNotPresent -dbImage: - repository: mariadb - tag: 10.6 - pullPolicy: IfNotPresent -imagePullSecrets: [] -nameOverride: '' -fullnameOverride: '' - -service: - type: NodePort - port: 6875 - targetPort: 80 - nodePort: 31197 # Unique NodePort for production - -dbService: - type: ClusterIP - port: 3306 - targetPort: 3306 - -ingress: - enabled: false - -resources: - limits: - cpu: 500m - memory: 1Gi - requests: - cpu: 100m - memory: 512Mi - -dbResources: - limits: - cpu: 500m - memory: 1Gi - requests: - cpu: 100m - memory: 512Mi - -autoscaling: - minReplicas: 2 - maxReplicas: 5 - targetCPUUtilizationPercentage: 80 - -nodeSelector: - role: high-mem - -tolerations: [] - -affinity: - nodeAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - preference: - matchExpressions: - - key: role - operator: In - values: - - high-mem - weight: 1 - -app: - name: airqo-prod-internal-docs - label: prod-internal-docs - namespace: production - configmap: env-internal-docs-prod - url: "https://platform.airqo.net/mariana" - -dbApp: - name: airqo-prod-internal-docs-db - label: prod-internal-docs-db - -persistence: - size: 5Gi - storageClass: "fast-ssd" - -dbPersistence: - size: 20Gi - storageClass: "fast-ssd" - -env: - APP_NAME: "Mariana Deep" - APP_KEY: "" # Must be securely injected via CI/CD or secrets management - APP_PUBLIC: "false" - DB_USER: "bookstack" - DB_PASSWORD: "" # Must be securely injected - DB_DATABASE: "bookstackapp" - MYSQL_ROOT_PASSWORD: "" # Must be securely injected diff --git a/src/vertex/app/changelog.md b/src/vertex/app/changelog.md index 4154de352f..54e37c4f23 100644 --- a/src/vertex/app/changelog.md +++ b/src/vertex/app/changelog.md @@ -2,6 +2,58 @@ > **Note**: This changelog consolidates all recent improvements, features, and fixes to the AirQo Vertex frontend. +## Version 2.0.8 +**Released:** June 28, 2026 + +### Comprehensive Unit Testing & CI Integration + +Introduced a robust unit testing infrastructure using Vitest and React Testing Library, alongside comprehensive CI/CD pipeline integration. This release focuses on improving code quality, ensuring robust component rendering, and validating API proxy behavior through extensive boundary testing. + +
+Test Infrastructure & CI Integration (3) + +- **Vitest & React Testing Library**: Replaced legacy test runners with a fast, modern Vitest setup configured to work seamlessly with Next.js App Router and absolute imports. +- **CI/CD Pipeline**: Added dedicated GitHub Actions workflows for automated type checking (`tsc --noEmit`) and unit testing (`vitest run --coverage`) on pull requests. +- **Testing Guidelines**: Introduced `TESTING.md` internal documentation to standardize component rendering, user-event interactions, and mocking strategies across the team. + +
+ +
+UI & Shared Component Testing (3) + +- **Core UI Components**: Added comprehensive tests for 30+ shadcn/radix UI components (Dialogs, Accords, Tooltips, Buttons, Inputs, Forms, etc.) verifying accessibility and interaction states without snapshot testing. +- **Shared Abstractions**: Extracted and tested shared wrappers including `ReusableTable`, `ReusableSelectInput`, `ReusableButton`, and `ReusableFileUpload`, ensuring robust prop forwarding and DOM nesting compliance. +- **Warning Resolutions**: Proactively fixed React DOM nesting warnings (e.g., `` cannot appear as a descendant of ``) and accessibility label warnings across components during test implementation. + +
+ +
+Service & API Boundary Testing (2) + +- **Mock Factories**: Created `apiResponseFactory.ts` and NextAuth session mocks to simulate full HTTP environments locally without hitting live staging endpoints or needing `axios-mock-adapter`. +- **Proxy Validation**: Validated both the Next.js App Router API handlers (`proxyClient`) and client-side Axios instances (`secureApiProxyClient`), verifying `JWT` vs `API_TOKEN` injection, error normalization, and custom event dispatching (`vertex-network-degraded`, `auth-token-expired`). + +
+ +
+Files Modified & Added (40+) + +- `package.json` [MODIFIED] +- `vitest.config.ts` [NEW] +- `.github/workflows/test.yml` [NEW] +- `src/vertex/app/_docs/internal/TESTING.md` [NEW] +- `src/vertex/components/ui/*.test.tsx` (30+ files) [NEW] +- `src/vertex/components/shared/**/*.test.tsx` [NEW] +- `src/vertex/core/services/*.test.ts` [NEW] +- `src/vertex/core/utils/*.test.ts` [NEW] +- `src/vertex/test/factories/apiResponseFactory.ts` [NEW] +- `src/vertex/test/mocks/nextAuth.ts` [NEW] +- `src/vertex/vitest.setup.ts` [NEW] + +
+ +--- + ## Version 2.0.7 **Released:** June 17, 2026 diff --git a/src/vertex/components/shared/button/ReusableButton.test.tsx b/src/vertex/components/shared/button/ReusableButton.test.tsx new file mode 100644 index 0000000000..a29a4b26f3 --- /dev/null +++ b/src/vertex/components/shared/button/ReusableButton.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import ReusableButton from "./ReusableButton"; + +describe("ReusableButton", () => { + it("renders as a button by default", () => { + render(Click me); + expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument(); + }); + + it("renders as an anchor when path is provided", () => { + render(Go Home); + expect(screen.getByRole("link", { name: "Go Home" })).toBeInTheDocument(); + }); + + it("handles loading state", () => { + render(Submit); + const button = screen.getByRole("button", { name: "Submit" }); + expect(button).toHaveAttribute("aria-busy", "true"); + expect(button).toBeDisabled(); + }); + + it("renders tooltip span when disabled with permission", () => { + render(Action); + const actionText = screen.getByText("Action"); + expect(actionText).toBeInTheDocument(); + }); +}); diff --git a/src/vertex/components/shared/card/CardWrapper.test.tsx b/src/vertex/components/shared/card/CardWrapper.test.tsx new file mode 100644 index 0000000000..e9507e3a31 --- /dev/null +++ b/src/vertex/components/shared/card/CardWrapper.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import userEvent from "@testing-library/user-event"; +import Card from "./CardWrapper"; + +describe("CardWrapper", () => { + it("renders with basic content", () => { + render(Card Content); + expect(screen.getByText("Card Content")).toBeInTheDocument(); + }); + + it("renders with headers and footers", () => { + render( + Header} footer={
Footer
}> + Content +
+ ); + expect(screen.getByTestId("header")).toBeInTheDocument(); + expect(screen.getByTestId("footer")).toBeInTheDocument(); + }); + + it("handles clicks", async () => { + const handleClick = vi.fn(); + render(Content); + await userEvent.click(screen.getByTestId("clickable-card")); + expect(handleClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/vertex/components/shared/fileupload/ReusableFileUpload.test.tsx b/src/vertex/components/shared/fileupload/ReusableFileUpload.test.tsx new file mode 100644 index 0000000000..9b54d69065 --- /dev/null +++ b/src/vertex/components/shared/fileupload/ReusableFileUpload.test.tsx @@ -0,0 +1,27 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import ReusableFileUpload from "./ReusableFileUpload"; + +describe("ReusableFileUpload", () => { + it("renders with placeholder and label", () => { + render(); + expect(screen.getByLabelText("Upload File")).toBeInTheDocument(); + expect(screen.getByText("Upload file")).toBeInTheDocument(); // default placeholder + }); + + it("handles file selection", () => { + const handleChange = vi.fn(); + render(); + + const file = new File(["hello"], "hello.png", { type: "image/png" }); + const input = screen.getByLabelText("Upload"); + + fireEvent.change(input, { target: { files: [file] } }); + expect(handleChange).toHaveBeenCalledWith(file); + }); + + it("shows error state", () => { + render(); + expect(screen.getByText("File too large")).toBeInTheDocument(); + }); +}); diff --git a/src/vertex/components/shared/inputfield/ReusableInputField.test.tsx b/src/vertex/components/shared/inputfield/ReusableInputField.test.tsx new file mode 100644 index 0000000000..9a5eca5fb2 --- /dev/null +++ b/src/vertex/components/shared/inputfield/ReusableInputField.test.tsx @@ -0,0 +1,60 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import userEvent from "@testing-library/user-event"; +import ReusableInputField from "./ReusableInputField"; + +// Mock toast to avoid missing canvas/window functions in jsdom sometimes or to just isolate +vi.mock("../toast/ReusableToast", () => ({ + default: vi.fn(), +})); + +// Mock clipboard +Object.assign(navigator, { + clipboard: { + writeText: vi.fn(), + }, +}); + +describe("ReusableInputField", () => { + it("renders basic input", () => { + render(); + expect(screen.getByLabelText("Email")).toBeInTheDocument(); + expect(screen.getByPlaceholderText("Enter email")).toBeInTheDocument(); + }); + + it("shows error message", () => { + render(); + expect(screen.getByText("Invalid email")).toBeInTheDocument(); + }); + + it("toggles password visibility", async () => { + render(); + const input = screen.getByLabelText("Password"); + expect(input).toHaveAttribute("type", "password"); + + const toggleBtn = screen.getByRole("button", { name: /show password/i }); + await userEvent.click(toggleBtn); + expect(input).toHaveAttribute("type", "text"); + + await userEvent.click(screen.getByRole("button", { name: /hide password/i })); + expect(input).toHaveAttribute("type", "password"); + }); + + it("calls custom action", async () => { + const handleAction = vi.fn(); + const MockIcon = () =>
; + + render( + + ); + + const actionBtn = screen.getByRole("button", { name: "Do search" }); + await userEvent.click(actionBtn); + expect(handleAction).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/vertex/components/shared/select/ReusableSelectInput.test.tsx b/src/vertex/components/shared/select/ReusableSelectInput.test.tsx new file mode 100644 index 0000000000..28a877149e --- /dev/null +++ b/src/vertex/components/shared/select/ReusableSelectInput.test.tsx @@ -0,0 +1,60 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import userEvent from "@testing-library/user-event"; +import ReusableSelectInput from "./ReusableSelectInput"; + +window.HTMLElement.prototype.scrollIntoView = vi.fn(); + +describe("ReusableSelectInput", () => { + it("renders placeholder correctly", () => { + render( + + + + ); + + const button = screen.getByRole("button"); + expect(button).toBeInTheDocument(); + expect(screen.getByText("Pick fruit")).toBeInTheDocument(); + }); + + it("shows options and handles selection", async () => { + const handleChange = vi.fn(); + render( + + + + + ); + + const button = screen.getByRole("button"); + await userEvent.click(button); + + expect(screen.getByRole("listbox")).toBeInTheDocument(); + const bananaOption = screen.getByText("Banana"); + + await userEvent.click(bananaOption); + + expect(handleChange).toHaveBeenCalledWith( + expect.objectContaining({ + target: expect.objectContaining({ value: "banana" }) + }) + ); + }); + + it("handles keyboard navigation", async () => { + render( + + + + + ); + + const button = screen.getByRole("button"); + button.focus(); + + // Open with arrow down + fireEvent.keyDown(button, { key: "ArrowDown" }); + expect(screen.getByRole("listbox")).toBeInTheDocument(); + }); +}); diff --git a/src/vertex/components/shared/table/ReusableTable.test.tsx b/src/vertex/components/shared/table/ReusableTable.test.tsx new file mode 100644 index 0000000000..d0689573e5 --- /dev/null +++ b/src/vertex/components/shared/table/ReusableTable.test.tsx @@ -0,0 +1,35 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import ReusableTable from "./ReusableTable"; + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ replace: vi.fn() }), + usePathname: () => "/test-path", + useSearchParams: () => new URLSearchParams(), +})); + +vi.mock("@/context/banner-context", () => ({ + useBanner: () => ({ showBanner: vi.fn(), hideBanner: vi.fn() }), +})); + +describe("ReusableTable", () => { + it("renders with empty data", () => { + render(); + expect(screen.getByText("Test Table")).toBeInTheDocument(); + expect(screen.getByText("No data available")).toBeInTheDocument(); + }); + + it("renders data correctly", () => { + const columns = [ + { key: "name", render: (val: unknown) => String(val) }, + ]; + const data = [ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" } + ]; + + render(); + expect(screen.getByText("Alice")).toBeInTheDocument(); + expect(screen.getByText("Bob")).toBeInTheDocument(); + }); +}); diff --git a/src/vertex/components/shared/table/ReusableTable.tsx b/src/vertex/components/shared/table/ReusableTable.tsx index caa9e8835c..2b6c25c752 100644 --- a/src/vertex/components/shared/table/ReusableTable.tsx +++ b/src/vertex/components/shared/table/ReusableTable.tsx @@ -1472,7 +1472,7 @@ const ReusableTable = ({ )} {/* Add CSS to hide scrollbar on thead */} -