From 75e2892e36534cb7a77f08fb59f34c76f2eb16e1 Mon Sep 17 00:00:00 2001
From: Amir Husayn Panahifar
Date: Fri, 16 Oct 2026 09:44:23 +0330
Subject: [PATCH] fix: release v0.2.1 lesson deletes and large uploads
- Raise Traefik read timeout to 1h (compose dev/prod + k3s HelmChartConfig)
so admin video uploads up to 200MB no longer 502 mid-upload.
- Migration 0029: cascade watch/quiz history on lesson delete and unlink
dependent lessons, so removing a lesson or chapter stops 500ing.
- Add the Apollo cluster Helm overlay (values-apollo.yaml) for the deployed
environment.
- k3s Traefik chart config also enables access logs, cross-namespace
IngressRoutes, and fixes the Prometheus scrape port.
Bump appVersion/product.go/values tags to 0.2.1; chart version 0.2.3.
---
README.md | 2 +-
backend/internal/api/product.go | 4 +-
.../0029_lesson_delete_cascades.sql | 26 ++++++
deploy/compose/compose.dev.yaml | 3 +
deploy/compose/compose.prod.yaml | 3 +
deploy/helm/pargar/Chart.yaml | 4 +-
deploy/helm/pargar/values-apollo.yaml | 81 +++++++++++++++++++
deploy/helm/pargar/values-prod.yaml | 4 +-
deploy/k3s/traefik-helmchartconfig.yaml | 43 ++++++++++
9 files changed, 163 insertions(+), 7 deletions(-)
create mode 100644 backend/internal/db/migrations/0029_lesson_delete_cascades.sql
create mode 100644 deploy/helm/pargar/values-apollo.yaml
create mode 100644 deploy/k3s/traefik-helmchartconfig.yaml
diff --git a/README.md b/README.md
index c78c2ab..29b345f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
diff --git a/backend/internal/api/product.go b/backend/internal/api/product.go
index 5d09040..d51e05a 100644
--- a/backend/internal/api/product.go
+++ b/backend/internal/api/product.go
@@ -583,7 +583,7 @@ func (s *Server) handleMeta(w http.ResponseWriter, r *http.Request) {
}
func appBuild() (version, built string) {
- version = "0.2.0"
+ version = "0.2.1"
built = "dev"
info, ok := debug.ReadBuildInfo()
if !ok {
@@ -594,7 +594,7 @@ func appBuild() (version, built string) {
built = setting.Value[:10]
}
if setting.Key == "vcs.revision" && len(setting.Value) >= 7 {
- version = "0.2.0+" + setting.Value[:7]
+ version = "0.2.1+" + setting.Value[:7]
}
}
return version, built
diff --git a/backend/internal/db/migrations/0029_lesson_delete_cascades.sql b/backend/internal/db/migrations/0029_lesson_delete_cascades.sql
new file mode 100644
index 0000000..11e4a35
--- /dev/null
+++ b/backend/internal/db/migrations/0029_lesson_delete_cascades.sql
@@ -0,0 +1,26 @@
+-- 0029_lesson_delete_cascades.sql
+-- Make deleting a lesson (directly, or via its chapter) work cleanly.
+--
+-- Previously only mcq_questions cascaded; watch/quiz history blocked the delete
+-- with a 500. Progress/history rows now go away with the lesson, while lessons
+-- that merely *required* the deleted one are unlinked (never deleted).
+
+ALTER TABLE watch_sessions DROP CONSTRAINT IF EXISTS watch_sessions_lesson_id_fkey;
+ALTER TABLE watch_sessions ADD CONSTRAINT watch_sessions_lesson_id_fkey
+ FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE;
+
+ALTER TABLE watch_heartbeats DROP CONSTRAINT IF EXISTS watch_heartbeats_lesson_id_fkey;
+ALTER TABLE watch_heartbeats ADD CONSTRAINT watch_heartbeats_lesson_id_fkey
+ FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE;
+
+ALTER TABLE lesson_progress DROP CONSTRAINT IF EXISTS lesson_progress_lesson_id_fkey;
+ALTER TABLE lesson_progress ADD CONSTRAINT lesson_progress_lesson_id_fkey
+ FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE;
+
+ALTER TABLE quiz_attempts DROP CONSTRAINT IF EXISTS quiz_attempts_lesson_id_fkey;
+ALTER TABLE quiz_attempts ADD CONSTRAINT quiz_attempts_lesson_id_fkey
+ FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE;
+
+ALTER TABLE lessons DROP CONSTRAINT IF EXISTS lessons_requires_lesson_id_fkey;
+ALTER TABLE lessons ADD CONSTRAINT lessons_requires_lesson_id_fkey
+ FOREIGN KEY (requires_lesson_id) REFERENCES lessons(id) ON DELETE SET NULL;
diff --git a/deploy/compose/compose.dev.yaml b/deploy/compose/compose.dev.yaml
index a3ec447..701a709 100644
--- a/deploy/compose/compose.dev.yaml
+++ b/deploy/compose/compose.dev.yaml
@@ -63,6 +63,9 @@ services:
- --entrypoints.web.http.redirections.entrypoint.permanent=true
- --entrypoints.web.http.aliasHeadersStrategy=delete
- --entrypoints.websecure.http.aliasHeadersStrategy=delete
+ # Allow slow, large uploads (admin video ≤ 200MB); default is 60s.
+ - --entrypoints.web.transport.respondingTimeouts.readTimeout=3600
+ - --entrypoints.websecure.transport.respondingTimeouts.readTimeout=3600
# Dev: Traefik default certificate (browser warning expected on localhost).
- --api.dashboard=true
- --api.insecure=true
diff --git a/deploy/compose/compose.prod.yaml b/deploy/compose/compose.prod.yaml
index a3c60c2..db75170 100644
--- a/deploy/compose/compose.prod.yaml
+++ b/deploy/compose/compose.prod.yaml
@@ -71,6 +71,9 @@ services:
- --entrypoints.web.http.redirections.entrypoint.permanent=true
- --entrypoints.web.http.aliasHeadersStrategy=delete
- --entrypoints.websecure.http.aliasHeadersStrategy=delete
+ # Allow slow, large uploads (admin video ≤ 200MB); default is 60s.
+ - --entrypoints.web.transport.respondingTimeouts.readTimeout=3600
+ - --entrypoints.websecure.transport.respondingTimeouts.readTimeout=3600
- --certificatesresolvers.le.acme.email=${ACME_EMAIL:?ACME_EMAIL must be set}
- --certificatesresolvers.le.acme.storage=/data/acme.json
- --certificatesresolvers.le.acme.httpchallenge=true
diff --git a/deploy/helm/pargar/Chart.yaml b/deploy/helm/pargar/Chart.yaml
index 05d8efa..945b67d 100644
--- a/deploy/helm/pargar/Chart.yaml
+++ b/deploy/helm/pargar/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
name: pargar
description: Pargar gated LMS - Go API, Next.js frontend, Postgres, Redis, S3-compatible storage
type: application
-version: 0.2.2
-appVersion: "0.2.0"
+version: 0.2.3
+appVersion: "0.2.1"
keywords:
- lms
- pargar
diff --git a/deploy/helm/pargar/values-apollo.yaml b/deploy/helm/pargar/values-apollo.yaml
new file mode 100644
index 0000000..9f9b2de
--- /dev/null
+++ b/deploy/helm/pargar/values-apollo.yaml
@@ -0,0 +1,81 @@
+backend:
+ autoscaling:
+ enabled: false
+ env:
+ devMode: "false"
+ donationEnabled: "true"
+ otelEndpoint: http://tempo.infra.svc.cluster.local:4318
+ physicalCertEnabled: "false"
+ seedCurriculum: "false"
+ existingSecret: pargar-app
+ image:
+ pullPolicy: IfNotPresent
+ repository: pargar-backend
+ tag: 0.2.1
+ metrics:
+ serviceMonitor:
+ enabled: true
+ interval: 30s
+ podDisruptionBudget:
+ enabled: true
+ minAvailable: 1
+ replicaCount: 3
+ resources:
+ limits:
+ cpu: "1"
+ memory: 512Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+backup:
+ enabled: true
+ persistence:
+ size: 2Gi
+ retentionDays: 7
+ schedule: 0 3 * * *
+certManager:
+ enabled: false
+frontend:
+ autoscaling:
+ enabled: false
+ image:
+ pullPolicy: IfNotPresent
+ repository: pargar-frontend
+ tag: 0.2.1
+ podDisruptionBudget:
+ enabled: false
+ replicaCount: 2
+ resources:
+ limits:
+ cpu: "1"
+ memory: 512Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+global:
+ domain: pargar.panahifar.ir
+ frontendOrigins: https://pargar.panahifar.ir
+ imageRegistry: ""
+ publicUrl: https://pargar.panahifar.ir
+imagePullSecrets: []
+ingress:
+ enabled: true
+ tls:
+ enabled: true
+ existingSecret: pargar-wildcard-tls
+ useIngressRoute: true
+networkPolicy:
+ enabled: false
+postgres:
+ enabled: true
+ persistence:
+ size: 10Gi
+redis:
+ enabled: true
+ persistence:
+ size: 2Gi
+rustfs:
+ consoleEnable: false
+ enabled: true
+ persistence:
+ size: 20Gi
diff --git a/deploy/helm/pargar/values-prod.yaml b/deploy/helm/pargar/values-prod.yaml
index b4afcf4..b38efc4 100644
--- a/deploy/helm/pargar/values-prod.yaml
+++ b/deploy/helm/pargar/values-prod.yaml
@@ -22,7 +22,7 @@ backend:
replicaCount: 2
image:
repository: backend
- tag: "0.2.0"
+ tag: "0.2.1"
pullPolicy: IfNotPresent
existingSecret: pargar-app
env:
@@ -49,7 +49,7 @@ frontend:
replicaCount: 2
image:
repository: frontend
- tag: "0.2.0"
+ tag: "0.2.1"
pullPolicy: IfNotPresent
resources:
requests:
diff --git a/deploy/k3s/traefik-helmchartconfig.yaml b/deploy/k3s/traefik-helmchartconfig.yaml
new file mode 100644
index 0000000..27307ef
--- /dev/null
+++ b/deploy/k3s/traefik-helmchartconfig.yaml
@@ -0,0 +1,43 @@
+# k3s Traefik customisation (HelmChartConfig for the built-in `traefik` chart).
+#
+# Apply with: kubectl apply -f deploy/k3s/traefik-helmchartconfig.yaml
+#
+# Why: Traefik's entrypoint `readTimeout` defaults to 60s and covers reading the
+# *entire request including the body*. Admin video uploads (up to 200MB) that take
+# longer than 60s were cut off with a 502 before reaching the backend. Raise it to
+# 1 hour (use 0 to disable entirely).
+apiVersion: helm.cattle.io/v1
+kind: HelmChartConfig
+metadata:
+ name: traefik
+ namespace: kube-system
+spec:
+ failurePolicy: reinstall
+ valuesContent: |-
+ # ACCESSLOG tile -> ON; access logs go to container stdout (kubectl logs).
+ # Key is logs.access.enabled on the chart k3s ships (40.1.5);
+ # the top-level accessLog.enabled only exists on newer chart versions.
+ logs:
+ access:
+ enabled: true
+ # Allow IngressRoutes to reference Services living in other namespaces,
+ # e.g. argocd-server in `argocd`.
+ providers:
+ kubernetesCRD:
+ allowCrossNamespace: true
+ deployment:
+ podAnnotations:
+ # fix: metrics entrypoint listens on :9100, annotation said 8082
+ prometheus.io/port: "9100"
+ prometheus.io/scrape: "true"
+ # Allow slow, large uploads (admin video upload ≤ 200MB) to finish.
+ # Default is 60s; 3600s = 1h. Set to 0 to disable the cap.
+ ports:
+ web:
+ transport:
+ respondingTimeouts:
+ readTimeout: 3600
+ websecure:
+ transport:
+ respondingTimeouts:
+ readTimeout: 3600