').text(text).html();
+ let highlighted = escapedText;
+ // Sort indices by start position (descending) to avoid offset issues
+ const sortedIndices = indices.slice().sort((a, b) => b[0] - a[0]);
+ sortedIndices.forEach(([start, end]) => {
+ const before = highlighted.substring(0, start);
+ const match = highlighted.substring(start, end + 1);
+ const after = highlighted.substring(end + 1);
+ highlighted = before + '
' + match + '' + after;
+ });
+ return highlighted;
+ }
+
+ $targetSearchInput.on('shown.bs.popover', () => {
+ $('.search-result-close-button').on('click', () => {
+ $targetSearchInput.val('');
+ $targetSearchInput.trigger('change');
+ });
+ });
+
+ $targetSearchInput
+ .data('content', $html[0].outerHTML)
+ .popover('show');
+ };
+ });
+})(jQuery);
+
diff --git a/assets/json/offline-search-index.json b/assets/json/offline-search-index.json
new file mode 100644
index 00000000..20cfd9f3
--- /dev/null
+++ b/assets/json/offline-search-index.json
@@ -0,0 +1,146 @@
+{{- $.Scratch.Add "offline-search-index" slice -}}
+{{- range where .Site.AllPages ".Params.exclude_search" "!=" true -}}
+{{/* Enhanced search index with comprehensive content extraction for better searchability */}}
+{{- $title := .Title | default "" -}}
+{{- $description := .Description | default .Summary | default "" -}}
+{{- $content := .Plain | htmlUnescape | default "" -}}
+{{- $categories := .Params.categories | default (slice) -}}
+{{- $tags := .Params.tags | default (slice) -}}
+{{- $section := .Section | default "" -}}
+{{- $type := .Type | default "" -}}
+{{- $excerpt := ($description | default $content) | htmlUnescape | truncate (.Site.Params.offlineSearchSummaryLength | default 150) | htmlUnescape -}}
+{{- $keywords := .Params.keywords | default (slice) -}}
+
+{{- /* Extract headings from Table of Contents for better keyword matching */}}
+{{- $headingText := "" -}}
+{{- if .TableOfContents -}}
+{{- $headingMatches := findRE `>([^<]+)` .TableOfContents -}}
+{{- range $headingMatches -}}
+{{- $headingText = printf "%s %s" $headingText (. | replaceRE `>` "" | replaceRE `
+ ```
+
+2. **Docker access to registry**
+ ```bash
+ docker login registry.ci.openshift.org
+ ```
+
+3. **Required tools**
+ - `oc` (OpenShift CLI)
+ - `docker` or `podman`
+ - Access to `dmistry` namespace on build01
+
+## Quick Deploy
+
+```bash
+# Make the script executable
+chmod +x deploy/deploy.sh
+
+# Run the deployment script
+./deploy/deploy.sh
+```
+
+The script will:
+1. Build the Docker image
+2. Push it to `registry.ci.openshift.org/dmistry/ci-docs-test:latest`
+3. Create/verify the namespace
+4. Deploy the application
+5. Display the route URL
+
+## Manual Deployment
+
+If you prefer to deploy manually:
+
+### 1. Build and Push Image
+
+```bash
+docker build -t registry.ci.openshift.org/dmistry/ci-docs-test:latest -f Dockerfile .
+docker push registry.ci.openshift.org/dmistry/ci-docs-test:latest
+```
+
+### 2. Apply Manifests
+
+```bash
+oc apply -f deploy/test-deployment.yaml
+```
+
+### 3. Check Status
+
+```bash
+# Check pods
+oc get pods -n dmistry
+
+# Check route
+oc get route -n dmistry
+
+# View logs
+oc logs -n dmistry -l app=ci-docs-test
+```
+
+## Accessing the Deployment
+
+After deployment, get the route URL:
+
+```bash
+oc get route ci-docs-test -n dmistry -o jsonpath='{.spec.host}'
+```
+
+The site will be available at: `https://
`
+
+## Differences from Production
+
+This test deployment:
+- Uses namespace `dmistry` (not the production namespace)
+- Uses service name `ci-docs-test` (not `ci-docs`)
+- Uses different route name to avoid conflicts
+- Uses a different image name/tag
+- Has minimal resources (1 replica, lower resource limits)
+
+## Troubleshooting
+
+### Image Build Fails
+
+```bash
+# Check Dockerfile
+cat Dockerfile
+
+# Build locally to test
+docker build -t test-build -f Dockerfile .
+```
+
+### Image Push Fails
+
+```bash
+# Verify registry login
+docker login registry.ci.openshift.org
+
+# Check permissions
+oc whoami
+```
+
+### Deployment Not Ready
+
+```bash
+# Check pod status
+oc get pods -n dmistry
+
+# Check pod logs
+oc logs -n dmistry -l app=ci-docs-test
+
+# Check events
+oc get events -n dmistry --sort-by='.lastTimestamp'
+```
+
+### Route Not Accessible
+
+```bash
+# Check route
+oc get route -n dmistry
+
+# Check service
+oc get svc -n dmistry
+
+# Check endpoints
+oc get endpoints -n dmistry
+```
+
+## Cleanup
+
+To remove the test deployment:
+
+```bash
+oc delete -f deploy/test-deployment.yaml
+```
+
+Or delete individual resources:
+
+```bash
+oc delete deployment ci-docs-test -n dmistry
+oc delete service ci-docs-test -n dmistry
+oc delete route ci-docs-test -n dmistry
+```
+
+## Updating the Deployment
+
+To update after making changes:
+
+1. Make your code changes
+2. Rebuild and push the image:
+ ```bash
+ docker build -t registry.ci.openshift.org/dmistry/ci-docs-test:latest -f Dockerfile .
+ docker push registry.ci.openshift.org/dmistry/ci-docs-test:latest
+ ```
+3. Restart the deployment:
+ ```bash
+ oc rollout restart deployment/ci-docs-test -n dmistry
+ ```
+
+Or use the deployment script again - it will rebuild and redeploy.
+
diff --git a/deploy/REBUILD_AND_SCALE.md b/deploy/REBUILD_AND_SCALE.md
new file mode 100644
index 00000000..01b62450
--- /dev/null
+++ b/deploy/REBUILD_AND_SCALE.md
@@ -0,0 +1,243 @@
+# Rebuild and Scale Commands
+
+Quick reference for rebuilding and scaling the `ci-docs-test` deployment.
+
+## Configuration
+
+```bash
+NAMESPACE="dmistry"
+IMAGE_NAME="ci-docs-test"
+CLUSTER="build01"
+OC_CMD="oc --context ${CLUSTER} --as system:admin"
+```
+
+## Rebuild Commands
+
+### Option 1: Rebuild from Local Source (Current Directory)
+
+This is the fastest way to rebuild with your current local changes:
+
+```bash
+# From the repository root directory
+cd /home/dmistry/CI/ci-docs
+
+# Ensure git submodules are initialized
+git submodule update --init --recursive || echo "Submodules already initialized"
+
+# Trigger a new build from current directory
+oc --context build01 --as system:admin start-build ci-docs-test --from-dir=. -n dmistry --follow
+
+# Or without following (build runs in background)
+oc --context build01 --as system:admin start-build ci-docs-test --from-dir=. -n dmistry
+```
+
+### Option 2: Rebuild from Specific Commit
+
+```bash
+# Checkout the commit you want to build
+git checkout
+
+# Ensure submodules are initialized
+git submodule update --init --recursive
+
+# Trigger build from current directory
+oc --context build01 --as system:admin start-build ci-docs-test --from-dir=. -n dmistry --follow
+```
+
+### Option 3: Rebuild from PR Branch
+
+```bash
+# Fetch and checkout the PR branch
+git fetch origin pull//head:
+git checkout
+
+# Ensure submodules are initialized
+git submodule update --init --recursive
+
+# Trigger build
+oc --context build01 --as system:admin start-build ci-docs-test --from-dir=. -n dmistry --follow
+```
+
+### Option 4: Rebuild from Git Repository (Requires Git BuildConfig)
+
+If you have a Git-based BuildConfig (not currently configured), you would use:
+
+```bash
+# Update BuildConfig to point to specific branch/commit
+oc --context build01 --as system:admin set env bc/ci-docs-test GIT_REF= -n dmistry
+
+# Trigger build
+oc --context build01 --as system:admin start-build ci-docs-test -n dmistry --follow
+```
+
+## Monitor Build Status
+
+```bash
+# List all builds
+oc --context build01 --as system:admin get builds -n dmistry -l app=ci-docs-test
+
+# Watch build status
+oc --context build01 --as system:admin get builds -n dmistry -l app=ci-docs-test -w
+
+# View build logs
+oc --context build01 --as system:admin logs -n dmistry build/ci-docs-test-
+
+# Get latest build
+LATEST_BUILD=$(oc --context build01 --as system:admin get builds -n dmistry -l app=ci-docs-test -o jsonpath='{.items[0].metadata.name}')
+oc --context build01 --as system:admin logs -n dmistry build/${LATEST_BUILD} --follow
+```
+
+## Scale Deployment Commands
+
+### Scale Up/Down
+
+```bash
+# Scale to specific number of replicas
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas= -n dmistry
+
+# Examples:
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas=1 -n dmistry
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas=2 -n dmistry
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas=3 -n dmistry
+```
+
+### Scale Down to Zero (Stop Deployment)
+
+```bash
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas=0 -n dmistry
+```
+
+### Scale Up from Zero
+
+```bash
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas=1 -n dmistry
+```
+
+## Check Deployment Status
+
+```bash
+# Get deployment status
+oc --context build01 --as system:admin get deployment/ci-docs-test -n dmistry
+
+# Get pods
+oc --context build01 --as system:admin get pods -n dmistry -l app=ci-docs-test
+
+# Watch pods
+oc --context build01 --as system:admin get pods -n dmistry -l app=ci-docs-test -w
+
+# Check rollout status
+oc --context build01 --as system:admin rollout status deployment/ci-docs-test -n dmistry
+
+# View pod logs
+oc --context build01 --as system:admin logs -n dmistry -l app=ci-docs-test --tail=50 -f
+```
+
+## Complete Rebuild and Deploy Workflow
+
+```bash
+#!/bin/bash
+# Complete rebuild and deploy workflow
+
+NAMESPACE="dmistry"
+IMAGE_NAME="ci-docs-test"
+CLUSTER="build01"
+OC_CMD="oc --context ${CLUSTER} --as system:admin"
+
+# 1. Ensure we're in the repo root
+cd /home/dmistry/CI/ci-docs
+
+# 2. Initialize submodules
+echo "Initializing git submodules..."
+git submodule update --init --recursive || echo "Submodules already initialized"
+
+# 3. Trigger build
+echo "Starting build..."
+${OC_CMD} start-build ${IMAGE_NAME} --from-dir=. -n ${NAMESPACE} --follow
+
+# 4. Wait for build to complete (if not using --follow)
+# ${OC_CMD} wait --for=condition=complete build/${IMAGE_NAME}- -n ${NAMESPACE} --timeout=10m
+
+# 5. Restart deployment to pick up new image
+echo "Restarting deployment..."
+${OC_CMD} rollout restart deployment/${IMAGE_NAME} -n ${NAMESPACE}
+
+# 6. Wait for rollout
+echo "Waiting for rollout to complete..."
+${OC_CMD} rollout status deployment/${IMAGE_NAME} -n ${NAMESPACE} --timeout=5m
+
+# 7. Get route URL
+echo ""
+echo "Deployment complete!"
+ROUTE_URL=$(${OC_CMD} get route ${IMAGE_NAME} -n ${NAMESPACE} -o jsonpath='https://{.spec.host}' 2>/dev/null)
+if [ -n "${ROUTE_URL}" ]; then
+ echo "Access your deployment at: ${ROUTE_URL}"
+else
+ echo "Route not found. Check with: ${OC_CMD} get route -n ${NAMESPACE}"
+fi
+```
+
+## Quick Commands Reference
+
+```bash
+# Rebuild from current directory
+oc --context build01 --as system:admin start-build ci-docs-test --from-dir=. -n dmistry --follow
+
+# Scale to 2 replicas
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas=2 -n dmistry
+
+# Scale down to 0 (stop)
+oc --context build01 --as system:admin scale deployment/ci-docs-test --replicas=0 -n dmistry
+
+# Restart deployment (picks up new image)
+oc --context build01 --as system:admin rollout restart deployment/ci-docs-test -n dmistry
+
+# Check status
+oc --context build01 --as system:admin get deployment/ci-docs-test -n dmistry
+oc --context build01 --as system:admin get pods -n dmistry -l app=ci-docs-test
+
+# View logs
+oc --context build01 --as system:admin logs -n dmistry -l app=ci-docs-test --tail=50 -f
+
+# Get route URL
+oc --context build01 --as system:admin get route ci-docs-test -n dmistry -o jsonpath='https://{.spec.host}'
+```
+
+## Troubleshooting
+
+### Build Fails
+
+```bash
+# Check build logs
+LATEST_BUILD=$(oc --context build01 --as system:admin get builds -n dmistry -l app=ci-docs-test -o jsonpath='{.items[0].metadata.name}' | head -1)
+oc --context build01 --as system:admin logs -n dmistry build/${LATEST_BUILD}
+
+# Check build events
+oc --context build01 --as system:admin describe build/${LATEST_BUILD} -n dmistry
+```
+
+### Deployment Not Updating
+
+```bash
+# Force rollout restart
+oc --context build01 --as system:admin rollout restart deployment/ci-docs-test -n dmistry
+
+# Check image being used
+oc --context build01 --as system:admin get deployment/ci-docs-test -n dmistry -o jsonpath='{.spec.template.spec.containers[0].image}'
+
+# Check ImageStream
+oc --context build01 --as system:admin get imagestream/ci-docs-test -n dmistry
+```
+
+### Pods Not Starting
+
+```bash
+# Check pod status
+oc --context build01 --as system:admin get pods -n dmistry -l app=ci-docs-test
+
+# Describe pod for events
+oc --context build01 --as system:admin describe pod -n dmistry -l app=ci-docs-test
+
+# Check pod logs
+oc --context build01 --as system:admin logs -n dmistry -l app=ci-docs-test --tail=100
+```
+
diff --git a/deploy/buildconfig.yaml b/deploy/buildconfig.yaml
new file mode 100644
index 00000000..19fbe218
--- /dev/null
+++ b/deploy/buildconfig.yaml
@@ -0,0 +1,26 @@
+---
+apiVersion: build.openshift.io/v1
+kind: BuildConfig
+metadata:
+ name: ci-docs-test
+ namespace: dmistry
+ labels:
+ app: ci-docs-test
+spec:
+ source:
+ type: Binary
+ binary: {}
+ # Note: This BuildConfig expects binary source to be provided via:
+ # oc --context build01 --as system:admin start-build ci-docs-test --from-dir=. -n dmistry
+ strategy:
+ type: Docker
+ dockerStrategy:
+ dockerfilePath: Dockerfile
+ output:
+ to:
+ kind: ImageStreamTag
+ name: ci-docs-test:latest
+ triggers:
+ - type: ConfigChange
+ runPolicy: Serial
+
diff --git a/deploy/deploy-openshift-build.sh b/deploy/deploy-openshift-build.sh
new file mode 100755
index 00000000..5231aafb
--- /dev/null
+++ b/deploy/deploy-openshift-build.sh
@@ -0,0 +1,134 @@
+#!/bin/bash
+set -e
+
+# Configuration
+NAMESPACE="dmistry"
+IMAGE_NAME="ci-docs-test"
+CLUSTER="build01"
+
+echo "=========================================="
+echo "Deploying CI Docs Test to build01 cluster"
+echo "Using OpenShift Build (no local Docker required)"
+echo "=========================================="
+echo "Namespace: ${NAMESPACE}"
+echo "Cluster: ${CLUSTER}"
+echo ""
+
+# Set OC context with system:admin
+OC_CMD="oc --context ${CLUSTER} --as system:admin"
+
+# Check if we can access the cluster
+if ! ${OC_CMD} get nodes &>/dev/null 2>&1; then
+ echo "Error: Cannot access ${CLUSTER} cluster"
+ exit 1
+fi
+
+echo "Using context: build01"
+echo "Deploying as: system:admin"
+echo ""
+
+# Create namespace if it doesn't exist
+echo "Step 1: Ensuring namespace exists..."
+if ${OC_CMD} get namespace ${NAMESPACE} &>/dev/null 2>&1; then
+ echo "✓ Namespace ${NAMESPACE} already exists"
+else
+ echo "Creating namespace ${NAMESPACE}..."
+ ${OC_CMD} create namespace ${NAMESPACE}
+ echo "✓ Namespace ${NAMESPACE} created"
+fi
+echo ""
+
+# Create ImageStream
+echo "Step 2: Creating ImageStream..."
+${OC_CMD} create imagestream ${IMAGE_NAME} -n ${NAMESPACE} --dry-run=client -o yaml | ${OC_CMD} apply -f -
+echo "✓ ImageStream ready"
+echo ""
+
+# Create BuildConfig
+echo "Step 3: Creating BuildConfig..."
+echo "Note: This will build from the current branch in your repository"
+${OC_CMD} apply -f deploy/buildconfig.yaml
+
+if [ $? -ne 0 ]; then
+ echo "Error: Failed to create BuildConfig"
+ exit 1
+fi
+
+echo "✓ BuildConfig created"
+echo ""
+
+# Start the build from local directory
+echo "Step 4: Starting build from local source..."
+echo "This will upload the current directory to the build..."
+echo "Note: Ensuring git submodules are initialized..."
+cd "$(dirname "$0")/.." # Go to repo root
+
+# Initialize submodules if needed
+if [ -f .gitmodules ]; then
+ git submodule update --init --recursive --depth 1 2>/dev/null || echo "Submodules may already be initialized"
+fi
+
+${OC_CMD} start-build ${IMAGE_NAME} --from-dir=. -n ${NAMESPACE} --follow
+
+if [ $? -ne 0 ]; then
+ echo "Error: Build failed"
+ echo "Check build logs with:"
+ echo " ${OC_CMD} logs -n ${NAMESPACE} build/${IMAGE_NAME}-"
+ exit 1
+fi
+
+echo "✓ Build completed successfully"
+echo ""
+
+# Apply deployment manifests (update to use ImageStream)
+echo "Step 5: Applying deployment manifests..."
+# Update the deployment to use ImageStream instead of external registry
+${OC_CMD} apply -f deploy/test-deployment.yaml
+
+# Update deployment to use ImageStream
+${OC_CMD} set image deployment/ci-docs-test nginx=${IMAGE_NAME}:latest -n ${NAMESPACE}
+
+echo "✓ Deployment manifests applied"
+echo ""
+
+# Wait for deployment to be ready
+echo "Step 6: Waiting for deployment to be ready..."
+${OC_CMD} rollout status deployment/ci-docs-test -n ${NAMESPACE} --timeout=5m
+
+if [ $? -ne 0 ]; then
+ echo "Error: Deployment failed to become ready"
+ echo "Check logs with: ${OC_CMD} logs -n ${NAMESPACE} -l app=ci-docs-test"
+ exit 1
+fi
+
+echo "✓ Deployment is ready"
+echo ""
+
+# Get the route URL
+echo "Step 7: Getting route URL..."
+ROUTE_URL=$(${OC_CMD} get route ci-docs-test -n ${NAMESPACE} -o jsonpath='{.spec.host}' 2>/dev/null)
+
+if [ -z "${ROUTE_URL}" ]; then
+ echo "Warning: Could not get route URL"
+ echo "Check route with: ${OC_CMD} get route -n ${NAMESPACE}"
+else
+ echo ""
+ echo "=========================================="
+ echo "✓ Deployment successful!"
+ echo "=========================================="
+ echo "Access your test deployment at:"
+ echo " https://${ROUTE_URL}"
+ echo ""
+ echo "To check status:"
+ echo " ${OC_CMD} get pods -n ${NAMESPACE}"
+ echo " ${OC_CMD} get route -n ${NAMESPACE}"
+ echo ""
+ echo "To view logs:"
+ echo " ${OC_CMD} logs -n ${NAMESPACE} -l app=ci-docs-test"
+ echo ""
+ echo "To delete the deployment:"
+ echo " ${OC_CMD} delete -f deploy/test-deployment.yaml"
+ echo " ${OC_CMD} delete buildconfig ${IMAGE_NAME} -n ${NAMESPACE}"
+ echo "=========================================="
+fi
+
diff --git a/deploy/deploy.sh b/deploy/deploy.sh
new file mode 100755
index 00000000..27fad47d
--- /dev/null
+++ b/deploy/deploy.sh
@@ -0,0 +1,172 @@
+#!/bin/bash
+set -e
+
+# Configuration
+NAMESPACE="dmistry"
+IMAGE_NAME="ci-docs-test"
+IMAGE_TAG="latest"
+REGISTRY="registry.ci.openshift.org"
+FULL_IMAGE="${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}"
+CLUSTER="build01"
+
+echo "=========================================="
+echo "Deploying CI Docs Test to build01 cluster"
+echo "=========================================="
+echo "Namespace: ${NAMESPACE}"
+echo "Image: ${FULL_IMAGE}"
+echo "Cluster: ${CLUSTER}"
+echo ""
+
+# Set OC context with system:admin
+OC_CMD="oc --context ${CLUSTER} --as system:admin"
+
+# Check if we can access the cluster (try a simple command)
+if ! ${OC_CMD} get nodes &>/dev/null 2>&1; then
+ echo "Error: Cannot access ${CLUSTER} cluster"
+ echo "Please ensure:"
+ echo " 1. You're logged in: oc login "
+ echo " 2. Context is set: oc config use-context ${CLUSTER}"
+ exit 1
+fi
+
+# Check current context
+CURRENT_CONTEXT=$(oc config current-context 2>/dev/null || echo "build01")
+echo "Using context: ${CURRENT_CONTEXT}"
+echo "Deploying as: system:admin"
+echo ""
+
+# Build the container image
+echo "Step 1: Building container image..."
+# Try podman first (works without sudo), then docker
+if podman ps &>/dev/null 2>&1; then
+ CONTAINER_CMD="podman"
+ echo "Using podman"
+elif docker ps &>/dev/null 2>&1; then
+ CONTAINER_CMD="docker"
+ echo "Using docker"
+elif sudo docker ps &>/dev/null 2>&1; then
+ CONTAINER_CMD="sudo docker"
+ echo "Using sudo docker"
+else
+ echo "Error: Cannot access container runtime (podman/docker)"
+ echo ""
+ echo "Options:"
+ echo " 1. Use OpenShift build (no local container runtime needed):"
+ echo " ./deploy/deploy-openshift-build.sh"
+ echo ""
+ echo " 2. Add your user to the docker group:"
+ echo " sudo usermod -aG docker $USER"
+ echo " (then log out and back in)"
+ echo ""
+ echo " 3. Run this script with sudo:"
+ echo " sudo ./deploy/deploy.sh"
+ exit 1
+fi
+
+${CONTAINER_CMD} build -t ${FULL_IMAGE} -f Dockerfile .
+
+if [ $? -ne 0 ]; then
+ echo "Error: Docker build failed"
+ exit 1
+fi
+
+echo "✓ Docker image built successfully"
+echo ""
+
+# Push the image to registry
+echo "Step 2: Pushing image to registry..."
+${CONTAINER_CMD} push ${FULL_IMAGE}
+
+if [ $? -ne 0 ]; then
+ echo "Error: Docker push failed"
+ echo "Make sure you're logged into the registry:"
+ echo " docker login ${REGISTRY}"
+ exit 1
+fi
+
+echo "✓ Image pushed successfully"
+echo ""
+
+# Create namespace if it doesn't exist
+echo "Step 3: Ensuring namespace exists..."
+if ${OC_CMD} get namespace ${NAMESPACE} &>/dev/null 2>&1; then
+ echo "✓ Namespace ${NAMESPACE} already exists"
+else
+ echo "Creating namespace ${NAMESPACE}..."
+ ${OC_CMD} create namespace ${NAMESPACE}
+ if [ $? -ne 0 ]; then
+ echo "Error: Failed to create namespace"
+ exit 1
+ fi
+ echo "✓ Namespace ${NAMESPACE} created"
+fi
+
+# Check permissions (using system:admin should have all permissions)
+echo "Checking permissions..."
+if ! ${OC_CMD} auth can-i create deployments -n ${NAMESPACE} &>/dev/null; then
+ echo "Warning: Cannot create deployments even with system:admin"
+ echo "This may indicate a cluster configuration issue"
+ read -p "Continue anyway? (y/n) " -n 1 -r
+ echo
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ exit 1
+ fi
+else
+ echo "✓ Permissions verified"
+fi
+
+echo "✓ Namespace ready"
+echo ""
+
+# Apply deployment manifests
+echo "Step 4: Applying deployment manifests..."
+${OC_CMD} apply -f deploy/test-deployment.yaml
+
+if [ $? -ne 0 ]; then
+ echo "Error: Failed to apply manifests"
+ exit 1
+fi
+
+echo "✓ Deployment manifests applied"
+echo ""
+
+# Wait for deployment to be ready
+echo "Step 5: Waiting for deployment to be ready..."
+${OC_CMD} rollout status deployment/ci-docs-test -n ${NAMESPACE} --timeout=5m
+
+if [ $? -ne 0 ]; then
+ echo "Error: Deployment failed to become ready"
+ echo "Check logs with: oc logs -n ${NAMESPACE} deployment/ci-docs-test"
+ exit 1
+fi
+
+echo "✓ Deployment is ready"
+echo ""
+
+# Get the route URL
+echo "Step 6: Getting route URL..."
+ROUTE_URL=$(${OC_CMD} get route ci-docs-test -n ${NAMESPACE} -o jsonpath='{.spec.host}' 2>/dev/null)
+
+if [ -z "${ROUTE_URL}" ]; then
+ echo "Warning: Could not get route URL"
+ echo "Check route with: oc --context ${CLUSTER} get route -n ${NAMESPACE}"
+else
+ echo ""
+ echo "=========================================="
+ echo "✓ Deployment successful!"
+ echo "=========================================="
+ echo "Access your test deployment at:"
+ echo " https://${ROUTE_URL}"
+ echo ""
+ echo "To check status:"
+ echo " oc --context ${CLUSTER} get pods -n ${NAMESPACE}"
+ echo " oc --context ${CLUSTER} get route -n ${NAMESPACE}"
+ echo ""
+ echo "To view logs:"
+ echo " oc --context ${CLUSTER} logs -n ${NAMESPACE} -l app=ci-docs-test"
+ echo ""
+ echo "To delete the deployment:"
+ echo " oc --context ${CLUSTER} delete -f deploy/test-deployment.yaml"
+ echo "=========================================="
+fi
+
diff --git a/deploy/test-deployment.yaml b/deploy/test-deployment.yaml
new file mode 100644
index 00000000..6c0ec023
--- /dev/null
+++ b/deploy/test-deployment.yaml
@@ -0,0 +1,94 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: dmistry
+ labels:
+ name: dmistry
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: ci-docs-test
+ namespace: dmistry
+ labels:
+ app: ci-docs-test
+ version: test
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: ci-docs-test
+ template:
+ metadata:
+ labels:
+ app: ci-docs-test
+ version: test
+ spec:
+ containers:
+ - name: nginx
+ image: image-registry.openshift-image-registry.svc:5000/dmistry/ci-docs-test:latest
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ resources:
+ requests:
+ memory: "128Mi"
+ cpu: "100m"
+ limits:
+ memory: "512Mi"
+ cpu: "500m"
+ livenessProbe:
+ httpGet:
+ path: /
+ port: 8080
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /
+ port: 8080
+ initialDelaySeconds: 10
+ periodSeconds: 5
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: ci-docs-test
+ namespace: dmistry
+ labels:
+ app: ci-docs-test
+spec:
+ ports:
+ - port: 8080
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ selector:
+ app: ci-docs-test
+ type: ClusterIP
+---
+apiVersion: route.openshift.io/v1
+kind: Route
+metadata:
+ name: ci-docs-test
+ namespace: dmistry
+ labels:
+ app: ci-docs-test
+spec:
+ to:
+ kind: Service
+ name: ci-docs-test
+ weight: 100
+ port:
+ targetPort: http
+ tls:
+ termination: edge
+ insecureEdgeTerminationPolicy: Redirect
+ # Use a different hostname to avoid conflicts
+ # This will create: ci-docs-test-dmistry.apps.build01.ci.devcluster.openshift.com
+ # Or you can specify a custom hostname if you have access
+ # host: ci-docs-test-dmistry.apps.build01.ci.devcluster.openshift.com
+
diff --git a/layouts/_default/search.html b/layouts/_default/search.html
new file mode 100644
index 00000000..32527815
--- /dev/null
+++ b/layouts/_default/search.html
@@ -0,0 +1,451 @@
+{{ define "main" }}
+
+
+
{{ .Title }}
+
+{{ if .Site.Params.gcs_engine_id }}
+
+
+
+{{ else if .Site.Params.offlineSearch }}
+
+
+
+
+
+
+ {{ if .Params.q }}
+
Searching for: {{ .Params.q }}
+ {{ else }}
+
Enter a search term above to find documentation.
+ {{ end }}
+
+
+
+
+
+
+{{ end }}
+
+
+{{ end }}
+
diff --git a/layouts/partials/ai-search.html b/layouts/partials/ai-search.html
new file mode 100644
index 00000000..bf01f1f4
--- /dev/null
+++ b/layouts/partials/ai-search.html
@@ -0,0 +1,220 @@
+{{ if .Site.Params.search.enhancedSearch }}
+
+
+
+
+
+
+{{ end }}
+
diff --git a/layouts/partials/enhanced-offline-search.js b/layouts/partials/enhanced-offline-search.js
new file mode 100644
index 00000000..c8d9c5ad
--- /dev/null
+++ b/layouts/partials/enhanced-offline-search.js
@@ -0,0 +1,215 @@
+// Enhanced offline search with improved indexing and search capabilities
+// Adapted from themes/docsy/assets/js/offline-search.js with enhancements
+
+(function ($) {
+ 'use strict';
+
+ $(document).ready(function () {
+ const $searchInput = $('.td-search-input');
+
+ // Options for popover
+ $searchInput.data('html', true);
+ $searchInput.data('placement', 'bottom');
+ $searchInput.data(
+ 'template',
+ ''
+ );
+
+ // Register handler
+ $searchInput.on('change', (event) => {
+ render($(event.target));
+ $searchInput.blur();
+ });
+
+ // Prevent reloading page by enter key on sidebar search
+ $searchInput.closest('form').on('submit', () => {
+ return false;
+ });
+
+ // Enhanced Lunr index with more fields
+ let idx = null;
+ const resultDetails = new Map();
+
+ // Load search index
+ $.ajax($searchInput.data('offline-search-index-json-src')).then(
+ (data) => {
+ idx = lunr(function () {
+ this.ref('ref');
+
+ // Enhanced field configuration with better boosting
+ this.field('title', { boost: 15 });
+ this.field('description', { boost: 8 });
+ this.field('categories', { boost: 5 });
+ this.field('tags', { boost: 5 });
+ this.field('keywords', { boost: 5 });
+ this.field('section', { boost: 3 });
+ this.field('type', { boost: 2 });
+ this.field('body', { boost: 1 });
+ this.field('allText', { boost: 1 });
+
+ data.forEach((doc) => {
+ this.add(doc);
+
+ resultDetails.set(doc.ref, {
+ title: doc.title || '',
+ excerpt: doc.excerpt || doc.description || '',
+ description: doc.description || '',
+ categories: doc.categories || [],
+ tags: doc.tags || [],
+ section: doc.section || '',
+ type: doc.type || '',
+ date: doc.date || '',
+ lastmod: doc.lastmod || ''
+ });
+ });
+ });
+
+ $searchInput.trigger('change');
+ }
+ );
+
+ const render = ($targetSearchInput) => {
+ // Dispose the previous result
+ $targetSearchInput.popover('dispose');
+
+ if (idx === null) {
+ return;
+ }
+
+ const searchQuery = $targetSearchInput.val();
+ if (searchQuery === '') {
+ return;
+ }
+
+ // Enhanced search query with better relevance
+ const results = idx
+ .query((q) => {
+ const tokens = lunr.tokenizer(searchQuery.toLowerCase());
+ tokens.forEach((token) => {
+ const queryString = token.toString();
+ // Exact match boost
+ q.term(queryString, { boost: 100 });
+ // Wildcard matches
+ q.term(queryString, {
+ wildcard:
+ lunr.Query.wildcard.LEADING |
+ lunr.Query.wildcard.TRAILING,
+ boost: 10,
+ });
+ // Fuzzy matches
+ q.term(queryString, {
+ editDistance: 2,
+ boost: 5
+ });
+ });
+ })
+ .slice(
+ 0,
+ $targetSearchInput.data('offline-search-max-results') || 10
+ );
+
+ // Build result HTML
+ const $html = $('');
+
+ $html.append(
+ $('
')
+ .css({
+ display: 'flex',
+ justifyContent: 'space-between',
+ marginBottom: '1em',
+ })
+ .append(
+ $('
')
+ .text(`Found ${results.length} result${results.length !== 1 ? 's' : ''}`)
+ .css({ fontWeight: 'bold' })
+ )
+ .append(
+ $('')
+ .addClass('fas fa-times search-result-close-button')
+ .css({ cursor: 'pointer' })
+ )
+ );
+
+ const $searchResultBody = $('').css({
+ maxHeight: `calc(100vh - ${
+ $targetSearchInput.offset().top -
+ $(window).scrollTop() +
+ 180
+ }px)`,
+ overflowY: 'auto',
+ });
+ $html.append($searchResultBody);
+
+ if (results.length === 0) {
+ $searchResultBody.append(
+ $('
').text(`No results found for "${searchQuery}"`)
+ );
+ } else {
+ results.forEach((r) => {
+ const doc = resultDetails.get(r.ref);
+ const href =
+ $searchInput.data('offline-search-base-href') +
+ r.ref.replace(/^\//, '');
+
+ const $entry = $('
').addClass('mt-4');
+
+ // Show path/section
+ if (doc.section) {
+ $entry.append(
+ $('
')
+ .addClass('d-block text-muted')
+ .text(doc.section + ' / ' + r.ref)
+ );
+ } else {
+ $entry.append(
+ $('')
+ .addClass('d-block text-muted')
+ .text(r.ref)
+ );
+ }
+
+ // Title with link
+ $entry.append(
+ $('')
+ .addClass('d-block')
+ .css({ fontSize: '1.2rem', fontWeight: 'bold' })
+ .attr('href', href)
+ .text(doc.title)
+ );
+
+ // Description/excerpt
+ if (doc.excerpt) {
+ $entry.append($('').text(doc.excerpt));
+ }
+
+ // Tags/categories
+ if (doc.tags && doc.tags.length > 0) {
+ const $tags = $('
').addClass('mt-2');
+ doc.tags.forEach(tag => {
+ $tags.append(
+ $('
')
+ .addClass('badge badge-secondary mr-1')
+ .text(tag)
+ );
+ });
+ $entry.append($tags);
+ }
+
+ $searchResultBody.append($entry);
+ });
+ }
+
+ $targetSearchInput.on('shown.bs.popover', () => {
+ $('.search-result-close-button').on('click', () => {
+ $targetSearchInput.val('');
+ $targetSearchInput.trigger('change');
+ });
+ });
+
+ $targetSearchInput
+ .data('content', $html[0].outerHTML)
+ .popover('show');
+ };
+ });
+})(jQuery);
+
diff --git a/layouts/partials/enhanced-search.js b/layouts/partials/enhanced-search.js
new file mode 100644
index 00000000..0345d6e3
--- /dev/null
+++ b/layouts/partials/enhanced-search.js
@@ -0,0 +1,42 @@
+// Enhanced search functionality that works with both popover and search page
+(function($) {
+ 'use strict';
+
+ var EnhancedSearch = {
+ init: function() {
+ $(document).ready(function() {
+ // Handle search input in navbar/sidebar
+ $(document).on('keypress', '.td-search-input', function(e) {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ var query = $(this).val().trim();
+ if (query) {
+ // If offline search is enabled, show popover results
+ // Otherwise redirect to search page
+ if ($(this).data('offline-search-index-json-src')) {
+ // Trigger change event to show popover (handled by offline-search.js)
+ $(this).trigger('change');
+ } else {
+ // Redirect to search page
+ var searchPage = "{{ "search/" | absURL }}?q=" + encodeURIComponent(query);
+ window.location.href = searchPage;
+ }
+ }
+ return false;
+ }
+ });
+
+ // Also handle click on search icon if present
+ $(document).on('click', '.td-search-input', function() {
+ var query = $(this).val().trim();
+ if (query && $(this).data('offline-search-index-json-src')) {
+ $(this).trigger('change');
+ }
+ });
+ });
+ }
+ };
+
+ EnhancedSearch.init();
+}(jQuery));
+
diff --git a/layouts/partials/head.html b/layouts/partials/head.html
index b36d176c..ad87d629 100644
--- a/layouts/partials/head.html
+++ b/layouts/partials/head.html
@@ -15,9 +15,105 @@
{{ partialCached "favicons.html" . }}
{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} | {{ end }}{{ .Site.Title }}{{ end }}
-{{- template "_internal/opengraph.html" . -}}
-{{- template "_internal/schema.html" . -}}
-{{- template "_internal/twitter_cards.html" . -}}
+{{- $description := .Description | default .Summary | default .Site.Params.description | default "" -}}
+{{- if $description -}}
+
+{{- end -}}
+{{- $keywords := .Params.keywords | default .Site.Params.keywords | default (slice) -}}
+{{- if $keywords -}}
+
+{{- end -}}
+{{- /* Canonical URL for SEO */ -}}
+
+{{- /* Open Graph and Twitter Card meta tags */ -}}
+
+
+
+
+
+{{- if .Params.image -}}
+
+{{- end -}}
+
+
+
+{{- if .Params.image -}}
+
+{{- end -}}
+{{- /* Article meta tags */ -}}
+{{- if .IsPage -}}
+
+
+{{- if .Params.author -}}
+
+{{- end -}}
+{{- if .Params.section -}}
+
+{{- end -}}
+{{- if .Params.tags -}}
+{{- range .Params.tags -}}
+
+{{- end -}}
+{{- end -}}
+{{- end -}}
+{{/* Enhanced structured data for AI and SEO */}}
+{{ if .IsPage }}
+
+{{ else if .IsHome }}
+
+{{ end }}
{{ if hugo.IsProduction }}
{{ template "_internal/google_analytics_async.html" . }}
{{ end }}
@@ -27,11 +123,13 @@
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous">
{{ if .Site.Params.offlineSearch }}
+
+
{{end}}
+{{/* Enhanced search configuration - removed unused AI libraries for better performance */}}
{{ if .Site.Params.prism_syntax_highlighting }}
diff --git a/layouts/partials/hooks/body-end.html b/layouts/partials/hooks/body-end.html
new file mode 100644
index 00000000..44d2fd79
--- /dev/null
+++ b/layouts/partials/hooks/body-end.html
@@ -0,0 +1,3 @@
+{{/* Enhanced Search Features */}}
+{{ partial "ai-search.html" . }}
+
diff --git a/layouts/partials/scripts.html b/layouts/partials/scripts.html
new file mode 100644
index 00000000..3d9185eb
--- /dev/null
+++ b/layouts/partials/scripts.html
@@ -0,0 +1,35 @@
+
+
+
+{{ if .Site.Params.mermaid.enable }}
+
+{{ end }}
+
+
+{{ if .Site.Params.plantuml.enable }}
+
+{{ end }}
+
+{{ $jsBase := resources.Get "js/base.js" }}
+{{ $jsAnchor := resources.Get "js/anchor.js" }}
+{{ $jsSearch := resources.Get "js/search.js" | resources.ExecuteAsTemplate "js/search.js" .Site.Home }}
+{{ $jsMermaid := resources.Get "js/mermaid.js" | resources.ExecuteAsTemplate "js/mermaid.js" . }}
+{{ $jsPlantuml := resources.Get "js/plantuml.js" | resources.ExecuteAsTemplate "js/plantuml.js" . }}
+{{ if .Site.Params.offlineSearch }}
+{{/* Use enhanced offline search with better indexing */}}
+{{ $jsSearch = resources.Get "js/offline-search.js" }}
+{{/* Note: Enhanced search index is in assets/json/offline-search-index.json */}}
+{{ end }}
+{{ $js := (slice $jsBase $jsAnchor $jsSearch $jsMermaid $jsPlantuml) | resources.Concat "js/main.js" }}
+{{ if .Site.IsServer }}
+
+{{ else }}
+{{ $js := $js | minify | fingerprint }}
+
+{{ end }}
+{{ if .Site.Params.prism_syntax_highlighting }}
+
+
+{{ end }}
+{{ partial "hooks/body-end.html" . }}
+
diff --git a/netlify.toml b/netlify.toml
index a596fa8f..e961b4ca 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -2,5 +2,28 @@
command = "make generate"
[context.deploy-preview.environment]
-HUGO_VERSION = "0.119.0"
+HUGO_VERSION = "0.128.0"
NODE_VERSION = "20.11.1"
+
+[build]
+ command = "make generate"
+ publish = "public"
+
+[build.environment]
+ HUGO_VERSION = "0.128.0"
+ NODE_VERSION = "20.11.1"
+<<<<<<< HEAD
+
+# Security Headers
+[[headers]]
+ for = "/*"
+ [headers.values]
+ X-Content-Type-Options = "nosniff"
+ X-Frame-Options = "DENY"
+ X-XSS-Protection = "1; mode=block"
+ Referrer-Policy = "strict-origin-when-cross-origin"
+ Permissions-Policy = "geolocation=(), microphone=(), camera=()"
+ Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"
+ Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://code.jquery.com https://unpkg.com https://cdn.jsdelivr.net https://cdn.datatables.net; style-src 'self' 'unsafe-inline' https://cdn.datatables.net; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://cluster-display.ci.openshift.org https://helpdesk-faq-ci.apps.ci.l2s4.p1.openshiftapps.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
+=======
+>>>>>>> 81d9fe6 (Migrate docs framework: upgrade Hugo, add AI search/chat, enhance SEO)
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
new file mode 100644
index 00000000..5346c8b8
--- /dev/null
+++ b/nginx/nginx.conf
@@ -0,0 +1,43 @@
+server {
+ listen 8080;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Security Headers
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-Frame-Options "DENY" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+ # Content Security Policy
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://code.jquery.com https://unpkg.com https://cdn.jsdelivr.net https://cdn.datatables.net; style-src 'self' 'unsafe-inline' https://cdn.datatables.net; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://cluster-display.ci.openshift.org https://helpdesk-faq-ci.apps.ci.l2s4.p1.openshiftapps.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';" always;
+
+ # 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/javascript application/json;
+
+ # Cache static assets
+ location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # Main location
+ location / {
+ try_files $uri $uri/ /index.html;
+ absolute_redirect off;
+ }
+
+ # Health check endpoint
+ location /health {
+ access_log off;
+ return 200 "healthy\n";
+ add_header Content-Type text/plain;
+ }
+}
+
diff --git a/presentations/beamer/README.md b/presentations/beamer/README.md
index 8a007526..9682eeda 100644
--- a/presentations/beamer/README.md
+++ b/presentations/beamer/README.md
@@ -1,5 +1,5 @@
Unofficial Red Hat template for Beamer
-======================================
+===
*Warning: programmer art!*
diff --git a/ssh-node.sh b/ssh-node.sh
new file mode 100755
index 00000000..e9156cdb
--- /dev/null
+++ b/ssh-node.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+# SSH into a build01 node using keys from /home/dmistry/keys/build02
+
+set -e
+
+CLUSTER="build01"
+KEY_FILE="/home/dmistry/keys/build02"
+OC_CMD="oc --context ${CLUSTER}"
+
+# Check if key file exists
+if [ ! -f "${KEY_FILE}" ]; then
+ echo "Error: Key file ${KEY_FILE} not found"
+ exit 1
+fi
+
+# Set proper permissions on key file
+chmod 600 "${KEY_FILE}" 2>/dev/null || true
+
+# Check if we can access the cluster
+if ! ${OC_CMD} get nodes &>/dev/null 2>&1; then
+ echo "Error: Cannot access ${CLUSTER} cluster"
+ exit 1
+fi
+
+# If node name provided as argument, use it
+if [ $# -eq 1 ]; then
+ NODE_NAME="$1"
+ # Remove 'node/' prefix if present
+ NODE_NAME="${NODE_NAME#node/}"
+else
+ # List nodes and let user select
+ echo "Available nodes in ${CLUSTER}:"
+ echo ""
+ ${OC_CMD} get nodes -o custom-columns=NAME:.metadata.name,EXTERNAL-IP:.status.addresses[?\(@.type==\"ExternalIP\"\)].address,INTERNAL-IP:.status.addresses[?\(@.type==\"InternalIP\"\)].address,ROLES:.status.capacity.kubernetes\\.io/arch --no-headers | nl -v 1 -w 2 -s '. '
+ echo ""
+ read -p "Enter node number or node name: " INPUT
+
+ # Check if input is a number
+ if [[ "$INPUT" =~ ^[0-9]+$ ]]; then
+ NODE_NAME=$(${OC_CMD} get nodes -o custom-columns=NAME:.metadata.name --no-headers | sed -n "${INPUT}p")
+ if [ -z "$NODE_NAME" ]; then
+ echo "Error: Invalid node number"
+ exit 1
+ fi
+ else
+ NODE_NAME="$INPUT"
+ # Remove 'node/' prefix if present
+ NODE_NAME="${NODE_NAME#node/}"
+ fi
+fi
+
+# Get node external IP
+NODE_IP=$(${OC_CMD} get node "${NODE_NAME}" -o jsonpath='{.status.addresses[?(@.type=="ExternalIP")].address}' 2>/dev/null)
+
+if [ -z "$NODE_IP" ]; then
+ echo "Error: Could not get external IP for node ${NODE_NAME}"
+ echo "Trying to use internal IP or hostname..."
+ NODE_IP=$(${OC_CMD} get node "${NODE_NAME}" -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}' 2>/dev/null)
+ if [ -z "$NODE_IP" ]; then
+ NODE_IP="${NODE_NAME}"
+ fi
+fi
+
+echo ""
+echo "Connecting to node: ${NODE_NAME}"
+echo "Using IP: ${NODE_IP}"
+echo "Using key: ${KEY_FILE}"
+echo ""
+
+# Try SSH first
+echo "Attempting SSH connection..."
+if timeout 5 ssh -i "${KEY_FILE}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 core@${NODE_IP} "echo 'SSH connection successful'" 2>/dev/null; then
+ echo "SSH connection successful! Opening interactive session..."
+ ssh -i "${KEY_FILE}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null core@${NODE_IP}
+else
+ echo "SSH connection failed (likely due to network/firewall restrictions)"
+ echo ""
+ echo "Falling back to 'oc debug node' method..."
+ echo "This uses OpenShift's built-in node debugging feature."
+ echo "Note: Run 'chroot /host' to access host binaries and filesystem"
+ echo ""
+ ${OC_CMD} --as system:admin debug node/${NODE_NAME}
+fi
+
diff --git a/static/_headers b/static/_headers
new file mode 100644
index 00000000..be1b0072
--- /dev/null
+++ b/static/_headers
@@ -0,0 +1,13 @@
+# Security Headers
+X-Content-Type-Options: nosniff
+X-Frame-Options: DENY
+X-XSS-Protection: 1; mode=block
+Referrer-Policy: strict-origin-when-cross-origin
+Permissions-Policy: geolocation=(), microphone=(), camera=()
+
+# Content Security Policy
+Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://code.jquery.com https://unpkg.com https://cdn.jsdelivr.net https://cdn.datatables.net; style-src 'self' 'unsafe-inline' https://cdn.datatables.net; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://cluster-display.ci.openshift.org https://helpdesk-faq-ci.apps.ci.l2s4.p1.openshiftapps.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';
+
+# Strict Transport Security (HTTPS only)
+Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
+
diff --git a/static/js/enhanced-search.js b/static/js/enhanced-search.js
new file mode 100644
index 00000000..ef48312d
--- /dev/null
+++ b/static/js/enhanced-search.js
@@ -0,0 +1,213 @@
+function openEnhancedSearch() {
+ document.getElementById('enhanced-search-container').style.display = 'block';
+ document.getElementById('enhanced-search-input').focus();
+}
+
+function closeEnhancedSearch() {
+ document.getElementById('enhanced-search-container').style.display = 'none';
+}
+
+function performEnhancedSearch() {
+ const query = document.getElementById('enhanced-search-input').value;
+ if (!query.trim()) return;
+
+ const resultsDiv = document.getElementById('enhanced-search-results');
+ const loadingDiv = document.getElementById('enhanced-search-loading');
+
+ loadingDiv.style.display = 'block';
+ resultsDiv.innerHTML = '';
+
+ // Use Fuse.js search with improved performance
+ performEnhancedFuseSearch(query, resultsDiv, loadingDiv);
+}
+
+// Helper function to escape HTML
+function escapeHtml(text) {
+ if (!text) return '';
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+// Security: Sanitize search input
+function sanitizeSearchInput(text) {
+ if (typeof text !== 'string') {
+ return '';
+ }
+ // Remove potentially dangerous characters
+ return text.replace(/[<>\"']/g, '').substring(0, 200);
+}
+
+// Security: Sanitize URLs to prevent XSS
+function sanitizeUrl(url) {
+ try {
+ // Only allow relative URLs or same origin
+ if (url.startsWith('/') || url.startsWith('#')) {
+ return url;
+ }
+ const parsed = new URL(url, window.location.origin);
+ if (parsed.origin === window.location.origin) {
+ return parsed.pathname + parsed.search + parsed.hash;
+ }
+ return '#';
+ } catch {
+ return '#';
+ }
+}
+
+function performEnhancedFuseSearch(query, resultsDiv, loadingDiv) {
+ // Fuse.js search - lightweight and fast with fuzzy matching
+ // Security: Sanitize query
+ const sanitizedQuery = sanitizeSearchInput(query);
+
+ // Use Fuse.js if available
+ if (window.fuseSearch && window.searchData) {
+ try {
+ const fuseResults = window.fuseSearch.search(sanitizedQuery, {
+ limit: 10
+ });
+
+ loadingDiv.style.display = 'none';
+
+ if (fuseResults.length === 0) {
+ const noResults = document.createElement('div');
+ noResults.className = 'alert alert-info';
+ noResults.innerHTML = `
+ No results found
+ I did not find information related to "${escapeHtml(sanitizedQuery)}".
+ Tips:
+
+ - Try using different keywords
+ - Check your spelling
+ - Use more general terms
+ - Try searching for phrases in quotes: "cluster profile"
+
+ `;
+ resultsDiv.innerHTML = '';
+ resultsDiv.appendChild(noResults);
+ return;
+ }
+
+ // Build results list
+ const list = document.createElement('ul');
+ list.className = 'enhanced-search-results-list';
+
+ fuseResults.forEach(result => {
+ const doc = result.item;
+ const item = document.createElement('li');
+ const link = document.createElement('a');
+ const safeUrl = sanitizeUrl(doc.ref);
+ link.href = safeUrl;
+
+ // Use Fuse.js matches for highlighting
+ let titleHtml = doc.title || doc.ref;
+ if (result.matches && result.matches.length > 0) {
+ const titleMatch = result.matches.find(m => m.key === 'title');
+ if (titleMatch && titleMatch.indices) {
+ titleHtml = highlightFuseMatches(doc.title, titleMatch.indices);
+ } else {
+ titleHtml = highlightQuery(doc.title, sanitizedQuery);
+ }
+ } else {
+ titleHtml = highlightQuery(doc.title, sanitizedQuery);
+ }
+ link.innerHTML = titleHtml;
+
+ const excerpt = document.createElement('p');
+ const excerptText = doc.excerpt || doc.description || '';
+ if (excerptText) {
+ let excerptHtml = excerptText;
+ if (result.matches && result.matches.length > 0) {
+ const descMatch = result.matches.find(m => m.key === 'description' || m.key === 'body');
+ if (descMatch && descMatch.indices) {
+ excerptHtml = highlightFuseMatches(excerptText, descMatch.indices);
+ } else {
+ excerptHtml = highlightQuery(excerptText, sanitizedQuery);
+ }
+ } else {
+ excerptHtml = highlightQuery(excerptText, sanitizedQuery);
+ }
+ excerpt.innerHTML = excerptHtml;
+ excerpt.className = 'text-muted small';
+ }
+
+ item.appendChild(link);
+ if (excerptText) {
+ item.appendChild(excerpt);
+ }
+ list.appendChild(item);
+ });
+
+ resultsDiv.innerHTML = '';
+ resultsDiv.appendChild(list);
+ return;
+ } catch (error) {
+ console.error('Fuse.js search error:', error);
+ }
+ } else {
+ loadingDiv.style.display = 'none';
+ const errorMsg = document.createElement('div');
+ errorMsg.className = 'alert alert-warning';
+ errorMsg.innerHTML = 'Search index not loaded. Please refresh the page.
';
+ resultsDiv.innerHTML = '';
+ resultsDiv.appendChild(errorMsg);
+ }
+}
+
+// Helper function to escape HTML
+function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+// Security: Initialize event listeners properly
+(function() {
+ 'use strict';
+
+ document.addEventListener('DOMContentLoaded', function() {
+ const closeBtn = document.getElementById('enhanced-search-close-btn');
+ const searchBtn = document.getElementById('enhanced-search-button');
+ const searchInput = document.getElementById('enhanced-search-input');
+
+ if (closeBtn) {
+ closeBtn.addEventListener('click', closeEnhancedSearch);
+ }
+
+ if (searchBtn) {
+ searchBtn.addEventListener('click', performEnhancedSearch);
+ }
+
+ if (searchInput) {
+ // Security: Input validation
+ searchInput.addEventListener('input', function(e) {
+ if (e.target.value.length > 500) {
+ e.target.value = e.target.value.substring(0, 500);
+ }
+ });
+
+ searchInput.addEventListener('keypress', function(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ performEnhancedSearch();
+ }
+ });
+ }
+
+ // Keyboard shortcut: Ctrl+K or Cmd+K to open enhanced search
+ document.addEventListener('keydown', function(e) {
+ // Security: Only trigger if not typing in an input
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
+ return;
+ }
+
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ openEnhancedSearch();
+ }
+ if (e.key === 'Escape') {
+ closeEnhancedSearch();
+ }
+ });
+ });
+})();
\ No newline at end of file
diff --git a/static/robots.txt b/static/robots.txt
new file mode 100644
index 00000000..fb20fc2a
--- /dev/null
+++ b/static/robots.txt
@@ -0,0 +1,10 @@
+User-agent: *
+Allow: /
+
+# Sitemap
+Sitemap: https://docs.ci.openshift.org/sitemap.xml
+
+# Disallow admin and private areas
+Disallow: /admin/
+Disallow: /private/
+