- Added REPO_INVENTORY.md with all repos, branches, remotes, and staging info - Added .gitignore - Synced all existing docs from local workspace - Centralized documentation hub for GrowQR team
67 KiB
GrowQR Staging VPS Service Deployment Runbook
Date: 2026-06-05
VPS SSH alias: gqr-temp
VPS IP: 168.144.123.127
Staging wildcard DNS: *.gqr.puter.wtf
Deployment root on VPS: /opt/growqr
User instructions followed
- Deploy services one by one to the staging VPS.
- For each service repo:
- Check Git status, current branch, remotes, and Gitea remote.
- If deployable work exists on a non-main branch, create a
stagingbranch from the current branch. - Push
stagingto Gitea. - Deploy the
stagingbranch/content to the VPS.
- Most repos are expected to have a Gitea remote named
gitea; if not, handle case by case. - Copy the local
.envfile to the VPS deployment. - Use Caddy as reverse proxy for APIs.
- Keep room for many services/ports:
- bind service/internal ports to
127.0.0.1only - expose publicly only through Caddy subdomains
- bind service/internal ports to
- Use the wildcard DNS under
*.gqr.puter.wtffor service URLs.
VPS base setup performed
Installed required packages on gqr-temp:
apt-get update
apt-get install -y git docker.io docker-compose caddy ca-certificates curl
systemctl enable --now docker caddy
Notes:
- Debian 13 package name was
docker-compose, notdocker-compose-plugin. - Caddy is managed by systemd.
- Docker Compose command available as
docker-compose.
Git deployment branch procedure
For each service repo locally:
cd <service-repo>
git status --short --branch
git remote -v
git fetch --all --prune
git remote show gitea
git rev-list --left-right --count HEAD...gitea/main
git ls-remote --heads gitea staging origin staging
If there is no existing staging branch and the current branch contains the intended deployable changes:
git switch -c staging
git push -u gitea staging
Source transfer procedure used
Initial direct HTTPS clone from Gitea on the VPS failed because the remote requires credentials:
fatal: could not read Username for 'https://git.openputer.com': No such device or address
So we deployed by archiving the local staging worktree and copying it to the VPS:
cd <service-repo>
COPYFILE_DISABLE=1 tar \
--exclude='./.venv' \
--exclude='./__pycache__' \
--exclude='*/__pycache__' \
--exclude='./.pytest_cache' \
-czf /tmp/<service>-staging.tgz .
scp /tmp/<service>-staging.tgz gqr-temp:/tmp/
ssh gqr-temp '
rm -rf /opt/growqr/<service>
mkdir -p /opt/growqr/<service>
tar --no-same-owner -xzf /tmp/<service>-staging.tgz -C /opt/growqr/<service>
cd /opt/growqr/<service>
find . -name "._*" -type f -delete
chown -R root:root .
chmod 600 .env
git status --short --branch
'
Important cleanup:
- macOS archive metadata files like
._*were deleted on the VPS. .envpermissions were set to600.- Ownership was normalized to
root:root.
Port allocation strategy
To leave public port space clean, service ports were patched on the VPS so Docker only binds to loopback.
Pattern:
| Purpose | Port range style |
|---|---|
| API public reverse-proxy backend | 127.0.0.1:180xx |
| Postgres internal/debug only | 127.0.0.1:154xx |
| MinIO API internal/debug only | 127.0.0.1:190xx |
| MinIO console internal/debug only | 127.0.0.1:190xx+1 |
Only Caddy listens publicly on 80/443.
Caddy procedure
For each service, append a site block to /etc/caddy/Caddyfile:
<service-subdomain>.gqr.puter.wtf {
encode gzip zstd
reverse_proxy 127.0.0.1:<local-api-port>
}
Then validate, format, and reload:
cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak.$(date +%Y%m%d%H%M%S)
caddy fmt --overwrite /etc/caddy/Caddyfile
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
Caddy automatically obtained Let's Encrypt certificates via the wildcard DNS-resolved subdomains.
Service 1: interview-service
Local repo status
Path:
/Users/puter/Workspace/growqr/interview-service
Before staging:
- Branch:
dashboard-service-rest-integration - Worktree: clean
- Gitea remote:
https://git.openputer.com/growqr-app/interview-service.git - GitHub origin also exists:
https://github.com/GrowQR-Code/interview-service.git - Gitea default branch:
main - Current branch was 1 commit ahead of
gitea/main - No existing
stagingbranch found
Created and pushed:
cd interview-service
git switch -c staging
git push -u gitea staging
VPS deployment
Remote path:
/opt/growqr/interview-service
Patched Docker Compose bindings on VPS:
api: 127.0.0.1:18007 -> 8000
postgres: 127.0.0.1:15440 -> 5432
minio: 127.0.0.1:19000 -> 9000
minio: 127.0.0.1:19001 -> 9001
Also changed Redis URL for this staging compose from host Redis to internal compose Redis:
REDIS_URL: redis://redis:6379/0
Started with:
cd /opt/growqr/interview-service
docker-compose -f docker-compose.yml -f docker-compose.staging.yml up -d --build
Caddy URL:
https://interview-staging.gqr.puter.wtf
Health check:
curl -fsS https://interview-staging.gqr.puter.wtf/health
Expected response:
{"status":"ok","service":"interview-service","a2a":true,"agent":true}
Final container state:
interview-service-api-1 healthy 127.0.0.1:18007->8000
interview-service-postgres-1 healthy 127.0.0.1:15440->5432
interview-service-minio-1 healthy 127.0.0.1:19000->9000, 127.0.0.1:19001->9001
interview-service-redis-1 healthy internal only
Service 2: roleplay-service
Local repo status
Path:
/Users/puter/Workspace/growqr/roleplay-service
Before staging:
- Branch:
dashboard-service-rest-integration - Worktree: clean
- Gitea remote:
https://git.openputer.com/growqr-app/roleplay-service.git - GitHub origin also exists:
https://github.com/GrowQR-Code/roleplay-service.git - Gitea default branch:
main - Current branch was 1 commit ahead of
gitea/main - No existing
stagingbranch found
Created and pushed:
cd roleplay-service
git switch -c staging
git push -u gitea staging
VPS deployment
Remote path:
/opt/growqr/roleplay-service
Patched Docker Compose bindings on VPS:
api: 127.0.0.1:18008 -> 8000
postgres: 127.0.0.1:15441 -> 5432
minio: 127.0.0.1:19010 -> 9000
minio: 127.0.0.1:19011 -> 9001
Started with:
cd /opt/growqr/roleplay-service
docker-compose -f docker-compose.yml -f docker-compose.staging.yml up -d --build
Caddy URL:
https://roleplay-staging.gqr.puter.wtf
Health check:
curl -fsS https://roleplay-staging.gqr.puter.wtf/health
Expected response:
{"status":"ok","service":"roleplay-service","a2a":true,"agent":true}
Final container state:
roleplay-service-api-1 healthy 127.0.0.1:18008->8000
roleplay-service-postgres-1 healthy 127.0.0.1:15441->5432
roleplay-service-minio-1 healthy 127.0.0.1:19010->9000, 127.0.0.1:19011->9001
qscore_service deployment (2026-06-05)
Local Git state
Repository:
/Users/puter/Workspace/growqr/qscore_service
Observed before deployment:
current branch: feature/quotients-from-pillars
modified: docker-compose.yml
untracked: .DS_Store, uv.lock
local .env: not present
Gitea remote was present as SSH fetch URL but SSH push failed with public-key auth. The push URL was changed to HTTPS and staging was pushed successfully:
cd qscore_service
git switch -C staging
git remote set-url --push gitea https://git.openputer.com/puter/qscore-service.git
git push -u gitea staging
VPS deployment
Remote path:
/opt/growqr/qscore_service
No local .env existed, so .env.example was copied to .env on the VPS and locked down with chmod 600.
Patched Docker Compose bindings on VPS:
api: 127.0.0.1:18009 -> 8000
postgres: 127.0.0.1:15442 -> 5432
redis: 127.0.0.1:16380 -> 6379
Started with:
cd /opt/growqr/qscore_service
docker-compose -p qscore-service-staging up -d --build
Caddy URL:
https://qscore-staging.gqr.puter.wtf
Health check:
curl -fsS https://qscore-staging.gqr.puter.wtf/health
Expected response:
{"status":"ok"}
Final container state:
qscore-service-staging-api-1 up 127.0.0.1:18009->8000
qscore-service-staging-postgres-1 healthy 127.0.0.1:15442->5432
qscore-service-staging-redis-1 healthy 127.0.0.1:16380->6379
qscore-service-staging-worker-1 up
growqr-app resume-builder and user-service deployment (2026-06-05)
These services live inside the monorepo:
/Users/puter/Workspace/growqr/growqr-app
Local Git state
Current branch before staging was dashboard-service-rest-integration, with modified service files under resume-builder/ and user-service/.
Created/pushed monorepo staging branch:
cd growqr-app
git switch -C staging
git push -u gitea staging
user-service VPS deployment
Remote path:
/opt/growqr/user-service
Copied local .env to the VPS and locked it down with chmod 600.
Patched Docker Compose bindings on VPS:
api: 127.0.0.1:18011 -> 8003
postgres: 127.0.0.1:15444 -> 5432
redis: 127.0.0.1:16382 -> 6379
Staging service URLs patched in compose:
PUBLIC_BASE_URL=https://user-staging.gqr.puter.wtf
RESUME_SERVICE_URL=https://resume-staging.gqr.puter.wtf
QSCORE_SERVICE_URL=https://qscore-staging.gqr.puter.wtf
Started with:
cd /opt/growqr/user-service
docker-compose -p user-service-staging up -d --build
Caddy URL:
https://user-staging.gqr.puter.wtf
Health check:
curl -fsS https://user-staging.gqr.puter.wtf/health
Expected response:
{"status":"healthy","service":"user-service","mcp":true,"agent":true,"a2a":true}
resume-builder VPS deployment
Remote path:
/opt/growqr/resume-builder
Copied local .env to the VPS and locked it down with chmod 600.
Patched Docker Compose bindings on VPS:
api: 127.0.0.1:18010 -> 8000
postgres: 127.0.0.1:15443 -> 5432
redis: internal compose network only, exposed as 6379/tcp inside Docker
Patched USER_SERVICE_URL in compose:
USER_SERVICE_URL=https://user-staging.gqr.puter.wtf
Started with:
cd /opt/growqr/resume-builder
docker-compose -p resume-builder-staging up -d --build
Caddy URL:
https://resume-staging.gqr.puter.wtf
Health check:
curl -fsS https://resume-staging.gqr.puter.wtf/health
Expected response:
{"status":"healthy","service":"resume-builder","mcp":true,"agent":true,"a2a":true}
Final container state:
growqr_resume_api healthy 127.0.0.1:18010->8000
growqr_resume_db healthy 127.0.0.1:15443->5432
growqr_resume_redis up 6379/tcp
growqr_user_service up 127.0.0.1:18011->8003
growqr_users_db healthy 127.0.0.1:15444->5432
growqr_users_redis up 127.0.0.1:16382->6379
growqr-backend deployment
Local repo:
/Users/puter/Workspace/growqr/growqr-backend
Branch and commit deployed:
staging @ d10ef2a feat: personalize home feed suggestions
Started from chore/release, committed the local home-feed changes, created staging, and pushed to Gitea:
cd /Users/puter/Workspace/growqr/growqr-backend
npm run typecheck
git switch -c staging
git add src/home/home-feed-agent.ts src/home/home-feed.ts src/home/types.ts src/routes/home.ts
git commit -m "feat: personalize home feed suggestions"
git push -u origin staging
Remote path:
/opt/growqr/growqr-backend
Copied local .env to the VPS via the tar deployment and locked it down with chmod 600.
Patched Docker Compose bindings on VPS:
backend api: 127.0.0.1:18012 -> 4000
postgres: 127.0.0.1:15445 -> 5432
gitea http: 127.0.0.1:13001 -> 3000
gitea ssh: 127.0.0.1:12222 -> 2222
rivet api: 127.0.0.1:16420 -> 6420
rivet guard: 127.0.0.1:16421 -> 6421
Patched staging URLs in .env/compose:
GITEA_PUBLIC_URL=https://backend-gitea-staging.gqr.puter.wtf
GITEA_ROOT_URL=https://backend-gitea-staging.gqr.puter.wtf
RIVET_CLIENT_ENDPOINT=https://backend-staging.gqr.puter.wtf/api/rivet
INTERVIEW_SERVICE_URL=https://interview-staging.gqr.puter.wtf
ROLEPLAY_SERVICE_URL=https://roleplay-staging.gqr.puter.wtf
QSCORE_SERVICE_URL=https://qscore-staging.gqr.puter.wtf
RESUME_SERVICE_URL=https://resume-staging.gqr.puter.wtf
FRONTEND_ORIGIN=https://dashboard-staging.gqr.puter.wtf
Started with:
cd /opt/growqr/growqr-backend
docker-compose -p growqr-backend-staging up -d --build
The first backend boot failed because the fresh Postgres volume had no tables. Applied migrations from the compiled runtime image:
docker-compose -p growqr-backend-staging run --rm backend node dist/db/migrate.js
docker-compose -p growqr-backend-staging up -d backend
Caddy URLs:
https://backend-staging.gqr.puter.wtf
https://backend-gitea-staging.gqr.puter.wtf
Health checks:
curl -fsS https://backend-staging.gqr.puter.wtf/healthz
curl -fsS https://backend-gitea-staging.gqr.puter.wtf/api/v1/version
Expected responses:
{"ok":true}
{"version":"1.22.6"}
growqr-dashboard frontend deployment
Local repo:
/Users/puter/Workspace/growqr/growqr-dashboard
Branch deployed:
staging @ 45ce954 updates
growqr-dashboard was on main ahead of origin/main by one commit. Created and pushed staging to Gitea:
cd /Users/puter/Workspace/growqr/growqr-dashboard
npm run build
git switch -c staging
git push -u origin staging
Remote path:
/opt/growqr/growqr-dashboard
Copied local .env to the VPS and locked it down with chmod 600.
Patched staging env values on VPS:
NEXT_PUBLIC_RIVET_ENDPOINT=https://backend-staging.gqr.puter.wtf/api/rivet
NEXT_PUBLIC_GROWQR_BACKEND_URL=https://backend-staging.gqr.puter.wtf
GROWQR_BACKEND_URL=https://backend-staging.gqr.puter.wtf
NEXT_PUBLIC_INTERVIEW_WS_URL=wss://interview-staging.gqr.puter.wtf/api/v1
NEXT_PUBLIC_ROLEPLAY_WS_URL=wss://roleplay-staging.gqr.puter.wtf/api/v1
Created Dockerfile.staging and docker-compose.staging.yml on the VPS. Dashboard binds only to localhost:
dashboard: 127.0.0.1:13000 -> 3000
Build note: npm ci failed because @clerk/nextjs@7.4.2 peer requirements conflict with next@14.2.18, while local build already uses the existing dependency resolution. The staging Dockerfile uses:
npm install --legacy-peer-deps
Started with:
cd /opt/growqr/growqr-dashboard
docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build
Caddy URL:
https://dashboard-staging.gqr.puter.wtf
Verification:
curl -fsSI https://dashboard-staging.gqr.puter.wtf
Expected status:
HTTP/2 200
Verification commands
Check all running services:
ssh gqr-temp 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
Check local listening ports:
ssh gqr-temp 'ss -ltnp | grep -E ":(80|443|13000|18007|18008|18009|18010|18011|18012|15440|15441|15442|15443|15444|15445|16380|16382|19000|19001|19010|19011|12222|13001|16420|16421)" || true'
Check Caddy logs:
ssh gqr-temp 'journalctl -u caddy --no-pager -n 80'
Check service logs:
ssh gqr-temp 'cd /opt/growqr/interview-service && docker-compose -f docker-compose.yml -f docker-compose.staging.yml logs --tail=100 api'
ssh gqr-temp 'cd /opt/growqr/roleplay-service && docker-compose -f docker-compose.yml -f docker-compose.staging.yml logs --tail=100 api'
ssh gqr-temp 'cd /opt/growqr/qscore_service && docker-compose -p qscore-service-staging logs --tail=100 api'
ssh gqr-temp 'cd /opt/growqr/user-service && docker-compose -p user-service-staging logs --tail=100 user-service'
ssh gqr-temp 'cd /opt/growqr/resume-builder && docker-compose -p resume-builder-staging logs --tail=100 api'
ssh gqr-temp 'cd /opt/growqr/growqr-backend && docker-compose -p growqr-backend-staging logs --tail=100 backend'
Service dependency map
Staging services point at each other via public HTTPS URLs (not host.docker.internal):
dashboard-staging.gqr.puter.wtf
├─→ backend-staging.gqr.puter.wtf (API + Gitea + Rivet)
├─→ interview-staging.gqr.puter.wtf
├─→ roleplay-staging.gqr.puter.wtf
├─→ resume-staging.gqr.puter.wtf
├─→ user-staging.gqr.puter.wtf
└─→ qscore-staging.gqr.puter.wtf
backend-staging.gqr.puter.wtf
├─→ interview-staging.gqr.puter.wtf
├─→ roleplay-staging.gqr.puter.wtf
├─→ qscore-staging.gqr.puter.wtf
└─→ resume-staging.gqr.puter.wtf
user-service-staging
├─→ resume-staging.gqr.puter.wtf
└─→ qscore-staging.gqr.puter.wtf
resume-builder-staging
└─→ user-staging.gqr.puter.wtf
Port allocation reference
| Service | API | Postgres | Redis | MinIO API | MinIO Console | Other |
|---|---|---|---|---|---|---|
| interview-service | 18007 | 15440 | internal | 19000 | 19001 | — |
| roleplay-service | 18008 | 15441 | internal | 19010 | 19011 | — |
| qscore_service | 18009 | 15442 | 16380 | — | — | — |
| resume-builder | 18010 | 15443 | internal | — | — | — |
| user-service | 18011 | 15444 | 16382 | — | — | — |
| growqr-backend | 18012 | 15445 | — | — | — | Gitea 13001/12222, Rivet 16420/16421 |
| growqr-dashboard | 13000 | — | — | — | — | — |
Public ports: only Caddy on 80/443.
Health check endpoints quick reference
| Service | Endpoint | Expected response |
|---|---|---|
| interview-service | GET /health |
{"status":"ok","service":"interview-service","a2a":true,"agent":true} |
| roleplay-service | GET /health |
{"status":"ok","service":"roleplay-service","a2a":true,"agent":true} |
| qscore_service | GET /health |
{"status":"ok"} |
| resume-builder | GET /health |
{"status":"healthy","service":"resume-builder","mcp":true,"agent":true,"a2a":true} |
| user-service | GET /health |
{"status":"healthy","service":"user-service","mcp":true,"agent":true,"a2a":true} |
| growqr-backend | GET /healthz |
{"ok":true} |
| growqr-gitea | GET /api/v1/version |
{"version":"1.22.6"} |
| growqr-dashboard | GET / (root page) |
HTTP/2 200 |
Post-reboot / resize recovery
After a VPS reboot or resize, Docker containers with restart: unless-stopped or compose restart policies may not auto-start if they were not running when systemd shut down. To recover all services:
# Backend services
ssh gqr-temp '
cd /opt/growqr/interview-service && docker-compose -f docker-compose.yml -f docker-compose.staging.yml up -d
cd /opt/growqr/roleplay-service && docker-compose -f docker-compose.yml -f docker-compose.staging.yml up -d
cd /opt/growqr/qscore_service && docker-compose -p qscore-service-staging up -d
cd /opt/growqr/user-service && docker-compose -p user-service-staging up -d
cd /opt/growqr/resume-builder && docker-compose -p resume-builder-staging up -d
cd /opt/growqr/growqr-backend && docker-compose -p growqr-backend-staging up -d
cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d
'
Also ensure Caddy is running:
ssh gqr-temp 'systemctl is-active caddy || systemctl start caddy'
Common issues & fixes encountered
1. Gitea clone requires credentials on VPS
Direct HTTPS clone from git.openputer.com fails because the VPS has no stored credentials. Fix: archive the local worktree, scp it, and extract on the VPS.
2. Gitea SSH push fails with public-key auth
qscore_service had a Gitea SSH remote but push failed. Fix: switch the push URL to HTTPS:
git remote set-url --push gitea https://git.openputer.com/puter/qscore-service.git
3. Fresh backend Postgres has no tables
On first growqr-backend deploy, the backend container crashed with:
PostgresError: relation "user_stacks" does not exist
Fix: run the compiled migration script inside the built image before starting the backend:
cd /opt/growqr/growqr-backend
docker-compose -p growqr-backend-staging run --rm backend node dist/db/migrate.js
docker-compose -p growqr-backend-staging up -d backend
4. Next.js peer dependency conflict during Docker build
npm ci failed because @clerk/nextjs@7.4.2 peer requirements conflict with next@14.2.18. Fix in the Dockerfile:
RUN npm install --legacy-peer-deps
5. VPS overloaded during Next.js build
The initial VPS size (1 vCPU / 2 GB RAM) caused the next build to hang and the VPS became unreachable. Fix: resize the VPS to at least 4 vCPU / 8 GB RAM for frontend builds, or build locally and copy only the .next output.
6. Caddy certificate issuance delay
New subdomains may take 10–30 seconds for Let's Encrypt TLS-ALPN-01 validation. During that window, curl may return SSL: TLS alert internal error. Fix: wait for Caddy logs to show certificate obtained successfully before verifying HTTPS.
Environment variable staging checklist
When deploying a new service, the following env vars usually need changing from localhost/host.docker.internal to staging URLs:
GITEA_PUBLIC_URL/GITEA_ROOT_URL→https://backend-gitea-staging.gqr.puter.wtfRIVET_CLIENT_ENDPOINT→https://backend-staging.gqr.puter.wtf/api/rivetGROWQR_BACKEND_URL/NEXT_PUBLIC_GROWQR_BACKEND_URL→https://backend-staging.gqr.puter.wtfFRONTEND_ORIGIN→https://dashboard-staging.gqr.puter.wtfINTERVIEW_SERVICE_URL/NEXT_PUBLIC_INTERVIEW_WS_URL→https://interview-staging.gqr.puter.wtf/wss://interview-staging.gqr.puter.wtfROLEPLAY_SERVICE_URL/NEXT_PUBLIC_ROLEPLAY_WS_URL→https://roleplay-staging.gqr.puter.wtf/wss://roleplay-staging.gqr.puter.wtfQSCORE_SERVICE_URL→https://qscore-staging.gqr.puter.wtfRESUME_SERVICE_URL→https://resume-staging.gqr.puter.wtfUSER_SERVICE_URL/PUBLIC_BASE_URL→https://user-staging.gqr.puter.wtfDATABASE_URL/ Redis URLs → use internal compose service names (postgres,redis) unless binding to host for debugging
Docker build tips
- Always add a
.dockerignoreon the VPS to reduce build context:
node_modules
.next
.git
screenshots
tsconfig.tsbuildinfo
.DS_Store
._*
*.tgz
- For Node.js projects, if
package-lock.jsonis stale,npm cimay fail. Usenpm install --legacy-peer-depsas a fallback. - For Next.js frontends, the build requires significant RAM. If the VPS is small, consider building locally with
npm run buildand copying only the.next/directory andpublic/to the VPS.
Things to preserve for future services
- Always check Git state before deployment.
- Prefer Gitea remote
giteafor staging branches. - Create
stagingbranch from the currently validated deploy branch. - Push
stagingto Gitea before deploying. - Copy
.envto the VPS with restrictive permissions. - Bind Docker service ports to
127.0.0.1, not0.0.0.0. - Use unique local port ranges for each service.
- Expose APIs only through Caddy subdomains.
- Validate Caddy before reload.
- Confirm both localhost health and public HTTPS health.
- Keep deployment directories under
/opt/growqr/<service>. - After a VPS resize/reboot, remember to restart all stopped Docker Compose stacks.
- Run database migrations for new backends before starting the API container.
Final staging polish: auth, CORS, service URLs, and restart policy
Performed on 2026-06-05 after the dashboard exposed a stale localhost:8007 browser request and backend /home/feed returned 500s from miswired user-service calls.
Canonical staging URLs now used consistently:
| Surface | URL |
|---|---|
| Dashboard | https://dashboard-staging.gqr.puter.wtf |
| Backend API | https://backend-staging.gqr.puter.wtf |
| Backend Gitea | https://backend-gitea-staging.gqr.puter.wtf |
| Interview | https://interview-staging.gqr.puter.wtf |
| Roleplay | https://roleplay-staging.gqr.puter.wtf |
| QScore | https://qscore-staging.gqr.puter.wtf |
| Resume Builder | https://resume-staging.gqr.puter.wtf |
| User Service | https://user-staging.gqr.puter.wtf |
Applied consistency fixes:
- Copied the same Clerk publishable/secret/JWKS/issuer values from local envs into all deployed service envs that use or may validate Clerk tokens.
- Set
NODE_ENV=productionfor backend/dashboard andENV=productionfor FastAPI-style staging services. - Replaced stale downstream URLs (
localhost,host.docker.internal) with staging HTTPS URLs where cross-service calls happen through Caddy. - Set CORS origins to include at least
https://dashboard-staging.gqr.puter.wtfandhttps://backend-staging.gqr.puter.wtfacross services. - Patched dashboard leaderboard/artifact defaults so browser code falls back to
/api/growqr/services/*, nothttp://localhost:8007/:8008. - Rebuilt
growqr-dashboardsoNEXT_PUBLIC_*values were baked into the production bundle. - Recreated service containers with updated env and set Docker restart policy to
unless-stoppedfor deployed service containers.
Verification commands:
# No baked frontend localhost service URLs
ssh gqr-temp 'docker exec growqr-dashboard sh -lc "grep -R \"localhost:800[2378]\|127.0.0.1:800[2378]\" -n .next/static .next/server 2>/dev/null | head || true"'
# Public health checks
for url in \
https://dashboard-staging.gqr.puter.wtf/ \
https://backend-staging.gqr.puter.wtf/healthz \
https://backend-gitea-staging.gqr.puter.wtf/api/v1/version \
https://interview-staging.gqr.puter.wtf/health \
https://roleplay-staging.gqr.puter.wtf/health \
https://qscore-staging.gqr.puter.wtf/health \
https://resume-staging.gqr.puter.wtf/health \
https://user-staging.gqr.puter.wtf/health; do
curl -ksS -o /tmp/out -w "%{http_code} %{url_effective}\n" "$url"
head -c 160 /tmp/out; echo
done
# CORS preflight example
curl -ksSI -X OPTIONS https://backend-staging.gqr.puter.wtf/healthz \
-H 'Origin: https://dashboard-staging.gqr.puter.wtf' \
-H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: authorization,content-type'
Expected state after polish:
- All public health checks return
200. - CORS preflights include
access-control-allow-origin: https://dashboard-staging.gqr.puter.wtf. growqr-backendcan fetch health from user, resume, interview, roleplay, and qscore via their staging HTTPS URLs.- Dashboard production bundle contains no
localhost:8007,localhost:8008,127.0.0.1:8007, or127.0.0.1:8008service URLs.
Additional actor/runtime polish:
- Added/passed
RIVET_RUNNER_VERSION=staging-20260605-polishfor the backend image/build so RivetKit self-hosted runner stops warning about unversioned actors. - Verified
growqr-rivetis running and backend starts without theRIVET_RUNNER_VERSION is not setwarning. - Verified backend
/home/feedreturns200with a trusted service token smoke test after the user-service URL fix.
Staging fix: onboarding QScore baseline and Clerk avatar image loading
Performed on 2026-06-05 after a fresh-account onboarding showed three different Q Score states:
- onboarding completion screen: static
QX Score · 35 - dashboard header: home-feed fallback score
47 /agents/qscore:Not ready/Not computed yet
Code changes deployed:
| Repo | Staging commit | Purpose |
|---|---|---|
growqr-backend |
213987a fix: persist onboarding qscore baseline |
Persists the onboarding baseline as a real Q Score projection when onboarding is completed and the account has no existing Q Score signals. |
growqr-dashboard |
08f0250 fix: align qscore header and avatar images |
Removes the fake header default score, shows an awaiting state until the backend returns a score, and bypasses Next image optimization for Clerk avatars. |
Backend behavior after this fix:
- On
PATCH /users/me, ifpreferences.onboarding.completed_atis present and the user has no existing Q Score signals/projection, backend seeds:grow_qscore_latest.signal_id = onboarding.completed_baselinescore = 35source = onboardinggrow_qscore_projection_state.score = 35
/home/feedalso lazily seeds the same baseline from user-service preferences before building the identity rail./services/qscore/currentalso lazily seeds before reading the Q Score page data, so direct navigation to/agents/qscorebecomes consistent after a fresh onboarding.- Existing users with any Q Score signal or non-zero projection are not overwritten.
Dashboard behavior after this fix:
- The top bar no longer initializes QX Score to the fake
76or keeps stale fallback values after failed fetches. - Header score now renders
—/Awaiting baselineuntil the backend returns a real score. - Clerk avatars use
next/imagewithunoptimizedso remote Clerk/Google avatar URLs do not fail through Next's image optimizer. next.config.mjsalso allows common Clerk/Google/Gravatar remote hosts for other optimized image usage.
Deployment commands used:
# Local verification before deploy
cd /Users/puter/Workspace/growqr/growqr-backend
npm run typecheck
npm run build
cd /Users/puter/Workspace/growqr/growqr-dashboard
npm run build
# Push staging branches
cd /Users/puter/Workspace/growqr/growqr-backend
git push origin staging
cd /Users/puter/Workspace/growqr/growqr-dashboard
git push origin staging
# Copy changed files without overwriting remote .env / compose staging patches
scp growqr-backend/src/events/onboarding-qscore.ts gqr-temp:/opt/growqr/growqr-backend/src/events/onboarding-qscore.ts
scp growqr-backend/src/routes/users.ts growqr-backend/src/routes/services.ts gqr-temp:/opt/growqr/growqr-backend/src/routes/
scp growqr-backend/src/home/home-feed.ts gqr-temp:/opt/growqr/growqr-backend/src/home/
scp growqr-dashboard/components/layout/TopBar.tsx gqr-temp:/opt/growqr/growqr-dashboard/components/layout/TopBar.tsx
scp growqr-dashboard/next.config.mjs gqr-temp:/opt/growqr/growqr-dashboard/next.config.mjs
# Rebuild/recreate only affected services
ssh gqr-temp 'cd /opt/growqr/growqr-backend && docker-compose -p growqr-backend-staging build backend && docker-compose -p growqr-backend-staging up -d backend'
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml build dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d dashboard'
Verification commands:
ssh gqr-temp 'docker ps --filter name=growqr-backend --filter name=growqr-dashboard --format "{{.Names}} {{.Status}}"'
ssh gqr-temp 'curl -fsS http://127.0.0.1:18012/healthz && echo'
curl -fsS https://backend-staging.gqr.puter.wtf/healthz
curl -fsSI https://dashboard-staging.gqr.puter.wtf | head -n 1
# Ensure new backend helper is in the running image
ssh gqr-temp 'docker exec growqr-backend sh -lc "grep -R \"completed onboarding baseline\" -n /app/dist | head -5"'
# Ensure old fake dashboard score initializer is gone from built assets
ssh gqr-temp 'docker exec growqr-dashboard sh -lc "grep -R \"useState(76)\" -n /app/.next/server /app/.next/static 2>/dev/null || true"'
Expected user-facing result:
- A fresh onboarded account that has no service activity should consistently show Q Score
35in the onboarding baseline, dashboard header, and Q Score page after refresh/navigation. - Once real service signals arrive, the backend projection replaces the empty state with the computed readiness score and signal ledger.
- The top-right account image should render instead of showing a broken optimized image response.
Staging fix: hide mission mocks in production and fix AppShell sidebar avatar
Performed on 2026-06-05 after the staging Missions page showed mock Interview-to-Offer Accelerator data to a fresh production-like account.
Code changes deployed:
| Repo | Staging commit | Purpose |
|---|---|---|
growqr-dashboard |
c7bbfb8 fix: hide demo missions in production |
Gates mission mock fallbacks behind NEXT_PUBLIC_SHOW_DEMO_MISSIONS=true; production/staging now render real backend data or empty states. |
growqr-dashboard |
95f0e75 fix: render Clerk avatars without optimizer |
Applies the same Clerk avatar image fix to the AppShell sidebar and the top-bar dropdown avatar. |
Behavior after this fix:
/missions/activeno longer falls back toMOCK_ACTIVE_SNAPSHOTSunlessNEXT_PUBLIC_SHOW_DEMO_MISSIONS=true./missions/availableno longer falls back toMOCK_AVAILABLE_MISSIONSunlessNEXT_PUBLIC_SHOW_DEMO_MISSIONS=true./missions/[id]no longer resolves mock mission details unless the demo flag is enabled.- Production/staging empty state for no active missions:
Nothing active yetwith a CTA to browse available missions. - Production/staging empty state for no available missions:
No missions available yet. - Sidebar and account dropdown Clerk avatars use
next/imagewithunoptimizedto avoid broken Next image optimizer responses for Clerk/remote account-image URLs.
Demo mode:
# Only set this in local/dev/demo deployments, not staging production-like deployments.
NEXT_PUBLIC_SHOW_DEMO_MISSIONS=true
Deployment commands used:
cd /Users/puter/Workspace/growqr/growqr-dashboard
npm run build
git push origin staging
scp 'growqr-dashboard/app/missions/(list)/active/page.tsx' 'gqr-temp:/opt/growqr/growqr-dashboard/app/missions/(list)/active/page.tsx'
scp 'growqr-dashboard/app/missions/(list)/available/page.tsx' 'gqr-temp:/opt/growqr/growqr-dashboard/app/missions/(list)/available/page.tsx'
scp 'growqr-dashboard/app/missions/[id]/page.tsx' 'gqr-temp:/opt/growqr/growqr-dashboard/app/missions/[id]/page.tsx'
scp growqr-dashboard/lib/demo-flags.ts gqr-temp:/opt/growqr/growqr-dashboard/lib/demo-flags.ts
scp growqr-dashboard/components/layout/Sidebar.tsx growqr-dashboard/components/layout/TopBar.tsx gqr-temp:/opt/growqr/growqr-dashboard/components/layout/
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml build dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d dashboard'
Verification commands:
ssh gqr-temp 'docker ps --filter name=growqr-dashboard --format "{{.Names}} {{.Status}}"'
curl -ksS -o /dev/null -w "%{http_code}\n" https://dashboard-staging.gqr.puter.wtf/
# Staging should not set this flag.
ssh gqr-temp 'docker exec growqr-dashboard sh -lc "printenv NEXT_PUBLIC_SHOW_DEMO_MISSIONS || true; printenv NODE_ENV"'
# The active missions build should include the flag-gated path, not unconditional mock fallback.
ssh gqr-temp 'docker exec growqr-dashboard sh -lc "grep -R \"NEXT_PUBLIC_SHOW_DEMO_MISSIONS\|showDemoMissions\" -n /app/.next/server/app/missions /app/.next/static/chunks/app/missions 2>/dev/null | head"'
Staging fix: resume upload parsing state
Performed on 2026-06-05 after uploaded resumes showed only Uploaded, while the background parser was still working, and clicking Edit could surface a blocking Failed to parse resume browser alert.
Code change deployed:
| Repo | Staging commit | Purpose |
|---|---|---|
growqr-dashboard |
be9671e fix: show resume parsing state |
Treats uploaded resumes as parsing/processing, polls the resume API until parsed, and replaces blocking alerts with an in-UI parsing status modal. |
Behavior after this fix:
- A newly uploaded resume with
status=uploadednow displays asParsingwith a spinner. - The resume card shows a processing overlay instead of implying it is ready.
- The resume list polls
/resumesevery 3s while any resume remainsuploadedor an edit-triggered parse is open. - Clicking Edit on an uploaded resume opens a non-blocking
Preparing resumemodal. - If parse-on-demand fails while background parsing is still underway, the UI keeps the card visible, shows the message inline, and continues polling instead of using
alert(). - When the API reports the resume has moved out of
uploaded, the modal clears and the card becomes editable.
Deployment commands used:
cd /Users/puter/Workspace/growqr/growqr-dashboard
npm run build
git push origin staging
scp growqr-dashboard/components/resume/ResumeCard.tsx growqr-dashboard/components/resume/ResumeList.tsx gqr-temp:/opt/growqr/growqr-dashboard/components/resume/
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml build dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d dashboard'
Verification commands:
ssh gqr-temp 'docker ps --filter name=growqr-dashboard --format "{{.Names}} {{.Status}}"'
curl -ksS -o /dev/null -w "%{http_code}\n" https://dashboard-staging.gqr.puter.wtf/
ssh gqr-temp 'docker exec growqr-dashboard sh -lc "grep -R \"We.ll unlock editing\|Large PDFs can take\|Parsing resume\" -n /app/.next/static/chunks /app/.next/server 2>/dev/null | head"'
Dashboard staging UI review deploy — PRM-32/33/34/37/44
Date: 2026-06-05
Repo: /Users/puter/Workspace/growqr/growqr-dashboard
Branch: staging
Commit: 61d0bea fix: polish dashboard review items
Remote: origin → https://git.openputer.com/puter/growqr-dashboard.git
VPS path: /opt/growqr/growqr-dashboard
Compose project: growqr-dashboard-staging
Container: growqr-dashboard
Public URL: https://dashboard-staging.gqr.puter.wtf
Changes deployed:
- PRM-33: Added
GettingStartedHeroaboveAgentSquad; uses Clerk user data,useDay(), andgetRewards(day)for streak; camera button opens ClerkUserProfileviaopenUserProfile(). - PRM-44: Added global
growqr:toggle-chatevent wiring and mountedHelperChatTogglefromAppShell; TopBar chat button dispatches the event and collapses label on mobile. - PRM-37: Added
QScoreWheeltoIdentityRail; growth drivers display weight %, score %, chevron affordance, and clickable rows. - PRM-34: Standardized resume and cover-letter template thumbnail previews to
aspect-[8.5/11]. - PRM-32: Added
docs/user-qr-code-plan.mddesign plan.
Local verification:
cd /Users/puter/Workspace/growqr/growqr-dashboard
npm run build
# Result: compiled successfully; zero build errors.
git diff --check
# Result: clean.
Deployment commands used:
cd /Users/puter/Workspace/growqr/growqr-dashboard
git add app/page.tsx components/AppShell.tsx \
components/coverLetter/CoverLetterEditor/tabs/CoverLetterDesignTab.tsx \
components/home/IdentityRail.tsx components/home/GettingStartedHero.tsx \
components/layout/HelperChatToggle.tsx components/layout/TopBar.tsx \
components/qscore/QScoreWheel.tsx \
components/resume/ResumeEditor/tabs/DesignTab.tsx \
components/resume/TemplateCard.tsx docs/user-qr-code-plan.md
git commit -m "fix: polish dashboard review items"
git push origin staging
TMP=/tmp/growqr-dashboard-staging-$(date +%s).tar.gz
tar --exclude='.git' --exclude='.next' --exclude='node_modules' --exclude='.env' --exclude='._*' -czf "$TMP" .
scp "$TMP" gqr-temp:/tmp/growqr-dashboard-staging.tar.gz
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && tar -xzf /tmp/growqr-dashboard-staging.tar.gz && find . -name "._*" -type f -delete && chown -R root:root . && chmod 600 .env && echo 61d0bea > .deployed-dashboard-git-sha'
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build'
Post-deploy verification:
curl -I http://127.0.0.1:13000/
curl -I https://dashboard-staging.gqr.puter.wtf/
ssh gqr-temp 'docker inspect -f "{{.Name}} {{.State.Status}} {{.State.Running}} restart={{.HostConfig.RestartPolicy.Name}}" growqr-dashboard'
ssh gqr-temp 'docker exec growqr-dashboard sh -lc "find /app/.next -path /app/.next/cache -prune -o -type f -print | xargs grep -n \"localhost:8007\\|localhost:8008\\|127.0.0.1:8007\\|127.0.0.1:8008\" 2>/dev/null || true"'
Results:
- Local and remote Docker builds succeeded.
- Public dashboard returned
HTTP/2 200. - Local dashboard returned
HTTP/1.1 200 OKon127.0.0.1:13000. - Container running with
restart=unless-stopped. - Runtime bundle audit excluding
.next/cachefound no oldlocalhost:8007/8008service references.
Note: Docker build emitted transient Google Fonts fetch retry messages, then completed successfully.
Dashboard idle/data-fetching stability fix (2026-06-05)
Root cause found in staging: dashboard server-side proxy requests were going from the dashboard container to https://backend-staging.gqr.puter.wtf, causing public HTTPS/Caddy hairpin timeouts from inside Docker after idle/reconnect periods. This made /api/growqr/* intermittently return backend_timeout, which caused the home/resume UI to replace real state with empty fallback state.
Fix applied:
- Dashboard runtime env now keeps public client URLs public, but uses internal Docker DNS for server-side proxying:
NEXT_PUBLIC_GROWQR_BACKEND_URL=https://backend-staging.gqr.puter.wtfGROWQR_BACKEND_URL=http://growqr-backend:4000
/opt/growqr/growqr-dashboard/docker-compose.staging.ymlattaches the dashboard container to the backend compose network:
services:
dashboard:
networks:
- default
- growqr-backend
networks:
growqr-backend:
external: true
name: growqr-backend-staging_default
Verification commands:
ssh gqr-temp 'docker exec growqr-dashboard sh -lc "wget -qO- --timeout=5 http://growqr-backend:4000/healthz"'
ssh gqr-temp 'docker logs --since=5m growqr-dashboard 2>&1 | grep -i backend_timeout || true'
Frontend robustness shipped in growqr-dashboard commit 0be0b65:
- Client API retries once with a fresh Clerk token (
getToken({ skipCache: true })) on401/403. - Home feed and TopBar send Clerk bearer tokens to
/api/growqr/*, preserve last good data on transient failures, and refresh on focus/online/visibility restore. - Resume list preserves last good data instead of blanking after idle, refreshes in the background, and reconciles uploaded resumes before showing parse errors.
- Resume edit flow no longer treats an on-demand parse race/timeout as terminal; it keeps polling and opens the editor automatically once the stored resume reaches
draft/complete.
Resume upload/edit parsing correction (2026-06-05)
Problem: staging had drifted away from the original growqr-app/frontend resume flow. Uploads could trigger/background-track parsing and the dashboard UI waited/polled as if parsing had already begun. The intended flow is: upload stores the PDF as uploaded; parsing starts only when the user clicks Edit Resume; successful parse transitions the resume to draft and opens the editor.
Source commits pushed:
growqr-dashboardstaging:42ba346 fix: parse uploaded resumes on editgrowqr-appstaging:e1b38d2 fix: defer resume parsing until edit
Files deployed to gqr-temp:
/opt/growqr/resume-builder/app/api/v1/resumes.py/opt/growqr/growqr-dashboard/app/api/growqr/[...path]/route.ts/opt/growqr/growqr-dashboard/components/resume/ResumeList.tsx/opt/growqr/growqr-dashboard/components/resume/ResumeUpload.tsx
Backups were written with .pre-edit-parse-flow-bak before replacing files.
Docker Compose deployment commands used:
ssh gqr-temp 'cd /opt/growqr/resume-builder && docker-compose -p resume-builder-staging up -d --build api'
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build dashboard'
Verification:
ssh gqr-temp 'cd /opt/growqr/resume-builder && docker-compose -p resume-builder-staging ps'
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml ps'
curl -fsS https://resume-staging.gqr.puter.wtf/health
curl -fsS -I https://dashboard-staging.gqr.puter.wtf/features/resume
Results:
growqr_resume_apirebuilt via Docker Compose and is healthy.growqr-dashboardrebuilt via Docker Compose and returnsHTTP/2 200on the public resume route.- Resume upload endpoint no longer dispatches the orchestrator background parse task.
- Dashboard proxy timeout default increased to 180 seconds so on-edit parsing can complete through
/api/growqr/services/resumewithout a premature proxybackend_timeout.
Resume stored-PDF parse proxy fix (2026-06-05)
Follow-up issue from browser testing: clicking an uploaded resume showed Failed to parse resume; DevTools showed a FastAPI validation error:
{"detail":[{"type":"missing","loc":["body","file"],"msg":"Field required"}]}
Root cause: the dashboard called the correct client path:
/api/growqr/services/resume/parse/resume/<resume_id>/parse
but growqr-backend extracted the resume-service subpath with:
c.req.path.split("/resume/")[1]
Because the target path itself contains /resume/, this truncated the forwarded route to parse, so the resume service received:
POST /api/v1/parse
instead of:
POST /api/v1/parse/resume/<resume_id>/parse
Fixes shipped:
growqr-backendc47e6de fix: preserve nested resume service proxy paths- Uses the first
/resume/marker index and slices the remainder, preserving nested/resume/path segments.
- Uses the first
growqr-dashboard624c5af fix: show uploaded resumes as ready to parse- Uploaded resumes now display as
Uploaded — click Edit to parse. - The spinner overlay only appears during an actual Edit-triggered parse request.
- Uploaded resumes now display as
Docker Compose deployment commands used:
ssh gqr-temp 'cd /opt/growqr/growqr-backend && docker-compose -p growqr-backend-staging up -d --build backend'
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build dashboard'
Safe route verification used a non-existent UUID, so no resume was parsed or mutated:
curl -X POST \
-H "Authorization: Bearer $SERVICE_TOKEN" \
-H "x-growqr-user: user_3EX1LG3gBk3KY6kfD9PiTkWubs4" \
http://127.0.0.1:18012/services/resume/parse/resume/00000000-0000-0000-0000-000000000000/parse
Expected/observed result:
404 {"detail":"Resume not found"}
Resume-service log confirmed the corrected forwarded path:
POST /api/v1/parse/resume/00000000-0000-0000-0000-000000000000/parse HTTP/1.1
Public verification:
curl -I https://backend-staging.gqr.puter.wtf/healthz
curl -I https://dashboard-staging.gqr.puter.wtf/features/resume
Both returned HTTP/2 200.
Interview live-avatar, personalization, MediaPipe, and video-analysis fix (2026-06-05)
Problem: after moving the interview UI from growqr-app/frontend/orchestrator to growqr-dashboard/growqr-backend, several orchestration behaviors were missing or incomplete:
- The dashboard build did not ship the MediaPipe runtime files used by
useFaceFeedback, so live camera coaching/presence analysis could not reliably start in production. growqr-backendcalledinterview-servicedirectly but did not recreate the orchestrator's enricheduser_contextflow (candidate_name, resume skills/experience/education, LinkedIn headline/summary/experience, and opt-incandidate_profile).interview-servicepage-state always returnedresume_available=falseandlinkedin_available=false, hiding the personalization checkbox.- The browser video-analysis upload flow posted with an empty body while
growqr-backendtried to parse JSON before proxying the request, so recorded session video registration could fail before the presigned upload flow began. - LiveAvatar envs were not enabled on the staging interview/roleplay containers.
- Interview-service supported only Docker-internal MinIO presign URLs; browsers need public HTTPS presign endpoints.
Source commits pushed:
growqr-dashboardstaging:4c95dcb fix: restore interview video analysis assetsgrowqr-backendstaging:aa8f285 fix: enrich interview service contextinterview-servicestaging:78c7070 fix: support public interview artifact presigningroleplay-servicestaging:6e64519 fix: support liveavatar api alias
Key source changes:
growqr-dashboard/package.json- Added
@mediapipe/tasks-vision. - Added
copy-mediapipe,predev, andprebuildscripts. copy-mediapipecopiesvision_bundle.mjsand the MediaPipe WASM files fromnode_modulesintopublic/mediapipeat build time.
- Added
growqr-dashboard/public/mediapipe/face_landmarker.task- Checked in the face landmark model required by
useFaceFeedback.
- Checked in the face landmark model required by
growqr-dashboard/lib/api/interview.tsandlib/api/roleplay.ts- Video upload-url/uploaded calls now send
{}JSON bodies for compatibility with older backend deployments.
- Video upload-url/uploaded calls now send
growqr-dashboard/app/features/interview/preview/page.tsxandapp/agents/interview/preview/page.tsx- Regenerate preserves
personalize=1so regenerated plans keep the grounded candidate profile.
- Regenerate preserves
growqr-backend/src/routes/services.ts- Recreates the orchestrator context enrichment flow by reading user-service profile plus resume/social state.
- Enriches interview configure payloads with
candidate_nameand, when requested, a compactcandidate_profilebuilt from resume + LinkedIn data. - Enriches
/services/interview/page-statewith realresume_availableandlinkedin_availableflags. - Video upload proxy routes no longer require JSON parsing before proxying.
growqr-backend/src/services/product-service-clients.ts- Video upload proxy calls are explicit
POSTs even when no body is required.
- Video upload proxy calls are explicit
interview-service/app/core/config.py,app/services/storage.py, andapp/api/artifacts.py- Added
S3_PUBLIC_ENDPOINT_URLsupport for browser-reachable presigned URLs. - Added compatibility with the existing
LIVEAVATAR_APIenv alias.
- Added
roleplay-service/app/core/config.py- Added compatibility with the existing
LIVEAVATAR_APIenv alias.
- Added compatibility with the existing
Staging env changes applied on gqr-temp (values redacted in this runbook):
/opt/growqr/interview-service/.env
LIVEAVATAR_ENABLED=true
LIVEAVATAR_API_KEY=<redacted>
LIVEAVATAR_API=<redacted>
S3_PUBLIC_ENDPOINT_URL=https://interview-minio-staging.gqr.puter.wtf
/opt/growqr/roleplay-service/.env
LIVEAVATAR_ENABLED=true
LIVEAVATAR_API_KEY=<redacted>
LIVEAVATAR_API=<redacted>
S3_PUBLIC_ENDPOINT_URL=https://roleplay-minio-staging.gqr.puter.wtf
Backups were created with:
.env.pre-interview-fix-bak-<timestamp>
Caddy blocks added for browser-reachable, presigned MinIO upload/download URLs:
interview-minio-staging.gqr.puter.wtf {
reverse_proxy 127.0.0.1:19000
}
roleplay-minio-staging.gqr.puter.wtf {
reverse_proxy 127.0.0.1:19010
}
Validated and reloaded Caddy:
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy
Docker Compose deployment commands used:
ssh gqr-temp 'cd /opt/growqr/growqr-backend && docker-compose -p growqr-backend-staging up -d --build backend'
ssh gqr-temp 'cd /opt/growqr/interview-service && docker-compose -p interview-service up -d --build api'
ssh gqr-temp 'cd /opt/growqr/roleplay-service && docker-compose -p roleplay-service up -d --build api'
ssh gqr-temp 'cd /opt/growqr/growqr-dashboard && docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build dashboard'
Verification commands:
curl -fsS http://127.0.0.1:18012/healthz
curl -fsS http://127.0.0.1:18007/health
curl -fsS http://127.0.0.1:18008/health
curl -I https://dashboard-staging.gqr.puter.wtf/mediapipe/vision_bundle.mjs
curl -I https://dashboard-staging.gqr.puter.wtf/mediapipe/wasm/vision_wasm_internal.wasm
curl -I https://dashboard-staging.gqr.puter.wtf/mediapipe/face_landmarker.task
curl -I https://interview-minio-staging.gqr.puter.wtf/minio/health/live
curl -I https://roleplay-minio-staging.gqr.puter.wtf/minio/health/live
Observed:
- Backend, interview-service, roleplay-service, and dashboard all healthy.
- Dashboard route returns
HTTP/2 200. - MediaPipe bundle, WASM, and
face_landmarker.taskall returnHTTP/2 200. - Both MinIO public health endpoints return
HTTP/2 200. /services/interview/page-statenow returns real personalization availability (verified with service-token probe:resume_available=truefor the probed staging user).- A presign probe against an existing interview session returned a browser-reachable URL beginning with
https://interview-minio-staging.gqr.puter.wtf/....
LiveAvatar sandbox-mode correction and VIP codes (2026-06-05)
Follow-up from browser comparison with the production interview page: staging was still showing the static interviewer headshot even when avatar_mode=video was stored on the interview session.
Root cause:
LIVEAVATAR_ENABLED=trueand the API key were present, butLIVEAVATAR_SANDBOXwas still at the service default oftrue.- The selected Payal/Emma/John persona avatar IDs are not supported in LiveAvatar sandbox mode. LiveAvatar returned
400 Bad RequestwithThis avatar is not supported in sandbox mode, so the service correctly degraded to the static-photo path.
Fix:
/opt/growqr/interview-service/.env
LIVEAVATAR_SANDBOX=false
INTERVIEW_UNLIMITED_PHRASE=GQR-VIDEO-VIP-2026,GQR-LIVEAVATAR-TEST,PUTER-VIDEO-TEST
/opt/growqr/roleplay-service/.env
LIVEAVATAR_SANDBOX=false
ROLEPLAY_UNLIMITED_PHRASE=GQR-VIDEO-VIP-2026,GQR-LIVEAVATAR-TEST,PUTER-VIDEO-TEST
Source commits:
interview-servicestaging:7c67ceb fix: support multiple interview vip codesroleplay-servicestaging:90392e0 fix: support multiple roleplay vip codes
Deployment:
ssh gqr-temp 'cd /opt/growqr/interview-service && docker-compose -p interview-service up -d --build api'
ssh gqr-temp 'cd /opt/growqr/roleplay-service && docker-compose -p roleplay-service up -d --build api'
Verification:
- Container settings now show
LIVEAVATAR_ENABLED=True,LIVEAVATAR_SANDBOX=False, API key present, and all three VIP codes loaded. - A staging backend configure call using
unlimited_phrase=GQR-LIVEAVATAR-TESTproducedconfig.avatar_mode=videofor a user already at the video cap. - A direct websocket smoke test for that configured session returned:
session.ready has_avatar=True provider=liveavatar livekit_url=wss://heygen-...
User-facing test instruction: choose Video mode on Interview Setup, expand/enter VIP code, and use any of:
GQR-VIDEO-VIP-2026
GQR-LIVEAVATAR-TEST
PUTER-VIDEO-TEST
2026-06-05 — Roleplay LiveAvatar parity polish
Goal: make staging Roleplay behave like the production GrowQR app/orchestrator path, same as the Interview LiveAvatar polish.
Source changes
-
growqr-backend/src/routes/services.ts- Added roleplay configure enrichment equivalent to the production orchestrator/A2A flow.
POST /services/roleplay/configurenow resolves Grow user context from user-service, resume-builder, and social-branding, then forwards:metadata.candidate_namemetadata.target_rolemetadata.candidate_rolemetadata.difficultymetadata.candidate_profilemetadata.context_notesuser_context- qscore fallback/defaults
GET /services/roleplay/page-statenow augments roleplay page-state withresume_availableandlinkedin_availablejust like interview page-state.
-
roleplay-service/app/services/storage.py- Fixed public MinIO presigning so explicit staging domains like
https://roleplay-minio-staging.gqr.puter.wtfare preserved. - The old fallback rewrite now only changes the exact Docker-internal hostname
miniotolocalhost; it no longer rewrites public domains containing the wordminio.
- Fixed public MinIO presigning so explicit staging domains like
Commits deployed
growqr-backend staging: 170d358 fix: enrich roleplay service context
roleplay-service staging: e3d50e5 fix: preserve public roleplay minio presign host
Staging deployment notes
During tar-based redeploy, the staging-only compose/env patches must be preserved or re-applied:
- Backend API port:
127.0.0.1:18012:4000 - Roleplay API port:
127.0.0.1:18008:8000 - Roleplay Postgres:
127.0.0.1:15441:5432 - Roleplay MinIO:
127.0.0.1:19010:9000,127.0.0.1:19011:9001 - Backend env URLs must point at staging HTTPS service domains, not local worktree defaults.
- Backend
RIVET_RUNNER_VERSIONmust be set to a non-devstaging value to avoid Rivet runner version collisions. - Roleplay env must include:
BASE_URL=https://roleplay-staging.gqr.puter.wtf
S3_ENDPOINT_URL=http://minio:9000
S3_PUBLIC_ENDPOINT_URL=https://roleplay-minio-staging.gqr.puter.wtf
LIVEAVATAR_ENABLED=true
LIVEAVATAR_SANDBOX=false
ROLEPLAY_UNLIMITED_PHRASE=GQR-VIDEO-VIP-2026,GQR-LIVEAVATAR-TEST,PUTER-VIDEO-TEST
CORS_ORIGINS=https://dashboard-staging.gqr.puter.wtf,https://backend-staging.gqr.puter.wtf
Verification performed
Health:
curl -fsS http://127.0.0.1:18012/healthz
curl -fsS http://127.0.0.1:18008/health
Roleplay configure smoke through backend with VIP video mode returned:
session_id=<uuid>
avatar_mode=video
metadata includes candidate_profile/context_notes
Roleplay presigned upload URL now starts with the browser-reachable HTTPS MinIO domain:
https://roleplay-minio-staging.gqr.puter.wtf/roleplay-artifacts/...
End-to-end WebSocket smoke:
wss://roleplay-staging.gqr.puter.wtf/api/v1/roleplays/session/<session_id>
→ send {"type":"session.start"}
→ received session.ready with avatar.provider=liveavatar and livekit_url present
Tested VIP code:
GQR-LIVEAVATAR-TEST
2026-06-06 — Dashboard mission runtime + home feed wiring polish
Goal: keep the existing dashboard UI, but wire it to the mission-first backend runtime and make Home/Missions production-ready on staging.
Changes deployed:
growqr-dashboardstaging commitd52c160(fix: wire mission runtime to dashboard)/api/growqr/*now guards against stale loopback backend envs in production Docker. IfGROWQR_BACKEND_URLis accidentallylocalhost:4000/127.0.0.1:4000, the proxy useshttp://growqr-backend:4000on the shared compose network.lib/grow-api.tsnow understandsactionsByMissionand mission action endpoints:- approve/reject/run/answer/snooze
- manual daily scrum:
POST /missions/active/:instanceId/scrum/run
- Active mission cards now render backend
mission_actionsinside the existing card-deck UI and can execute HITL actions without adding new navigation/sidebar surfaces.
growqr-backendstaging commitdd48321(fix: keep home feed responsive)- Product service page-state calls used by Home feed now have a short timeout (
PRODUCT_SERVICE_TIMEOUT_MS, default3500ms) so one slow service cannot block the dashboard. - Home Feed Agent LLM refinement has a bounded timeout (
HOME_FEED_AGENT_TIMEOUT_MS, default8000ms) and falls back to deterministic notifications.
- Product service page-state calls used by Home feed now have a short timeout (
- Staging dashboard env fixed in
/opt/growqr/growqr-dashboard/.env:GROWQR_BACKEND_URL=http://growqr-backend:4000NEXT_PUBLIC_GROWQR_BACKEND_URL=https://backend-staging.gqr.puter.wtfGROWQR_BACKEND_TIMEOUT_MS=180000
Deployment commands used:
# Dashboard: copy only committed mission/proxy files, preserving unrelated working-tree UI edits
cd growqr-dashboard
git archive --format=tar HEAD \
'app/api/growqr/[...path]/route.ts' \
'app/missions/(list)/active/page.tsx' \
components/mission/ActiveMissionView.tsx \
components/mission/AgentCard.tsx \
components/mission/adapter.ts \
lib/grow-api.ts \
> /tmp/growqr-dashboard-mission-wire.tar
scp /tmp/growqr-dashboard-mission-wire.tar gqr-temp:/tmp/
# Backend: copy only responsive home-feed files
cd growqr-backend
git archive --format=tar HEAD \
src/home/home-feed-agent.ts \
src/services/product-service-clients.ts \
> /tmp/growqr-backend-home-responsive.tar
scp /tmp/growqr-backend-home-responsive.tar gqr-temp:/tmp/
ssh gqr-temp
cd /opt/growqr/growqr-dashboard
cp .env .env.pre-mission-wire-bak.$(date +%Y%m%d%H%M%S)
tar -xf /tmp/growqr-dashboard-mission-wire.tar
chmod 600 .env
cd /opt/growqr/growqr-backend
tar -xf /tmp/growqr-backend-home-responsive.tar
docker-compose -p growqr-backend-staging up -d --build backend
cd /opt/growqr/growqr-dashboard
docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build dashboard
Verification performed:
# Dashboard container can reach backend over Docker DNS
docker exec growqr-dashboard wget -qO- http://growqr-backend:4000/healthz
# => {"ok":true}
# Public app and backend are healthy
curl -I https://dashboard-staging.gqr.puter.wtf/
curl https://backend-staging.gqr.puter.wtf/healthz
# Dashboard proxy works for mission definitions, active mission runtime, and home feed
curl -H "Authorization: Bearer $SERVICE_TOKEN" -H "x-growqr-user: $USER_ID" \
http://127.0.0.1:13000/api/growqr/missions/available
curl -H "Authorization: Bearer $SERVICE_TOKEN" -H "x-growqr-user: $USER_ID" \
http://127.0.0.1:13000/api/growqr/missions/active
curl -H "Authorization: Bearer $SERVICE_TOKEN" -H "x-growqr-user: $USER_ID" \
http://127.0.0.1:13000/api/growqr/home/feed
# Manual mission scrum endpoint works through dashboard proxy
curl -X POST -H "Authorization: Bearer $SERVICE_TOKEN" -H "x-growqr-user: $USER_ID" \
http://127.0.0.1:13000/api/growqr/missions/active/<instanceId>/scrum/run
Expected browser result after hard refresh:
- Home tiles should show real backend notification counts/content instead of all-zero fallback tiles.
/missions/availableshould show the five backend mission definitions./missions/activeshould show active missions and backend action/HITL cards in the existing active mission card UI.
2026-06-06 — Interview/Roleplay staging WebSocket endpoint fix
Symptom: the dashboard preview/session UI showed WebSocket connection error after successful Interview configure calls.
Root cause: growqr-dashboard staging .env still baked loopback WebSocket URLs into the Next.js browser bundle:
NEXT_PUBLIC_INTERVIEW_WS_URL=ws://127.0.0.1:8007/api/v1
NEXT_PUBLIC_ROLEPLAY_WS_URL=ws://127.0.0.1:8040/api/v1
Those URLs work only from the VPS, not from a user's browser. The public browser endpoints must use the Caddy HTTPS domains with wss://.
Fix deployed:
growqr-dashboardcommit6604ff8(fix: use staging websocket endpoints)- Runtime/source fallback now maps
*.gqr.puter.wtfbrowser sessions to:wss://interview-staging.gqr.puter.wtf/api/v1wss://roleplay-staging.gqr.puter.wtf/api/v1
- Staging
.envpatched to explicitly bake:
NEXT_PUBLIC_INTERVIEW_WS_URL=wss://interview-staging.gqr.puter.wtf/api/v1
NEXT_PUBLIC_ROLEPLAY_WS_URL=wss://roleplay-staging.gqr.puter.wtf/api/v1
NEXT_PUBLIC_INTERVIEW_API_URL=/api/growqr/services/interview
NEXT_PUBLIC_ROLEPLAY_API_URL=/api/growqr/services/roleplay
NEXT_PUBLIC_INTERVIEW_ARTIFACTS_URL=/api/growqr/services/interview
NEXT_PUBLIC_ROLEPLAY_ARTIFACTS_URL=/api/growqr/services/roleplay
Deployment command:
cd /opt/growqr/growqr-dashboard
docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build dashboard
Verification:
# Dashboard bundle must not contain browser-invalid loopback WS URLs
docker exec growqr-dashboard sh -lc '
for p in "localhost:8007" "127.0.0.1:8007" "localhost:8008" "127.0.0.1:8040"; do
echo "$p:$(grep -R -l "$p" /app/.next/static /app/.next/server 2>/dev/null | wc -l)"
done
'
# all counts should be 0
# Public websocket handshake should succeed; fake session should connect then return Session not found
# Interview: wss://interview-staging.gqr.puter.wtf/api/v1/session/<session_id>
# Roleplay: wss://roleplay-staging.gqr.puter.wtf/api/v1/roleplays/session/<session_id>
Browser action required: hard-refresh https://dashboard-staging.gqr.puter.wtf so the new Next.js chunks are loaded.
2026-06-06 — Dashboard onboarding first-login trigger fix
Symptom: after signing in through the dashboard modal, a not-yet-onboarded user landed on the dashboard and the onboarding modal only appeared after a hard refresh.
Root cause: immediately after modal sign-in, Clerk client state can become signed-in before a usable session token/cookie is available to the Next.js /api/growqr/users/bootstrap proxy. The first user bootstrap could fail once, leaving UserProvider with user=null; OnboardingGate therefore had no user profile to inspect until a page refresh retried bootstrap with a settled session.
Fix deployed:
growqr-dashboardcommit1140d43(fix: trigger onboarding immediately after sign-in)UserProvidernow retries user bootstrap withgetToken({ skipCache: true })before giving up.GrowBootstrapsends an explicit Clerk bearer token instead of relying only on server-side cookies.useOnboardingnow merges saves withDEFAULT_ONBOARDING_DATAand the freshestpreferences.onboarding, so every onboarding step persists a complete onboarding object to user-service.
Deployment command:
cd /opt/growqr/growqr-dashboard
docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build dashboard
Verification performed:
cd growqr-dashboard
npm run build
curl -I https://dashboard-staging.gqr.puter.wtf/
# => HTTP/2 200
Browser verification:
- Hard-refresh once to load commit
1140d43. - Sign out / sign in with a user whose
preferences.onboarding.completed_atis missing. - Confirm the onboarding modal opens automatically without manually refreshing.
- Complete onboarding and confirm
preferences.onboarding.completed_atplus the collected fields are saved in user-service.
2026-06-06 — Onboarding privacy-screen loop fix
Symptom: the first onboarding step opened, but clicking Continue after accepting privacy terms sent the user back to the same privacy screen.
Root cause: each onboarding step save calls refreshUser() after persisting to user-service. The previous first-login fix made UserProvider.fetchUser() set global isLoading=true for every refresh. OnboardingGate returned null while isLoading was true, which unmounted AdaptiveOnboarding; when the profile refresh completed, the modal remounted from local step=0, creating a loop back to the privacy screen.
Fix deployed:
growqr-dashboardcommitdbdd681(fix: keep onboarding mounted during saves)UserProvidernow shows global loading only before the first user profile arrives, not during refreshes.OnboardingGatekeeps the modal mounted while an existing user profile is refreshing.
Deployment command:
cd /opt/growqr/growqr-dashboard
docker-compose -p growqr-dashboard-staging -f docker-compose.staging.yml up -d --build dashboard
Verification performed:
cd growqr-dashboard
npm run build
curl -I https://dashboard-staging.gqr.puter.wtf/
# => HTTP/2 200
Browser action required: hard-refresh the dashboard once so the new bundle is used, then continue onboarding. Step saves should no longer reset the modal back to the privacy screen.
2026-06-06 — Onboarding Q Score baseline correction
Symptom: immediately after onboarding, the dashboard showed QX Score 100 even though a new user should start around the baseline (35).
Root cause: growqr-backend projected a plain resume.uploaded event as a perfect 100 signal. Users who uploaded a resume during onboarding therefore got a projection of 100 before any parsed resume analysis, interview review, or roleplay review existed.
Fix deployed:
growqr-backendcommit9fd478c(fix: keep onboarding qscore baseline at 35)resume.uploadednow projects as baseline35, not100.- The onboarding baseline seeder also repairs users affected by the old upload-only
100projection.
Deployment note:
- Docker Hub returned
429 Too Many Requestswhile resolvingnode:22-alpine. - Workaround used on staging: tag the existing backend image as the local
node:22-alpinebase, then rebuild without pulling.
docker tag growqr-backend-staging-backend:latest node:22-alpine
cd /opt/growqr/growqr-backend
DOCKER_BUILDKIT=0 docker-compose -p growqr-backend-staging up -d --build backend
Verification performed for sumit26696@gmail.com:
resume.uploaded: 100 -> 35
home/feed identity.qx: { from: 35, to: 42, baseline: 35 }
The user's current score was 42 because later roleplay review signals existed; a fresh onboarding-only user with only the resume-upload baseline should now show 35, not 100.