Add helm chart

This commit is contained in:
Jonny Ervine 2025-10-09 22:59:26 +08:00
parent 09d5c79e8d
commit 6886f94888
19 changed files with 1836 additions and 12 deletions

View File

@ -0,0 +1,20 @@
apiVersion: v2
name: motm-app
description: A Helm chart for MOTM (Man of the Match) Hockey Voting Application
type: application
version: 1.0.0
appVersion: "1.0.0"
home: https://github.com/your-org/motm-app
sources:
- https://github.com/your-org/motm-app
maintainers:
- name: Your Name
email: your.email@example.com
keywords:
- flask
- hockey
- voting
- web-application
annotations:
category: Sports
licenses: MIT

View File

@ -0,0 +1,359 @@
# MOTM App Kubernetes Deployment Guide
This guide provides step-by-step instructions for deploying the MOTM (Man of the Match) Hockey Voting Application to a Kubernetes cluster using Helm.
## Prerequisites
### Required Tools
- **Kubernetes Cluster** (version 1.19+)
- **Helm** (version 3.0+)
- **kubectl** (configured for your cluster)
- **Docker** (for building images)
### Required Services
- **PostgreSQL Database** (or MySQL/SQLite)
- **S3-compatible Storage** (optional, for asset management)
## Quick Start
### 1. Build and Push Docker Image
```bash
# Navigate to the application directory
cd /home/jonny/Projects/gcp-hockey-results/motm_app
# Build the Docker image
docker build -t your-registry/motm-app:latest .
# Push to your container registry
docker push your-registry/motm-app:latest
```
### 2. Deploy to Development
```bash
# Navigate to the helm chart directory
cd helm-chart/motm-app
# Update the image repository in values-development.yaml
sed -i 's/your-registry\/motm-app/your-actual-registry\/motm-app/g' values-development.yaml
# Deploy using the deployment script
./scripts/deploy.sh development install
```
### 3. Deploy to Production
```bash
# Update production values
cp values-production.yaml my-production-values.yaml
# Edit my-production-values.yaml with your production settings
# Deploy to production
./scripts/deploy.sh production install
```
## Manual Deployment
### 1. Customize Values
Edit the appropriate values file:
```bash
# For development
vim values-development.yaml
# For production
vim values-production.yaml
```
Key values to update:
- `image.repository`: Your container registry
- `database.host`: Database service name
- `ingress.hosts[0].host`: Your domain name
- `secrets.*`: Database and S3 credentials
### 2. Install with Helm
```bash
# Development
helm install motm-app ./motm-app \
--namespace motm-app \
--values values-development.yaml \
--create-namespace
# Production
helm install motm-app ./motm-app \
--namespace motm-app \
--values values-production.yaml \
--create-namespace
```
## Configuration
### Database Setup
The application supports multiple database types:
#### PostgreSQL (Recommended)
```yaml
database:
type: "postgresql"
host: "postgresql-service"
port: 5432
name: "motm"
username: "motm_user"
```
#### MySQL
```yaml
database:
type: "mysql"
host: "mysql-service"
port: 3306
name: "motm"
username: "motm_user"
```
#### SQLite (Development only)
```yaml
database:
type: "sqlite"
# Other fields ignored for SQLite
```
### S3 Configuration
For asset management (logos, images):
```yaml
s3:
enabled: true
endpoint: "https://s3.amazonaws.com"
region: "us-east-1"
bucket: "motm-assets"
# Credentials set via secrets
```
### Security Configuration
#### Secrets Management
Set secrets via Helm values or external secret management:
```yaml
secrets:
dbPassword: "your-database-password"
s3AccessKey: "your-s3-access-key"
s3SecretKey: "your-s3-secret-key"
```
Or use external secret management:
```bash
# Create secrets manually
kubectl create secret generic motm-app-secrets \
--from-literal=db-password=your-password \
--from-literal=s3-access-key=your-key \
--from-literal=s3-secret-key=your-secret \
--namespace motm-app
```
#### Network Policies
For enhanced security, create network policies:
```yaml
# network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: motm-app-netpol
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: motm-app
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
egress:
- to:
- namespaceSelector:
matchLabels:
name: postgresql
```
## Monitoring and Observability
### Health Checks
The application includes built-in health checks:
- **Liveness Probe**: Checks if the application is running
- **Readiness Probe**: Checks if the application is ready to serve traffic
### Logging
Configure log levels in values:
```yaml
logging:
level: "INFO" # DEBUG, INFO, WARNING, ERROR
format: "json" # json, text
```
### Metrics (Optional)
Add Prometheus metrics endpoint:
```yaml
monitoring:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
```
## Scaling
### Horizontal Pod Autoscaling
Enable HPA for production:
```yaml
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
```
### Resource Limits
Adjust based on your cluster capacity:
```yaml
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 512Mi
```
## Troubleshooting
### Common Issues
#### 1. Pod Not Starting
```bash
# Check pod status
kubectl get pods -n motm-app -l app.kubernetes.io/name=motm-app
# Check pod logs
kubectl logs -n motm-app -l app.kubernetes.io/name=motm-app
```
#### 2. Database Connection Issues
```bash
# Test database connectivity
kubectl exec -n motm-app -it deployment/motm-app -- python -c "
from database import sql_read_static
from sqlalchemy import text
try:
result = sql_read_static(text('SELECT 1'))
print('Database connection successful')
except Exception as e:
print(f'Database connection failed: {e}')
"
```
#### 3. S3 Connection Issues
```bash
# Check S3 configuration
kubectl exec -n motm-app -it deployment/motm-app -- cat /app/s3_config.json
```
#### 4. Ingress Issues
```bash
# Check ingress status
kubectl get ingress -n motm-app
# Check ingress controller logs
kubectl logs -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
```
### Debugging Commands
```bash
# Get all resources
kubectl get all -n motm-app
# Describe deployment
kubectl describe deployment -n motm-app motm-app
# Check events
kubectl get events -n motm-app --sort-by='.lastTimestamp'
# Port forward for local testing
kubectl port-forward -n motm-app svc/motm-app 8080:80
```
## Maintenance
### Updates
```bash
# Update application
helm upgrade motm-app ./motm-app \
--namespace motm-app \
--values values-production.yaml
# Rollback if needed
helm rollback motm-app 1 --namespace motm-app
```
### Backup
```bash
# Backup database (PostgreSQL)
kubectl exec -n postgresql postgresql-0 -- pg_dump -U motm_user motm > backup.sql
# Backup application data
kubectl exec -n motm-app deployment/motm-app -- tar -czf /tmp/data-backup.tar.gz /app/data
kubectl cp motm-app/deployment/motm-app:/tmp/data-backup.tar.gz ./data-backup.tar.gz
```
### Cleanup
```bash
# Uninstall application
helm uninstall motm-app --namespace motm-app
# Delete namespace (if no other resources)
kubectl delete namespace motm-app
```
## Security Best Practices
1. **Use Non-Root Containers**: The application runs as non-root user (UID 1000)
2. **Read-Only Root Filesystem**: Enable in production values
3. **Network Policies**: Implement to restrict pod-to-pod communication
4. **RBAC**: Use dedicated service accounts with minimal permissions
5. **Secret Management**: Use external secret management solutions
6. **Image Security**: Scan images for vulnerabilities
7. **TLS**: Enable TLS for all ingress traffic
8. **Resource Limits**: Set appropriate CPU and memory limits
## Support
For issues and questions:
1. Check the application logs
2. Review Kubernetes events
3. Consult the Helm chart documentation
4. Create an issue in the repository

