66 # We pin via flake.lock, but choose a channel that contains Prisma 5.22.x
77 # so NixOS can use nixpkgs-provided Prisma engines without version mismatches.
88 nixpkgs . url = "github:NixOS/nixpkgs/nixos-24.11" ;
9+ nixpkgs-unstable . url = "github:NixOS/nixpkgs/nixpkgs-unstable" ;
910 } ;
1011
11- outputs = { self , nixpkgs } :
12+ outputs = { self , nixpkgs , nixpkgs-unstable } :
1213 let
1314 systems = [
1415 "x86_64-linux"
1819 ] ;
1920
2021 forAllSystems =
21- f : nixpkgs . lib . genAttrs systems ( system : f system ( import nixpkgs { inherit system ; } ) ) ;
22+ f : nixpkgs . lib . genAttrs systems ( system : f system
23+ ( import nixpkgs { inherit system ; } )
24+ ( import nixpkgs-unstable {
25+ inherit system ;
26+ config . allowUnfreePredicate = pkg :
27+ builtins . elem ( nixpkgs-unstable . lib . getName pkg ) [ "ngrok" ] ;
28+ } ) ) ;
2229
2330 # Shared PostGIS 3.3.5 builder - used by dev packages and preview module
2431 # This ensures both use the same locked version matching production
5158 in {
5259 # Export shared builders for use by nixosModules
5360 lib = { inherit mkPostgis335 mkPostgresCompat mkPrismaEnv mkOpenSslEnv ; } ;
54- devShells = forAllSystems ( _system : pkgs : {
61+ devShells = forAllSystems ( _system : pkgs : _pkgs-unstable : {
5562 default = pkgs . mkShell {
5663 buildInputs =
5764 ( with pkgs ; [
131138 } ;
132139 } ) ;
133140
134- packages = forAllSystems ( _system : pkgs :
141+ packages = forAllSystems ( _system : pkgs : pkgs-unstable :
135142 let
136143 # Default postgres - uses nixpkgs PostGIS (fast, pre-built from binary cache)
137144 postgres = pkgs . postgresql_16 . withPackages ( ps : [ ps . postgis ] ) ;
@@ -485,6 +492,8 @@ USAGE
485492 pkgs . nodejs
486493 pkgs . nodePackages . npm
487494 pkgs . curl
495+ pkgs-unstable . ngrok
496+ pkgs . jq
488497 oc-dev-db-nix
489498 oc-dev-db-nix-locked
490499 oc-dev-db-docker
@@ -502,7 +511,7 @@ USAGE
502511 usage() {
503512 cat <<'USAGE'
504513Usage:
505- nix run .#dev -- [--db=remote|external|nix|docker] [--db-url URL] [--direct-url URL] [--migrate] [--no-studio] [--locked] [--no-lan]
514+ nix run .#dev -- [--db=remote|external|nix|docker] [--db-url URL] [--direct-url URL] [--migrate] [--no-studio] [--locked] [--no-lan] [--tasks-preview=N]
506515
507516DB modes:
508517 --db=nix Start Postgres+PostGIS via Nix + app (process-compose TUI) (default)
@@ -511,10 +520,11 @@ DB modes:
511520 --db=docker Start Docker PostGIS + app (requires Docker)
512521
513522Flags:
514- --migrate Run migrations (npm run db:deploy) before starting the app (remote/external only)
515- --no-studio Disable Prisma Studio process (enabled by default for local DB modes)
516- --fast Use pre-built PostGIS from binary cache (faster first build, but may not match production)
517- --no-lan Bind dev server to localhost only (default: binds to 0.0.0.0 for mobile preview)
523+ --migrate Run migrations (npm run db:deploy) before starting the app (remote/external only)
524+ --no-studio Disable Prisma Studio process (enabled by default for local DB modes)
525+ --fast Use pre-built PostGIS from binary cache (faster first build, but may not match production)
526+ --no-lan Bind dev server to localhost only (default: binds to 0.0.0.0 for mobile preview)
527+ --tasks-preview=N Connect to opencouncil-tasks preview for PR #N (starts ngrok tunnel for callbacks)
518528USAGE
519529 }
520530
@@ -528,6 +538,7 @@ USAGE
528538 studio_enabled=""
529539 postgis_locked="'' ${OC_POSTGIS_LOCKED:-1}"
530540 lan_enabled="'' ${OC_LAN:-1}"
541+ tasks_preview_pr="'' ${OC_TASKS_PREVIEW:-}"
531542
532543 for arg in "$@"; do
533544 case "$arg" in
@@ -538,6 +549,7 @@ USAGE
538549 --no-studio) studio_override="0" ;;
539550 --fast) postgis_locked="0" ;;
540551 --no-lan) lan_enabled="0" ;;
552+ --tasks-preview=*) tasks_preview_pr="'' ${arg#--tasks-preview=}" ;;
541553 --help|-h) usage; exit 0 ;;
542554 *)
543555 echo "Unknown argument: $arg" >&2
@@ -603,16 +615,96 @@ USAGE
603615 studio_port="$(find_available_port 5555)"
604616 fi
605617
606- # Check if TASK_API_URL is configured and reachable (non-blocking warning)
607- if [ -n "'' ${TASK_API_URL:-}" ]; then
608- echo "🔗 Task API configured: $TASK_API_URL"
609- if command -v curl >/dev/null 2>&1; then
610- if curl -sf --connect-timeout 2 "$TASK_API_URL/health" >/dev/null 2>&1; then
611- echo " ✓ Task server is reachable"
618+ # Check task API health and optionally validate API key.
619+ # Usage: check_task_api <url> <strict>
620+ # strict=1: exit on failure (for --tasks-preview)
621+ # strict=0: warn on failure (for .env TASK_API_URL)
622+ check_task_api() {
623+ local url="$1" strict="$2"
624+ local health_json auth_ok
625+
626+ # Health check (with auth header if TASK_API_KEY is set)
627+ health_json=$(curl -sf --connect-timeout 5 \
628+ '' ${TASK_API_KEY:+-H "Authorization: Bearer $TASK_API_KEY"} \
629+ "$url/health" 2>/dev/null) || {
630+ if [ "$strict" = "1" ]; then
631+ echo " ✗ Not reachable at $url" >&2
632+ exit 1
612633 else
613634 echo " ⚠ Task server not reachable (start it separately for E2E testing)"
635+ return 1
636+ fi
637+ }
638+ echo " ✓ Task server is reachable"
639+
640+ # Validate API key if one was sent
641+ if [ -n "'' ${TASK_API_KEY:-}" ]; then
642+ auth_ok=$(echo "$health_json" | jq -r '.authenticated' 2>/dev/null || true)
643+ if [ "$auth_ok" = "true" ]; then
644+ echo " ✓ API key accepted"
645+ elif [ "$auth_ok" = "false" ]; then
646+ if [ "$strict" = "1" ]; then
647+ echo " ✗ TASK_API_KEY is not valid for this server." >&2
648+ echo " Check the token in your .env matches the server." >&2
649+ exit 1
650+ else
651+ echo " ⚠ TASK_API_KEY is not valid for this server"
652+ fi
614653 fi
615654 fi
655+ }
656+
657+ # --tasks-preview: connect to a remote tasks preview and start ngrok for callbacks
658+ ngrok_pid=""
659+ if [ -n "$tasks_preview_pr" ]; then
660+ tasks_preview_url="https://pr-'' ${tasks_preview_pr}.tasks.opencouncil.gr"
661+ export TASK_API_URL="$tasks_preview_url"
662+ if [ -z "'' ${TASK_API_KEY:-}" ]; then
663+ echo " ✗ TASK_API_KEY is not set. Set it in .env or export it." >&2
664+ echo " The tasks preview server requires a valid API token." >&2
665+ exit 1
666+ fi
667+
668+ echo "🔗 Connecting to tasks preview PR #$tasks_preview_pr..."
669+ check_task_api "$tasks_preview_url" 1
670+
671+ # Start ngrok tunnel so the remote tasks server can POST callbacks to localhost
672+ echo " Starting ngrok tunnel for localhost:$app_port..."
673+ ngrok http "$app_port" --log=stdout > "$logs_dir/ngrok.log" 2>&1 &
674+ ngrok_pid=$!
675+
676+ # Wait for ngrok to assign a public URL
677+ ngrok_url=""
678+ for _i in $(seq 1 30); do
679+ ngrok_url=$(curl -sf http://localhost:4040/api/tunnels 2>/dev/null \
680+ | jq -r '.tunnels[] | select(.proto == "https") | .public_url' 2>/dev/null || true)
681+ if [ -n "$ngrok_url" ] && [ "$ngrok_url" != "null" ]; then
682+ break
683+ fi
684+ sleep 0.5
685+ done
686+
687+ if [ -z "$ngrok_url" ] || [ "$ngrok_url" = "null" ]; then
688+ echo " ✗ Failed to start ngrok tunnel." >&2
689+ echo " Check $logs_dir/ngrok.log" >&2
690+ echo " If first run: ngrok config add-authtoken <TOKEN>" >&2
691+ kill "$ngrok_pid" 2>/dev/null || true
692+ exit 1
693+ fi
694+
695+ export NEXTAUTH_URL="$ngrok_url"
696+ echo " ✓ Tunnel active: $ngrok_url → localhost:$app_port"
697+ echo ""
698+ echo " Tasks API: $tasks_preview_url"
699+ echo " Callbacks: $ngrok_url"
700+ echo " Ngrok logs: $logs_dir/ngrok.log"
701+ echo ""
702+ fi
703+
704+ # Check if TASK_API_URL is configured and reachable (non-blocking warning)
705+ if [ -z "$tasks_preview_pr" ] && [ -n "'' ${TASK_API_URL:-}" ]; then
706+ echo "🔗 Task API configured: $TASK_API_URL"
707+ check_task_api "$TASK_API_URL" 0 || true
616708 echo ""
617709 fi
618710
735827
736828 pc_port="$(find_available_port 8080)"
737829
738- # On Linux with --lan, open the firewall port automatically and
739- # clean it up when the dev runner exits.
830+ # Determine if we need cleanup on exit (firewall rule and/or ngrok).
831+ # When cleanup is needed we run process-compose without exec so the
832+ # trap fires after it exits. Otherwise we exec for a cleaner process tree.
833+ needs_cleanup=false
834+ cleanup_cmds=""
835+
836+ # On Linux with --lan, open the firewall port automatically.
740837 if [ "$lan_enabled" = "1" ] && command -v iptables >/dev/null 2>&1 \
741838 && ! sudo -n iptables -C INPUT -p tcp --dport "$app_port" -j ACCEPT 2>/dev/null; then
742839 echo ""
745842 echo ""
746843 sudo iptables -I INPUT -p tcp --dport "$app_port" -j ACCEPT
747844 echo "Opened firewall port $app_port for LAN access"
748- trap 'sudo iptables -D INPUT -p tcp --dport "$app_port" -j ACCEPT 2>/dev/null; echo "Closed firewall port $app_port"' EXIT
845+ needs_cleanup=true
846+ cleanup_cmds="sudo iptables -D INPUT -p tcp --dport \"$app_port\" -j ACCEPT 2>/dev/null; echo \"Closed firewall port $app_port\";"
847+ fi
848+
849+ # If ngrok is running, ensure it gets cleaned up on exit.
850+ if [ -n "$ngrok_pid" ]; then
851+ needs_cleanup=true
852+ cleanup_cmds="'' ${cleanup_cmds}kill $ngrok_pid 2>/dev/null || true; echo \"Stopped ngrok tunnel\";"
853+ fi
854+
855+ # Brief pause so startup messages are readable before TUI takes over
856+ echo "Starting process-compose..."
857+ sleep 5
858+
859+ if [ "$needs_cleanup" = "true" ]; then
860+ cleanup_cmds="'' ${cleanup_cmds}rm -rf \"$tmp_dir\";"
861+ # shellcheck disable=SC2064 # Intentional: expand commands now, not at signal time
862+ trap "$cleanup_cmds" EXIT
749863 process-compose -f "$pc_file" up --port "$pc_port"
750864 else
751865 exec process-compose -f "$pc_file" up --port "$pc_port"
@@ -1604,7 +1718,7 @@ CADDYEOF
16041718 } ;
16051719 } ;
16061720
1607- apps = forAllSystems ( system : pkgs : {
1721+ apps = forAllSystems ( system : pkgs : _pkgs-unstable : {
16081722 dev = {
16091723 type = "app" ;
16101724 program = "${ self . packages . ${ system } . oc-dev } /bin/oc-dev" ;
0 commit comments