View File

@ -0,0 +1,239 @@
# MOTM App Helm Chart
This Helm chart deploys the MOTM (Man of the Match) Hockey Voting Application to a Kubernetes cluster.
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- PostgreSQL database (or MySQL/SQLite)
- S3-compatible storage (optional)
## Installation
### 1. Build and Push Docker Image
First, build and push your Docker image to a registry:
```bash
# Build the image
docker build -t your-registry/motm-app:latest .
# Push to registry
docker push your-registry/motm-app:latest
```
### 2. Configure Values
Copy the default values file and customize it:
```bash
cp values.yaml my-values.yaml
```
Key values to update in `my-values.yaml`:
```yaml
# Image configuration
image:
repository: your-registry/motm-app
tag: "latest"
# Database configuration
database:
host: "your-postgresql-service"
name: "motm"
username: "motm_user"
# S3 configuration (if using S3)
s3:
enabled: true
endpoint: "https://s3.amazonaws.com"
bucket: "your-bucket-name"
# Ingress configuration
ingress:
enabled: true
hosts:
- host: motm.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: motm-app-tls
hosts:
- motm.yourdomain.com
# Secrets (set these via --set or separate secret management)
secrets:
dbPassword: "your-db-password"
s3AccessKey: "your-s3-access-key"
s3SecretKey: "your-s3-secret-key"
```
### 3. Deploy with Helm
#### Option A: Using values file
```bash
helm install motm-app ./motm-app -f my-values.yaml
```
#### Option B: Using command line parameters
```bash
helm install motm-app ./motm-app \
--set image.repository=your-registry/motm-app \
--set database.host=your-postgresql-service \
--set ingress.hosts[0].host=motm.yourdomain.com \
--set secrets.dbPassword=your-db-password
```
#### Option C: Using external secret management
If using external secret management (e.g., Sealed Secrets, External Secrets Operator), create the secrets separately and set:
```yaml
secrets:
dbPassword: "" # Will be managed externally
s3AccessKey: "" # Will be managed externally
s3SecretKey: "" # Will be managed externally
```
## Configuration
### Database Setup
The application supports PostgreSQL, MySQL, and SQLite. Configure your database connection in the values file:
```yaml
database:
type: "postgresql" # postgresql, mysql, or sqlite
host: "postgresql-service"
port: 5432
name: "motm"
username: "motm_user"
```
### S3 Configuration
Configure S3-compatible storage for asset management:
```yaml
s3:
enabled: true
endpoint: "https://s3.amazonaws.com"
region: "us-east-1"
bucket: "motm-assets"
```
### Resource Limits
Adjust resource limits based on your cluster capacity:
```yaml
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
```
### Autoscaling
Enable horizontal pod autoscaling:
```yaml
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
```
## Upgrading
To upgrade the application:
```bash
helm upgrade motm-app ./motm-app -f my-values.yaml
```
## Uninstalling
To uninstall the application:
```bash
helm uninstall motm-app
```
## Troubleshooting
### Check Pod Status
```bash
kubectl get pods -l app.kubernetes.io/name=motm-app
```
### View Logs
```bash
kubectl logs -l app.kubernetes.io/name=motm-app
```
### Check Service
```bash
kubectl get svc -l app.kubernetes.io/name=motm-app
```
### Debug Database Connection
```bash
kubectl exec -it deployment/motm-app -- python -c "
from database import sql_read_static
from sqlalchemy import text
try:
result = sql_read_static(text('SELECT 1'))
print('Database connection successful')
except Exception as e:
print(f'Database connection failed: {e}')
"
```
## Values Reference
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `image.repository` | string | `"your-registry/motm-app"` | Image repository |
| `image.tag` | string | `"latest"` | Image tag |
| `service.type` | string | `"ClusterIP"` | Service type |
| `ingress.enabled` | bool | `true` | Enable ingress |
| `database.type` | string | `"postgresql"` | Database type |
| `database.host` | string | `"postgresql-service"` | Database host |
| `s3.enabled` | bool | `true` | Enable S3 storage |
| `resources.limits.cpu` | string | `"500m"` | CPU limit |
| `resources.limits.memory` | string | `"512Mi"` | Memory limit |
## Security Considerations
1. **Secrets Management**: Use proper secret management solutions (e.g., Sealed Secrets, External Secrets Operator)
2. **Network Policies**: Implement network policies to restrict pod-to-pod communication
3. **RBAC**: Configure proper RBAC for service accounts
4. **Image Security**: Use non-root containers and scan images for vulnerabilities
5. **TLS**: Enable TLS for ingress and internal communication
## Monitoring
The chart includes basic health checks. For production deployments, consider adding:
- Prometheus metrics endpoint
- ServiceMonitor for Prometheus Operator
- Grafana dashboards
- Alerting rules
## Support
For issues and questions, please refer to the application documentation or create an issue in the repository.

View File

@ -0,0 +1,256 @@
#!/bin/bash
# MOTM App Helm Deployment Script
# Usage: ./deploy.sh [environment] [action]
# Environment: development, staging, production
# Action: install, upgrade, uninstall, template
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Default values
ENVIRONMENT=${1:-development}
ACTION=${2:-install}
RELEASE_NAME="motm-app"
CHART_PATH="./motm-app"
NAMESPACE="motm-app"
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to check prerequisites
check_prerequisites() {
print_status "Checking prerequisites..."
# Check if helm is installed
if ! command -v helm &> /dev/null; then
print_error "Helm is not installed. Please install Helm 3.0+"
exit 1
fi
# Check if kubectl is installed
if ! command -v kubectl &> /dev/null; then
print_error "kubectl is not installed. Please install kubectl"
exit 1
fi
# Check if we can connect to Kubernetes cluster
if ! kubectl cluster-info &> /dev/null; then
print_error "Cannot connect to Kubernetes cluster"
exit 1
fi
print_success "Prerequisites check passed"
}
# Function to create namespace
create_namespace() {
print_status "Creating namespace: $NAMESPACE"
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
print_success "Namespace created/verified"
}
# Function to validate values file
validate_values() {
local values_file="values-${ENVIRONMENT}.yaml"
if [[ ! -f "$values_file" ]]; then
print_error "Values file $values_file not found"
exit 1
fi
print_status "Validating values file: $values_file"
# Check for required values
if ! grep -q "repository:" "$values_file"; then
print_error "Image repository not specified in $values_file"
exit 1
fi
if ! grep -q "host:" "$values_file"; then
print_error "Database host not specified in $values_file"
exit 1
fi
print_success "Values file validation passed"
}
# Function to template the chart
template_chart() {
local values_file="values-${ENVIRONMENT}.yaml"
print_status "Generating Kubernetes manifests..."
helm template $RELEASE_NAME $CHART_PATH \
--namespace $NAMESPACE \
--values $values_file \
--output-dir ./generated-manifests
print_success "Manifests generated in ./generated-manifests/"
}
# Function to install the chart
install_chart() {
local values_file="values-${ENVIRONMENT}.yaml"
print_status "Installing MOTM App in $ENVIRONMENT environment..."
helm install $RELEASE_NAME $CHART_PATH \
--namespace $NAMESPACE \
--values $values_file \
--create-namespace \
--wait \
--timeout 10m
print_success "MOTM App installed successfully"
# Show status
kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=motm-app
}
# Function to upgrade the chart
upgrade_chart() {
local values_file="values-${ENVIRONMENT}.yaml"
print_status "Upgrading MOTM App in $ENVIRONMENT environment..."
helm upgrade $RELEASE_NAME $CHART_PATH \
--namespace $NAMESPACE \
--values $values_file \
--wait \
--timeout 10m
print_success "MOTM App upgraded successfully"
# Show status
kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=motm-app
}
# Function to uninstall the chart
uninstall_chart() {
print_warning "Uninstalling MOTM App from $ENVIRONMENT environment..."
read -p "Are you sure you want to uninstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
helm uninstall $RELEASE_NAME --namespace $NAMESPACE
print_success "MOTM App uninstalled successfully"
else
print_status "Uninstall cancelled"
fi
}
# Function to show status
show_status() {
print_status "Showing MOTM App status..."
echo "=== Helm Release Status ==="
helm status $RELEASE_NAME --namespace $NAMESPACE
echo -e "\n=== Pods Status ==="
kubectl get pods -n $NAMESPACE -l app.kubernetes.io/name=motm-app
echo -e "\n=== Services ==="
kubectl get svc -n $NAMESPACE -l app.kubernetes.io/name=motm-app
echo -e "\n=== Ingress ==="
kubectl get ingress -n $NAMESPACE -l app.kubernetes.io/name=motm-app
}
# Function to show logs
show_logs() {
print_status "Showing MOTM App logs..."
kubectl logs -n $NAMESPACE -l app.kubernetes.io/name=motm-app --tail=100 -f
}
# Function to show help
show_help() {
echo "MOTM App Helm Deployment Script"
echo ""
echo "Usage: $0 [environment] [action]"
echo ""
echo "Environments:"
echo " development Deploy to development environment"
echo " staging Deploy to staging environment"
echo " production Deploy to production environment"
echo ""
echo "Actions:"
echo " install Install the application (default)"
echo " upgrade Upgrade the application"
echo " uninstall Uninstall the application"
echo " template Generate Kubernetes manifests"
echo " status Show application status"
echo " logs Show application logs"
echo " help Show this help message"
echo ""
echo "Examples:"
echo " $0 development install"
echo " $0 production upgrade"
echo " $0 staging template"
echo " $0 development status"
}
# Main execution
main() {
case $ACTION in
install)
check_prerequisites
create_namespace
validate_values
install_chart
;;
upgrade)
check_prerequisites
create_namespace
validate_values
upgrade_chart
;;
uninstall)
check_prerequisites
uninstall_chart
;;
template)
check_prerequisites
validate_values
template_chart
;;
status)
check_prerequisites
show_status
;;
logs)
check_prerequisites
show_logs
;;
help|--help|-h)
show_help
;;
*)
print_error "Unknown action: $ACTION"
show_help
exit 1
;;
esac
}
# Run main function
main "$@"

View File

@ -0,0 +1,65 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "motm-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "motm-app.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 "motm-app.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "motm-app.labels" -}}
helm.sh/chart: {{ include "motm-app.chart" . }}
{{ include "motm-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.labels }}
{{- toYaml . | nindent 0 }}
{{- end }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "motm-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "motm-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "motm-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "motm-app.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,43 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "motm-app.fullname" . }}-config
labels:
{{- include "motm-app.labels" . | nindent 4 }}
data:
database_config.ini: |
[DATABASE]
type = {{ .Values.database.type }}
sqlite_database_path = hockey_results.db
[MYSQL]
host = {{ .Values.database.host }}
port = {{ .Values.database.port }}
database = {{ .Values.database.name }}
username = {{ .Values.database.username }}
charset = utf8mb4
[POSTGRESQL]
host = {{ .Values.database.host }}
port = {{ .Values.database.port }}
database = {{ .Values.database.name }}
username = {{ .Values.database.username }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "motm-app.fullname" . }}-s3-config
labels:
{{- include "motm-app.labels" . | nindent 4 }}
data:
s3_config.json: |
{
"enable_s3": {{ .Values.s3.enabled }},
"aws_access_key_id": "",
"aws_secret_access_key": "",
"aws_region": "{{ .Values.s3.region }}",
"bucket_name": "{{ .Values.s3.bucket }}",
"endpoint_url": "{{ .Values.s3.endpoint }}",
"use_ssl": true,
"verify_ssl": true
}

View File

@ -0,0 +1,134 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "motm-app.fullname" . }}
labels:
{{- include "motm-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount | default 1 }}
{{- end }}
selector:
matchLabels:
{{- include "motm-app.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "motm-app.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "motm-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 5000
protocol: TCP
env:
# Application environment variables
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
# Database configuration
- name: DB_HOST
value: {{ .Values.database.host | quote }}
- name: DB_PORT
value: {{ .Values.database.port | quote }}
- name: DB_NAME
value: {{ .Values.database.name | quote }}
- name: DB_USER
value: {{ .Values.database.username | quote }}
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "motm-app.fullname" . }}-secrets
key: db-password
# S3 configuration
{{- if .Values.s3.enabled }}
- name: S3_ENDPOINT
value: {{ .Values.s3.endpoint | quote }}
- name: S3_REGION
value: {{ .Values.s3.region | quote }}
- name: S3_BUCKET
value: {{ .Values.s3.bucket | quote }}
- name: S3_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: {{ include "motm-app.fullname" . }}-secrets
key: s3-access-key
- name: S3_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ include "motm-app.fullname" . }}-secrets
key: s3-secret-key
{{- end }}
livenessProbe:
httpGet:
path: {{ .Values.healthCheck.path }}
port: http
initialDelaySeconds: {{ .Values.healthCheck.initialDelaySeconds }}
periodSeconds: {{ .Values.healthCheck.periodSeconds }}
timeoutSeconds: {{ .Values.healthCheck.timeoutSeconds }}
failureThreshold: {{ .Values.healthCheck.failureThreshold }}
readinessProbe:
httpGet:
path: {{ .Values.healthCheck.path }}
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config-volume
mountPath: /app/database_config.ini
subPath: database_config.ini
- name: s3-config-volume
mountPath: /app/s3_config.json
subPath: s3_config.json
{{- if .Values.persistence.enabled }}
- name: data-volume
mountPath: /app/data
{{- end }}
volumes:
- name: config-volume
configMap:
name: {{ include "motm-app.fullname" . }}-config
- name: s3-config-volume
configMap:
name: {{ include "motm-app.fullname" . }}-s3-config
{{- if .Values.persistence.enabled }}
- name: data-volume
persistentVolumeClaim:
claimName: {{ include "motm-app.fullname" . }}-pvc
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@ -0,0 +1,32 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "motm-app.fullname" . }}
labels:
{{- include "motm-app.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "motm-app.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,59 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "motm-app.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class")) }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "motm-app.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,18 @@
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "motm-app.fullname" . }}
labels:
{{- include "motm-app.labels" . | nindent 4 }}
spec:
{{- if .Values.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- end }}
{{- if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "motm-app.selectorLabels" . | nindent 6 }}
{{- end }}

View File

@ -0,0 +1,21 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "motm-app.fullname" . }}-pvc
labels:
{{- include "motm-app.labels" . | nindent 4 }}
spec:
accessModes:
- {{ .Values.persistence.accessMode }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- if .Values.persistence.storageClass }}
{{- if (eq "-" .Values.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: "{{ .Values.persistence.storageClass }}"
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,29 @@
apiVersion: v1
kind: Secret
metadata:
name: {{ include "motm-app.fullname" . }}-secrets
labels:
{{- include "motm-app.labels" . | nindent 4 }}
type: Opaque
data:
# Database password
{{- if .Values.secrets.dbPassword }}
db-password: {{ .Values.secrets.dbPassword | b64enc | quote }}
{{- else }}
db-password: {{ "changeme" | b64enc | quote }}
{{- end }}
{{- if .Values.s3.enabled }}
# S3 credentials
{{- if .Values.secrets.s3AccessKey }}
s3-access-key: {{ .Values.secrets.s3AccessKey | b64enc | quote }}
{{- else }}
s3-access-key: {{ "changeme" | b64enc | quote }}
{{- end }}
{{- if .Values.secrets.s3SecretKey }}
s3-secret-key: {{ .Values.secrets.s3SecretKey | b64enc | quote }}
{{- else }}
s3-secret-key: {{ "changeme" | b64enc | quote }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "motm-app.fullname" . }}
labels:
{{- include "motm-app.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "motm-app.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "motm-app.serviceAccountName" . }}
labels:
{{- include "motm-app.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,128 @@
# Development values for MOTM App
# Use this file for development/staging environments
# Application Configuration
app:
name: motm-app-dev
version: "dev"
# Image Configuration
image:
repository: your-registry/motm-app
tag: "dev" # Use dev tag for development
pullPolicy: Always # Always pull latest dev image
# Resource Limits for Development (lighter)
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 50m
memory: 128Mi
# No autoscaling for development
autoscaling:
enabled: false
# Pod Disruption Budget
podDisruptionBudget:
enabled: false
# Security Context (more permissive for development)
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false # Allow writing for development
runAsNonRoot: true
runAsUser: 1000
# Service Configuration
service:
type: ClusterIP
port: 80
targetPort: 5000
# Ingress Configuration for Development
ingress:
enabled: true
className: "nginx"
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false" # No SSL for dev
cert-manager.io/cluster-issuer: "letsencrypt-staging"
hosts:
- host: motm-dev.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: motm-app-dev-tls
hosts:
- motm-dev.yourdomain.com
# Database Configuration for Development
database:
type: "postgresql"
host: "postgresql-dev-service"
port: 5432
name: "motm_dev"
username: "motm_user"
# S3 Configuration for Development
s3:
enabled: true
endpoint: "https://s3.amazonaws.com"
region: "us-east-1"
bucket: "motm-assets-dev"
# Environment Variables for Development
env:
FLASK_ENV: "development"
FLASK_APP: "main.py"
FLASK_RUN_HOST: "0.0.0.0"
FLASK_RUN_PORT: "5000"
PYTHONUNBUFFERED: "1"
PYTHONDONTWRITEBYTECODE: "1"
FLASK_DEBUG: "1" # Enable debug mode for development
# Health Checks
healthCheck:
enabled: true
path: "/"
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 5
# Persistence for Development
persistence:
enabled: true
storageClass: "standard" # Use standard storage class
accessMode: ReadWriteOnce
size: 1Gi
# Monitoring (disabled for development)
monitoring:
enabled: false
# Logging
logging:
level: "DEBUG"
format: "text"
# Labels and Annotations
labels:
environment: "development"
team: "development"
annotations:
deployment.kubernetes.io/revision: "dev"
podLabels:
environment: "development"
podAnnotations:
debug: "true"

View File

@ -0,0 +1,166 @@
# Production values for MOTM App
# Use this file as a template for production deployment
# Application Configuration
app:
name: motm-app
version: "1.0.0"
# Image Configuration
image:
repository: your-registry/motm-app
tag: "v1.0.0" # Use specific version tags in production
pullPolicy: IfNotPresent
# Resource Limits for Production
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 200m
memory: 512Mi
# Autoscaling for Production
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
# Pod Disruption Budget
podDisruptionBudget:
enabled: true
minAvailable: 1
# Security Context
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
# Service Configuration
service:
type: ClusterIP
port: 80
targetPort: 5000
# Ingress Configuration for Production
ingress:
enabled: true
className: "nginx"
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/rate-limit-window: "1m"
hosts:
- host: motm.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: motm-app-tls
hosts:
- motm.yourdomain.com
# Database Configuration
database:
type: "postgresql"
host: "postgresql-primary-service"
port: 5432
name: "motm_prod"
username: "motm_user"
# S3 Configuration for Production
s3:
enabled: true
endpoint: "https://s3.amazonaws.com"
region: "us-east-1"
bucket: "motm-assets-prod"
# Environment Variables
env:
FLASK_ENV: "production"
FLASK_APP: "main.py"
FLASK_RUN_HOST: "0.0.0.0"
FLASK_RUN_PORT: "5000"
PYTHONUNBUFFERED: "1"
PYTHONDONTWRITEBYTECODE: "1"
# Health Checks
healthCheck:
enabled: true
path: "/"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Persistence for Production
persistence:
enabled: true
storageClass: "fast-ssd" # Use fast storage class
accessMode: ReadWriteOnce
size: 10Gi
# Monitoring
monitoring:
enabled: true
serviceMonitor:
enabled: true
interval: 30s
scrapeTimeout: 10s
# Logging
logging:
level: "INFO"
format: "json"
# Node Selector for Production
nodeSelector:
node-type: "production"
# Tolerations
tolerations:
- key: "production"
operator: "Equal"
value: "true"
effect: "NoSchedule"
# Affinity Rules
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- motm-app
topologyKey: kubernetes.io/hostname
# Labels and Annotations
labels:
environment: "production"
team: "platform"
annotations:
deployment.kubernetes.io/revision: "1"
podLabels:
environment: "production"
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "5000"
prometheus.io/path: "/metrics"

View File

@ -0,0 +1,194 @@
# Default values for motm-app
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Application Configuration
app:
name: motm-app
version: "1.0.0"
description: "MOTM Hockey Voting Application"
# Image Configuration
image:
repository: your-registry/motm-app
tag: "latest"
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
# tag: ""
# Image pull secrets
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# Service Account
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# Pod Security Context
podSecurityContext:
fsGroup: 1000
runAsNonRoot: true
runAsUser: 1000
# Container Security Context
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 1000
# Service Configuration
service:
type: ClusterIP
port: 80
targetPort: 5000
annotations: {}
# Ingress Configuration
ingress:
enabled: true
className: ""
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: motm.yourdomain.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: motm-app-tls
hosts:
- motm.yourdomain.com
# Resource Limits and Requests
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 256Mi
# Autoscaling
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
# Node Selector
nodeSelector: {}
# Tolerations
tolerations: []
# Affinity
affinity: {}
# Pod Disruption Budget
podDisruptionBudget:
enabled: false
minAvailable: 1
# Database Configuration
database:
type: "postgresql" # postgresql, mysql, sqlite
host: "postgresql-service"
port: 5432
name: "motm"
username: "motm_user"
# Password should be set via secret
# password: ""
# S3 Configuration
s3:
enabled: true
endpoint: "https://s3.amazonaws.com"
region: "us-east-1"
bucket: "motm-assets"
accessKeyId: ""
secretAccessKey: ""
# These should be set via secret
# Environment Variables
env:
FLASK_ENV: "production"
FLASK_APP: "main.py"
FLASK_RUN_HOST: "0.0.0.0"
FLASK_RUN_PORT: "5000"
PYTHONUNBUFFERED: "1"
PYTHONDONTWRITEBYTECODE: "1"
# ConfigMap for application configuration
configMap:
databaseConfig: |
[DATABASE]
type = {{ .Values.database.type }}
[MYSQL]
host = {{ .Values.database.host }}
port = {{ .Values.database.port }}
database = {{ .Values.database.name }}
username = {{ .Values.database.username }}
[POSTGRESQL]
host = {{ .Values.database.host }}
port = {{ .Values.database.port }}
database = {{ .Values.database.name }}
username = {{ .Values.database.username }}
# Secrets
secrets:
# Database password
dbPassword: ""
# S3 credentials
s3AccessKey: ""
s3SecretKey: ""
# Health Checks
healthCheck:
enabled: true
path: "/"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
# Persistence
persistence:
enabled: false
# storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
# Monitoring
monitoring:
enabled: false
serviceMonitor:
enabled: false
interval: 30s
scrapeTimeout: 10s
# Logging
logging:
level: "INFO"
format: "json"
# Labels and Annotations
labels: {}
annotations: {}
podLabels: {}
podAnnotations: {}

View File

@ -136,14 +136,15 @@
margin-bottom: 4px;
position: relative;
overflow: hidden;
background-color: #e9ecef; /* Light gray background for empty bars */
}
.poty-bar-motm {
background: linear-gradient(90deg, #28a745, #20c997);
background-color: rgba(40, 167, 69, 0.1); /* Very light green background */
}
.poty-bar-dotd {
background: linear-gradient(90deg, #dc3545, #fd7e14);
background-color: rgba(220, 53, 69, 0.1); /* Very light red background */
}
.poty-bar-fill {
@ -238,7 +239,7 @@ function renderPOTYChart(data) {
return a.dotdTotal - b.dotdTotal;
});
// Find max values for scaling - each bar scales independently
// Find max values for independent scaling - each bar type scales to its own maximum
const maxMotm = Math.max(...data.map(p => p.motmTotal || 0));
const maxDotd = Math.max(...data.map(p => p.dotdTotal || 0));
@ -249,6 +250,9 @@ function renderPOTYChart(data) {
const ranking = index + 1;
const rankingClass = ranking === 1 ? 'gold' : ranking === 2 ? 'silver' : ranking === 3 ? 'bronze' : '';
const motmVotes = player.motmTotal || 0;
const dotdVotes = player.dotdTotal || 0;
html += '<div class="poty-player-card">';
html += '<div class="d-flex align-items-center">';
html += '<div class="poty-ranking ' + rankingClass + '">' + ranking + '</div>';
@ -256,19 +260,19 @@ function renderPOTYChart(data) {
html += '<div class="poty-player-name">' + (player.playerName || 'Unknown Player') + '</div>';
html += '<div class="poty-stats">';
html += '<div class="poty-stat">';
html += '<div class="poty-stat-value text-success">' + (player.motmTotal || 0) + '</div>';
html += '<div class="poty-stat-value text-success">' + motmVotes + '</div>';
html += '<div class="poty-stat-label">MOTM</div>';
html += '</div>';
html += '<div class="poty-stat">';
html += '<div class="poty-stat-value text-danger">' + (player.dotdTotal || 0) + '</div>';
html += '<div class="poty-stat-value text-danger">' + dotdVotes + '</div>';
html += '<div class="poty-stat-label">DotD</div>';
html += '</div>';
html += '<div class="poty-bar-container">';
html += '<div class="poty-bar poty-bar-motm">';
html += '<div class="poty-bar-fill" style="width: ' + (maxMotm > 0 ? ((player.motmTotal || 0) / maxMotm * 100) : 0) + '%"></div>';
html += '<div class="poty-bar-fill" style="width: ' + (maxMotm > 0 ? (motmVotes / maxMotm * 100) : 0) + '%"></div>';
html += '</div>';
html += '<div class="poty-bar poty-bar-dotd">';
html += '<div class="poty-bar-fill" style="width: ' + (maxDotd > 0 ? ((player.dotdTotal || 0) / maxDotd * 100) : 0) + '%"></div>';
html += '<div class="poty-bar-fill" style="width: ' + (maxDotd > 0 ? (dotdVotes / maxDotd * 100) : 0) + '%"></div>';
html += '</div>';
html += '</div>';
html += '</div>';

View File

@ -136,14 +136,15 @@
margin-bottom: 4px;
position: relative;
overflow: hidden;
background-color: #e9ecef; /* Light gray background for empty bars */
}
.vote-bar-motm {
background: linear-gradient(90deg, #28a745, #20c997);
background-color: rgba(40, 167, 69, 0.1); /* Very light green background */
}
.vote-bar-dotd {
background: linear-gradient(90deg, #dc3545, #fd7e14);
background-color: rgba(220, 53, 69, 0.1); /* Very light red background */
}
.vote-bar-fill {
@ -250,12 +251,14 @@ function renderVoteChart(data) {
return aDotd - bDotd;
});
// Find max values for scaling - each bar scales independently
// Find max values for independent scaling - each bar type scales to its own maximum
const maxMotm = Math.max(...data.map(p => p[motmKey] || 0));
const maxDotd = Math.max(...data.map(p => p[dotdKey] || 0));
let html = '';
console.log('Max values calculated:', {maxMotm, maxDotd});
data.forEach((player, index) => {
console.log('Processing vote player:', player);
const ranking = index + 1;
@ -264,6 +267,8 @@ function renderVoteChart(data) {
const motmVotes = player[motmKey] || 0;
const dotdVotes = player[dotdKey] || 0;
console.log(`${player.playerName}: MOTM=${motmVotes}, DotD=${dotdVotes}`);
html += '<div class="vote-player-card">';
html += '<div class="d-flex align-items-center">';
html += '<div class="vote-ranking ' + rankingClass + '">' + ranking + '</div>';
@ -278,12 +283,18 @@ function renderVoteChart(data) {
html += '<div class="vote-stat-value text-danger">' + dotdVotes + '</div>';
html += '<div class="vote-stat-label">DotD</div>';
html += '</div>';
// Calculate percentages with debugging
const motmPercent = maxMotm > 0 ? (motmVotes / maxMotm * 100) : 0;
const dotdPercent = maxDotd > 0 ? (dotdVotes / maxDotd * 100) : 0;
console.log(`${player.playerName} percentages: MOTM=${motmPercent.toFixed(1)}%, DotD=${dotdPercent.toFixed(1)}%`);
html += '<div class="vote-bar-container">';
html += '<div class="vote-bar vote-bar-motm">';
html += '<div class="vote-bar-fill" style="width: ' + (maxMotm > 0 ? (motmVotes / maxMotm * 100) : 0) + '%"></div>';
html += '<div class="vote-bar-fill" style="width: ' + motmPercent + '%"></div>';
html += '</div>';
html += '<div class="vote-bar vote-bar-dotd">';
html += '<div class="vote-bar-fill" style="width: ' + (maxDotd > 0 ? (dotdVotes / maxDotd * 100) : 0) + '%"></div>';
html += '<div class="vote-bar-fill" style="width: ' + dotdPercent + '%"></div>';
html += '</div>';
html += '</div>';
html += '</div>';
@ -294,6 +305,11 @@ function renderVoteChart(data) {
container.innerHTML = html;
// Verify bar widths are set correctly
setTimeout(() => {
verifyBarWidths();
}, 100);
// Animate bars after a short delay
setTimeout(() => {
const bars = container.querySelectorAll('.vote-bar-fill');
@ -326,5 +342,15 @@ function testVoteChart() {
console.log('Testing with sample vote data:', testData);
renderVoteChart(testData);
}
// Add a simple test to verify bar widths are being set
function verifyBarWidths() {
const bars = document.querySelectorAll('.vote-bar-fill');
console.log('Found bars:', bars.length);
bars.forEach((bar, index) => {
const width = bar.style.width;
console.log(`Bar ${index}: width = ${width}`);
});
}
</script>
{% endblock %}