diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6a56943b..7c4be7f8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,41 @@ "dockerfile": "./Dockerfile", "context": ".." }, - "postCreateCommand": "bin/ensure-root-env-file && devbox run setup", + "onCreateCommand": "bin/ensure-root-env-file && devbox run setup", + "postStartCommand": "devbox services up -b && echo 'Frontend will open in browser when ready. Run `devbox services attach` to see the running processes.'", + "forwardPorts": [ + 5173, + 8081, + 8083, + 9099, + 8080, + 9199, + 4000 + ], + "portsAttributes": { + "5173": { + "label": "Frontend", + "onAutoForward": "openBrowser" + }, + "8081": { + "label": "Builder API" + }, + "8083": { + "label": "Library API" + }, + "9099": { + "label": "Firebase Auth" + }, + "8080": { + "label": "Firestore" + }, + "9199": { + "label": "Storage" + }, + "4000": { + "label": "Firebase UI" + } + }, "customizations": { "vscode": { "settings": {}, diff --git a/README.md b/README.md index 29f87c0a..6b4505f0 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ Try out an [example elegibility screener](https://phillypropertytaxrelief.org/) If you are interested in getting involved with the project, check out [our page on the Code For Philly website](https://codeforphilly.org/projects/dmn_benefit_toolbox-including_the_philly_property_tax_relief_screener) +## Testing Pull Requests + +Want to test changes from a pull request without setting up a local development environment? See our [Codespaces Testing Guide](docs/testing-prs-with-codespaces.md) for step-by-step instructions. + ## User-Facing Technologies [Decision Model and Notation (DMN)](https://learn-dmn-in-15-minutes.com/) is used to define the logic of the screener forms. diff --git a/bin/library/load-library-metadata.py b/bin/library/load-library-metadata.py index 6ce0d6f3..94225042 100644 --- a/bin/library/load-library-metadata.py +++ b/bin/library/load-library-metadata.py @@ -5,6 +5,17 @@ import json from datetime import datetime import os +import google.auth.credentials + + +class EmulatorCredentials(credentials.Base): + """Mock credentials for use with Firebase emulators.""" + + def __init__(self): + self._mock_credential = google.auth.credentials.AnonymousCredentials() + + def get_credential(self): + return self._mock_credential # ----------------------------------- # CONFIGURATION @@ -41,11 +52,19 @@ if storage_host_override: os.environ["STORAGE_EMULATOR_HOST"] = storage_host_override -cred = credentials.ApplicationDefault() - firebase_options = {"storageBucket": STORAGE_BUCKET} -if not IS_PRODUCTION: - # Emulators need an explicit project ID; production gets it from credentials + +if IS_PRODUCTION: + # Production uses Application Default Credentials + cred = credentials.ApplicationDefault() +else: + # Emulators don't need real credentials - use anonymous/mock credentials + # Set FIRESTORE_EMULATOR_HOST if not already set (standard Firebase emulator env var) + if not os.getenv("FIRESTORE_EMULATOR_HOST"): + os.environ["FIRESTORE_EMULATOR_HOST"] = "localhost:8080" + + # Use mock credentials for emulator mode + cred = EmulatorCredentials() firebase_options["projectId"] = os.getenv("QUARKUS_GOOGLE_CLOUD_PROJECT_ID", "demo-bdt-dev") firebase_admin.initialize_app(cred, firebase_options) diff --git a/bin/library/sync-metadata b/bin/library/sync-metadata index 12e1a88f..10bfc5c0 100755 --- a/bin/library/sync-metadata +++ b/bin/library/sync-metadata @@ -35,19 +35,56 @@ if [ "$MODE" = "development" ]; then echo " 2. library-api must be running (quarkus dev)" echo "" + # Helper function to check URL using Python (works without curl) + # Accepts any HTTP response (including 4xx/5xx) as proof server is running + check_url() { + python3 -c " +import urllib.request +import urllib.error +import sys +try: + urllib.request.urlopen('$1', timeout=5) + sys.exit(0) +except urllib.error.HTTPError: + # Server responded with an error code, but it IS responding + sys.exit(0) +except Exception as e: + print(f'DEBUG: {type(e).__name__}: {e}') + sys.exit(1) +" + } + + # Retry settings - Codespaces needs more time for services to become ready + MAX_RETRIES=30 + RETRY_DELAY=2 + # Check if Firebase Storage emulator is running - if ! curl -s http://localhost:9199 >/dev/null 2>&1; then - echo "ERROR: Firebase Storage emulator not responding at localhost:9199" - echo "Start emulators with: firebase emulators:start --project demo-bdt-dev --only auth,storage,firestore" - exit 1 - fi + for i in $(seq 1 $MAX_RETRIES); do + if check_url "http://localhost:9199"; then + echo "Checking if Firebase Storage emulator is ready... ($i/$MAX_RETRIES)" + break + fi + if [ $i -eq $MAX_RETRIES ]; then + echo "ERROR: Firebase Storage emulator not responding at localhost:9199" + echo "Start emulators with: firebase emulators:start --project demo-bdt-dev --only auth,storage,firestore" + exit 1 + fi + sleep $RETRY_DELAY + done # Check if library-api is running - if ! curl -s "${LIBRARY_API_URL}/q/health" >/dev/null 2>&1; then - echo "ERROR: library-api not responding at ${LIBRARY_API_URL}" - echo "Start library-api with: cd library-api && quarkus dev" - exit 1 - fi + for i in $(seq 1 $MAX_RETRIES); do + if check_url "${LIBRARY_API_URL}/q/health"; then + echo "Checking if library-api is ready... ($i/$MAX_RETRIES)" + break + fi + if [ $i -eq $MAX_RETRIES ]; then + echo "ERROR: library-api not responding at ${LIBRARY_API_URL}" + echo "Start library-api with: cd library-api && quarkus dev" + exit 1 + fi + sleep $RETRY_DELAY + done # Check if $VENV_DIR is set and activate it if [[ "$VENV_DIR" != "" ]]; then diff --git a/builder-frontend/src/api/benefit.ts b/builder-frontend/src/api/benefit.ts index cd7568ef..f40a4841 100644 --- a/builder-frontend/src/api/benefit.ts +++ b/builder-frontend/src/api/benefit.ts @@ -1,8 +1,9 @@ import { authGet, authPut } from "@/api/auth"; +import { env } from "@/config/environment"; import { Benefit } from "@/types"; -const apiUrl = import.meta.env.VITE_API_URL; +const apiUrl = env.apiUrl; export const fetchScreenerBenefit = async ( srceenerId: string, diff --git a/builder-frontend/src/api/check.ts b/builder-frontend/src/api/check.ts index 75dfe8b0..dfd08c82 100644 --- a/builder-frontend/src/api/check.ts +++ b/builder-frontend/src/api/check.ts @@ -1,4 +1,5 @@ import { authGet, authPatch, authPost, authPut } from "@/api/auth"; +import { env } from "@/config/environment"; import type { EligibilityCheck, @@ -7,7 +8,7 @@ import type { UpdateCheckRequest, } from "@/types"; -const apiUrl = import.meta.env.VITE_API_URL; +const apiUrl = env.apiUrl; export const fetchPublicChecks = async (): Promise => { const url = apiUrl + "/library-checks"; diff --git a/builder-frontend/src/api/publishedScreener.ts b/builder-frontend/src/api/publishedScreener.ts index 0fc97dbe..a5d6f808 100644 --- a/builder-frontend/src/api/publishedScreener.ts +++ b/builder-frontend/src/api/publishedScreener.ts @@ -1,6 +1,7 @@ import type { PublishedScreener, ScreenerResult } from "@/types"; +import { env } from "@/config/environment"; -const apiUrl = import.meta.env.VITE_API_URL; +const apiUrl = env.apiUrl; export const fetchPublishedScreener = async (publishedScreenerId: string): Promise => { const url = apiUrl + "/published/screener/" + publishedScreenerId; diff --git a/builder-frontend/src/api/screener.ts b/builder-frontend/src/api/screener.ts index 81a19353..92aabfb1 100644 --- a/builder-frontend/src/api/screener.ts +++ b/builder-frontend/src/api/screener.ts @@ -1,8 +1,9 @@ import { authDelete, authGet, authPost, authPut } from "@/api/auth"; +import { env } from "@/config/environment"; import type { BenefitDetail, ScreenerResult } from "@/types"; -const apiUrl = import.meta.env.VITE_API_URL; +const apiUrl = env.apiUrl; export const fetchProjects = async () => { const url = apiUrl + "/screeners"; diff --git a/builder-frontend/src/config/environment.ts b/builder-frontend/src/config/environment.ts new file mode 100644 index 00000000..8a777454 --- /dev/null +++ b/builder-frontend/src/config/environment.ts @@ -0,0 +1,56 @@ +/** + * Runtime URL resolver for development environments. + * + * In dev mode, URLs are derived from `window.location.hostname` so the app + * works correctly across local Devbox, Devcontainer, local-VS-Code-to-Codespace, + * and browser-based Codespace environments without build-time patching. + * + * In production, env vars are used directly. + */ + +interface Env { + apiUrl: string; + authDomain: string; + screenerBaseUrl: string; +} + +function resolveEnv(): Env { + const fallback: Env = { + apiUrl: import.meta.env.VITE_API_URL, + authDomain: import.meta.env.VITE_AUTH_DOMAIN, + screenerBaseUrl: import.meta.env.VITE_SCREENER_BASE_URL, + }; + + if (import.meta.env.MODE !== "development") { + return fallback; + } + + const hostname = window.location.hostname; + + // Local access (Devbox, Devcontainer, or local VS Code forwarding from Codespace) + if (hostname === "localhost" || hostname === "127.0.0.1") { + return { + apiUrl: "http://localhost:8081/api", + authDomain: "localhost:9099", + screenerBaseUrl: "http://localhost:5174/", + }; + } + + // Browser-based Codespace: hostname looks like "-.app.github.dev" + const codespaceMatch = hostname.match( + /^(.+)-\d+\.(app\.github\.dev)$/, + ); + if (codespaceMatch) { + const codespaceName = codespaceMatch[1]; + const domain = codespaceMatch[2]; + return { + apiUrl: `/api`, + authDomain: window.location.host, + screenerBaseUrl: `https://${codespaceName}-5174.${domain}/`, + }; + } + + return fallback; +} + +export const env = resolveEnv(); diff --git a/builder-frontend/src/firebase/firebase.js b/builder-frontend/src/firebase/firebase.js index 10c284a5..2365af83 100644 --- a/builder-frontend/src/firebase/firebase.js +++ b/builder-frontend/src/firebase/firebase.js @@ -1,9 +1,10 @@ import { initializeApp } from "firebase/app"; // import { getAnalytics } from "firebase/analytics"; import { getAuth, connectAuthEmulator } from "firebase/auth"; +import { env } from "@/config/environment"; const API_KEY = import.meta.env.VITE_API_KEY; -const AUTH_DOMAIN = import.meta.env.VITE_AUTH_DOMAIN; +const AUTH_DOMAIN = env.authDomain; const PROJECT_ID = import.meta.env.VITE_PROJECT_ID; const STORAGE_BUCKET = import.meta.env.VITE_STORAGE_BUCKET; const MESSAGING_SENDER_ID = import.meta.env.VITE_MESSAGING_SENDER_ID; @@ -28,9 +29,13 @@ export const auth = getAuth(app); // Connect to emulators in development if (import.meta.env.MODE === 'development') { try { - connectAuthEmulator(auth, "http://localhost:9099", { disableWarnings: true }); - console.log("🔧 Connected to Firebase emulators"); + const prefixes = ["localhost", "127.0.0.1"]; + const isLocalhost = prefixes.some(prefix => AUTH_DOMAIN.startsWith(prefix)); + const protocol = isLocalhost ? 'http' : 'https'; + const authEmulatorUrl = `${protocol}://${AUTH_DOMAIN}`; + connectAuthEmulator(auth, authEmulatorUrl, { disableWarnings: true }); + console.log("🔧 Connected to Firebase auth emulator"); } catch (error) { - console.log("Emulators already connected or not available"); + console.log("Error connecting to Firebase auth emulator:", error); } } diff --git a/builder-frontend/vite.config.js b/builder-frontend/vite.config.js index ed314820..64f7fa73 100644 --- a/builder-frontend/vite.config.js +++ b/builder-frontend/vite.config.js @@ -5,6 +5,23 @@ import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig({ plugins: [solid(), tsconfigPaths()], server: { - port: process.env.DEV_SERVER_PORT || 5173 + port: process.env.DEV_SERVER_PORT || 5173, + proxy: { + '/identitytoolkit.googleapis.com': { + target: 'http://localhost:9099', + }, + '/securetoken.googleapis.com': { + target: 'http://localhost:9099', + }, + '/emulator': { + target: 'http://localhost:9099', + }, + '/__/auth': { + target: 'http://localhost:9099', + }, + '/api': { + target: 'http://localhost:8081', + }, + }, } }); diff --git a/devbox.json b/devbox.json index 82529541..b173da1c 100644 --- a/devbox.json +++ b/devbox.json @@ -4,12 +4,13 @@ "maven@latest", "quarkus@latest", "jdk21@latest", - "firebase-tools@latest", + "firebase-tools@14.27.0", "google-cloud-sdk@latest", "nodejs@22", "bruno-cli@latest", "process-compose@latest", - "python@latest" + "python@3.14", + "python314Packages.pip@latest" ], "env_from": ".env", "shell": { diff --git a/devbox.lock b/devbox.lock index d19cd5e0..2cae4846 100644 --- a/devbox.lock +++ b/devbox.lock @@ -2,150 +2,150 @@ "lockfile_version": "1", "packages": { "bruno-cli@latest": { - "last_modified": "2025-10-22T20:59:19Z", - "resolved": "github:NixOS/nixpkgs/01b6809f7f9d1183a2b3e081f0a1e6f8f415cb09#bruno-cli", + "last_modified": "2026-01-23T17:20:52Z", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#bruno-cli", "source": "devbox-search", - "version": "2.13.2", + "version": "3.0.2", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/2yrf1xw8da5a0g7d92zg5psn6ss8p67b-bruno-cli-2.13.2", + "path": "/nix/store/5yh986h7c66qssma4i7i38xbsh725zlg-bruno-cli-3.0.2", "default": true } ], - "store_path": "/nix/store/2yrf1xw8da5a0g7d92zg5psn6ss8p67b-bruno-cli-2.13.2" + "store_path": "/nix/store/5yh986h7c66qssma4i7i38xbsh725zlg-bruno-cli-3.0.2" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/l0gnb73v2z53l2v48323wn4apd3l3xxz-bruno-cli-2.13.2", + "path": "/nix/store/5sfmhkj0fxvy7ym8ls76663g8jp3gc8y-bruno-cli-3.0.2", "default": true } ], - "store_path": "/nix/store/l0gnb73v2z53l2v48323wn4apd3l3xxz-bruno-cli-2.13.2" + "store_path": "/nix/store/5sfmhkj0fxvy7ym8ls76663g8jp3gc8y-bruno-cli-3.0.2" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/vh42arjvvw2ns9c1yr4zcjycnz13sq1i-bruno-cli-2.13.2", + "path": "/nix/store/m0i011xhj6bs24vc011rvkk6453yn695-bruno-cli-3.0.2", "default": true } ], - "store_path": "/nix/store/vh42arjvvw2ns9c1yr4zcjycnz13sq1i-bruno-cli-2.13.2" + "store_path": "/nix/store/m0i011xhj6bs24vc011rvkk6453yn695-bruno-cli-3.0.2" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/j0jr5fvhxnng60jhynwjagl1bzrd0yqd-bruno-cli-2.13.2", + "path": "/nix/store/q1g5q00m37nqi9f6339csmvwc8n52f7g-bruno-cli-3.0.2", "default": true } ], - "store_path": "/nix/store/j0jr5fvhxnng60jhynwjagl1bzrd0yqd-bruno-cli-2.13.2" + "store_path": "/nix/store/q1g5q00m37nqi9f6339csmvwc8n52f7g-bruno-cli-3.0.2" } } }, - "firebase-tools@latest": { - "last_modified": "2025-08-08T08:05:48Z", - "resolved": "github:NixOS/nixpkgs/a3f3e3f2c983e957af6b07a1db98bafd1f87b7a1#firebase-tools", + "firebase-tools@14.27.0": { + "last_modified": "2025-12-09T08:49:39Z", + "resolved": "github:NixOS/nixpkgs/677fbe97984e7af3175b6c121f3c39ee5c8d62c9#firebase-tools", "source": "devbox-search", - "version": "14.11.2", + "version": "14.27.0", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/zf8q36g73lakbil2ri5w999p8l6y2hi4-firebase-tools-14.11.2", + "path": "/nix/store/lik4kg99vwvzajlbj51ybwckkh32af6m-firebase-tools-14.27.0", "default": true } ], - "store_path": "/nix/store/zf8q36g73lakbil2ri5w999p8l6y2hi4-firebase-tools-14.11.2" + "store_path": "/nix/store/lik4kg99vwvzajlbj51ybwckkh32af6m-firebase-tools-14.27.0" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/s1nvrwwynvpl4xaxyvydhxkwnsyjk4hh-firebase-tools-14.11.2", + "path": "/nix/store/x1zf04ga5c24d4mhvc8dhzql8mm162p7-firebase-tools-14.27.0", "default": true } ], - "store_path": "/nix/store/s1nvrwwynvpl4xaxyvydhxkwnsyjk4hh-firebase-tools-14.11.2" + "store_path": "/nix/store/x1zf04ga5c24d4mhvc8dhzql8mm162p7-firebase-tools-14.27.0" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/p0azzz6gdpgq8r6xxajmlagpgvz2kgxy-firebase-tools-14.11.2", + "path": "/nix/store/cx6yssxq92rz1hphq58y19mvfl7d00ar-firebase-tools-14.27.0", "default": true } ], - "store_path": "/nix/store/p0azzz6gdpgq8r6xxajmlagpgvz2kgxy-firebase-tools-14.11.2" + "store_path": "/nix/store/cx6yssxq92rz1hphq58y19mvfl7d00ar-firebase-tools-14.27.0" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/s6sgsylhy32asrr2bpm7v3sv9pvdy40x-firebase-tools-14.11.2", + "path": "/nix/store/hzyjibb7s04ni71h6ll5xr80dfgacnb2-firebase-tools-14.27.0", "default": true } ], - "store_path": "/nix/store/s6sgsylhy32asrr2bpm7v3sv9pvdy40x-firebase-tools-14.11.2" + "store_path": "/nix/store/hzyjibb7s04ni71h6ll5xr80dfgacnb2-firebase-tools-14.27.0" } } }, "github:NixOS/nixpkgs/nixpkgs-unstable": { - "last_modified": "2025-09-12T04:37:21Z", - "resolved": "github:NixOS/nixpkgs/ad4e6dd68c30bc8bd1860a27bc6f0c485bd7f3b6?lastModified=1757651841&narHash=sha256-Lh9QoMzTjY%2FO4LqNwcm6s%2FWSYStDmCH6f3V%2FizwlkHc%3D" + "last_modified": "2026-02-08T07:51:33Z", + "resolved": "github:NixOS/nixpkgs/fef9403a3e4d31b0a23f0bacebbec52c248fbb51?lastModified=1770537093&narHash=sha256-pF1quXG5wsgtyuPOHcLfYg%2Fft%2FQMr8NnX0i6tW2187s%3D" }, "google-cloud-sdk@latest": { - "last_modified": "2025-07-28T17:09:23Z", - "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#google-cloud-sdk", + "last_modified": "2026-02-04T01:49:30Z", + "resolved": "github:NixOS/nixpkgs/aa290c9891fa4ebe88f8889e59633d20cc06a5f2#google-cloud-sdk", "source": "devbox-search", - "version": "529.0.0", + "version": "552.0.0", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/9v8y10z0y9lkhjsaf728nhhbc4jbp83q-google-cloud-sdk-529.0.0", + "path": "/nix/store/8d8crpn5vkhz4yrzifvf2yh1pvmiq1d4-google-cloud-sdk-552.0.0", "default": true } ], - "store_path": "/nix/store/9v8y10z0y9lkhjsaf728nhhbc4jbp83q-google-cloud-sdk-529.0.0" + "store_path": "/nix/store/8d8crpn5vkhz4yrzifvf2yh1pvmiq1d4-google-cloud-sdk-552.0.0" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/5kz4dghm4lc2rmjy85xz813irlpyglhl-google-cloud-sdk-529.0.0", + "path": "/nix/store/h9jpvqmyg33b8b8378f8qf2h3av8wwmm-google-cloud-sdk-552.0.0", "default": true } ], - "store_path": "/nix/store/5kz4dghm4lc2rmjy85xz813irlpyglhl-google-cloud-sdk-529.0.0" + "store_path": "/nix/store/h9jpvqmyg33b8b8378f8qf2h3av8wwmm-google-cloud-sdk-552.0.0" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/arw4kq8hig5v2n3gq3pzw521qb3rc736-google-cloud-sdk-529.0.0", + "path": "/nix/store/v7zj255khs62bm52hh2rvihc71gq1kyh-google-cloud-sdk-552.0.0", "default": true } ], - "store_path": "/nix/store/arw4kq8hig5v2n3gq3pzw521qb3rc736-google-cloud-sdk-529.0.0" + "store_path": "/nix/store/v7zj255khs62bm52hh2rvihc71gq1kyh-google-cloud-sdk-552.0.0" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/aw6lxd787x16w8d2ll2afnjjz3kqi0m4-google-cloud-sdk-529.0.0", + "path": "/nix/store/04cm7ghf9ngxlzamzlv2yv93y2zmjv5k-google-cloud-sdk-552.0.0", "default": true } ], - "store_path": "/nix/store/aw6lxd787x16w8d2ll2afnjjz3kqi0m4-google-cloud-sdk-529.0.0" + "store_path": "/nix/store/04cm7ghf9ngxlzamzlv2yv93y2zmjv5k-google-cloud-sdk-552.0.0" } } }, @@ -186,186 +186,270 @@ } }, "maven@latest": { - "last_modified": "2025-07-28T17:09:23Z", - "resolved": "github:NixOS/nixpkgs/648f70160c03151bc2121d179291337ad6bc564b#maven", + "last_modified": "2026-01-23T17:20:52Z", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#maven", "source": "devbox-search", - "version": "3.9.11", + "version": "3.9.12", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/4l03gxm9lnkap9z4s219lk2lmznf6ksr-maven-3.9.11", + "path": "/nix/store/k9sgk3q1306si2jz1xfl9apd1v1r6md8-maven-3.9.12", "default": true } ], - "store_path": "/nix/store/4l03gxm9lnkap9z4s219lk2lmznf6ksr-maven-3.9.11" + "store_path": "/nix/store/k9sgk3q1306si2jz1xfl9apd1v1r6md8-maven-3.9.12" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/6663sm7x2b6lh3lcy40ysng365h0n41w-maven-3.9.11", + "path": "/nix/store/rv85ra1bqqy2zq99a1x06ri24rwzpb8j-maven-3.9.12", "default": true } ], - "store_path": "/nix/store/6663sm7x2b6lh3lcy40ysng365h0n41w-maven-3.9.11" + "store_path": "/nix/store/rv85ra1bqqy2zq99a1x06ri24rwzpb8j-maven-3.9.12" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/i1fz4dsqcfy2ilz9yq0z84n973iw4k8z-maven-3.9.11", + "path": "/nix/store/a6wv4w68bsp3znbgqdnnxxkmhcba4d5h-maven-3.9.12", "default": true } ], - "store_path": "/nix/store/i1fz4dsqcfy2ilz9yq0z84n973iw4k8z-maven-3.9.11" + "store_path": "/nix/store/a6wv4w68bsp3znbgqdnnxxkmhcba4d5h-maven-3.9.12" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/f055dmaqf8japp7nqww78zp3g37j5qfs-maven-3.9.11", + "path": "/nix/store/qfg0cblwp966vjrzqcyrzq5hjffifbvb-maven-3.9.12", "default": true } ], - "store_path": "/nix/store/f055dmaqf8japp7nqww78zp3g37j5qfs-maven-3.9.11" + "store_path": "/nix/store/qfg0cblwp966vjrzqcyrzq5hjffifbvb-maven-3.9.12" } } }, "nodejs@22": { - "last_modified": "2025-06-06T01:46:53Z", + "last_modified": "2026-02-06T12:24:04Z", "plugin_version": "0.0.2", - "resolved": "github:NixOS/nixpkgs/6ad174a6dc07c7742fc64005265addf87ad08615#nodejs_22", + "resolved": "github:NixOS/nixpkgs/ae67888ff7ef9dff69b3cf0cc0fbfbcd3a722abe#nodejs_22", "source": "devbox-search", - "version": "22.14.0", + "version": "22.22.0", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/g5bv4gi6p7ryhs2hbaryxs6ivsxa6lqc-nodejs-22.14.0", + "path": "/nix/store/zphm4748w514z6p9g073f673sjl338gm-nodejs-22.22.0", "default": true }, { "name": "dev", - "path": "/nix/store/96vpirdcns8zxdwzwl7n804012lwrf1n-nodejs-22.14.0-dev" + "path": "/nix/store/pp6k9i2dpr8rimfl5dhrpm8y2384g8l3-nodejs-22.22.0-dev" }, { "name": "libv8", - "path": "/nix/store/xph2837xqjnra80mqlk64xp7vb3kkqxs-nodejs-22.14.0-libv8" + "path": "/nix/store/f5mnrbcj44qx8pb0xj9g71cb0ffxrmd0-nodejs-22.22.0-libv8" } ], - "store_path": "/nix/store/g5bv4gi6p7ryhs2hbaryxs6ivsxa6lqc-nodejs-22.14.0" + "store_path": "/nix/store/zphm4748w514z6p9g073f673sjl338gm-nodejs-22.22.0" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/cwb781xbp70f72njqh8vhvbsf1xq87i5-nodejs-22.14.0", + "path": "/nix/store/2hzivwi1fq7gyn42nxf1299ydza2qvqq-nodejs-22.22.0", "default": true }, { - "name": "dev", - "path": "/nix/store/0w5mql46am2qai707a42q983l44l1h9z-nodejs-22.14.0-dev" + "name": "libv8", + "path": "/nix/store/xl8f6lq0p5lf1dixhd9rhxzkvx4lzpzl-nodejs-22.22.0-libv8" }, { - "name": "libv8", - "path": "/nix/store/vy56lahz381ayvw3hym3wj4r4ggv7gbk-nodejs-22.14.0-libv8" + "name": "dev", + "path": "/nix/store/4m4b63ix1kljgcap8zhbpiy4xvg90n1r-nodejs-22.22.0-dev" } ], - "store_path": "/nix/store/cwb781xbp70f72njqh8vhvbsf1xq87i5-nodejs-22.14.0" + "store_path": "/nix/store/2hzivwi1fq7gyn42nxf1299ydza2qvqq-nodejs-22.22.0" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/086zjv14ipka2vbpgwil3xyvi5ml4vh3-nodejs-22.14.0", + "path": "/nix/store/2jw64xbn385nrg0dkinyxy1dk57zib21-nodejs-22.22.0", "default": true }, { - "name": "dev", - "path": "/nix/store/24n01nfvy908pwzwpgsl0k4227187i7i-nodejs-22.14.0-dev" + "name": "libv8", + "path": "/nix/store/q81bvbbawxkm3kb741x4wmxvyyxd8w51-nodejs-22.22.0-libv8" }, { - "name": "libv8", - "path": "/nix/store/jiwk0in1wk85crn9vp03gg937gbh1s0w-nodejs-22.14.0-libv8" + "name": "dev", + "path": "/nix/store/vaq9lp4bchqhvfbfjws5rx2qwx6m57pn-nodejs-22.22.0-dev" } ], - "store_path": "/nix/store/086zjv14ipka2vbpgwil3xyvi5ml4vh3-nodejs-22.14.0" + "store_path": "/nix/store/2jw64xbn385nrg0dkinyxy1dk57zib21-nodejs-22.22.0" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/c8jxsih8yy2rnncdmx2hyraizf689nvp-nodejs-22.14.0", + "path": "/nix/store/cv3yxgf7zp70wk8d8lg5zi84lg35nyxs-nodejs-22.22.0", "default": true }, { - "name": "libv8", - "path": "/nix/store/s8gnrgh9hgnbkrc1wrn21d6zvkbvm9vi-nodejs-22.14.0-libv8" + "name": "dev", + "path": "/nix/store/azkh9xkfaphcs7cj5l43rh4a92zjc510-nodejs-22.22.0-dev" }, { - "name": "dev", - "path": "/nix/store/xw8c1c0inxq3xl1d7axz19vq8c05kjk5-nodejs-22.14.0-dev" + "name": "libv8", + "path": "/nix/store/vzddff6gppvny62qsvr1sgqq629laz12-nodejs-22.22.0-libv8" } ], - "store_path": "/nix/store/c8jxsih8yy2rnncdmx2hyraizf689nvp-nodejs-22.14.0" + "store_path": "/nix/store/cv3yxgf7zp70wk8d8lg5zi84lg35nyxs-nodejs-22.22.0" } } }, "process-compose@latest": { - "last_modified": "2025-11-23T21:50:36Z", - "resolved": "github:NixOS/nixpkgs/ee09932cedcef15aaf476f9343d1dea2cb77e261#process-compose", + "last_modified": "2026-02-02T23:09:17Z", + "resolved": "github:NixOS/nixpkgs/47472570b1e607482890801aeaf29bfb749884f6#process-compose", + "source": "devbox-search", + "version": "1.90.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ky986spyv6sfijb1k44hddla7hvi56lp-process-compose-1.90.0", + "default": true + } + ], + "store_path": "/nix/store/ky986spyv6sfijb1k44hddla7hvi56lp-process-compose-1.90.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/iqx95vl5dwkrsfcf4mj2643bfisyj1ff-process-compose-1.90.0", + "default": true + } + ], + "store_path": "/nix/store/iqx95vl5dwkrsfcf4mj2643bfisyj1ff-process-compose-1.90.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/fki23dffy7nzlma507q5lhf3dkm5l6wv-process-compose-1.90.0", + "default": true + } + ], + "store_path": "/nix/store/fki23dffy7nzlma507q5lhf3dkm5l6wv-process-compose-1.90.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/c7fywarscr5c07bxgd6d5g0isn0ijysz-process-compose-1.90.0", + "default": true + } + ], + "store_path": "/nix/store/c7fywarscr5c07bxgd6d5g0isn0ijysz-process-compose-1.90.0" + } + } + }, + "python314Packages.pip@latest": { + "last_modified": "2026-01-23T17:20:52Z", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#python314Packages.pip", "source": "devbox-search", - "version": "1.78.0", + "version": "25.3", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/d00iad83k88x0fq045h5pzhfj9ibdd3a-process-compose-1.78.0", + "path": "/nix/store/i5bvx5c65pyid4lkhbdlz6gbkhq4ap1l-python3.14-pip-25.3", + "default": true + }, + { + "name": "man", + "path": "/nix/store/9fh235jldxar3yhk2l2qrrvyhwxc1rgz-python3.14-pip-25.3-man", "default": true + }, + { + "name": "dist", + "path": "/nix/store/035vlli7j7n21zi8ajjxmarmmzpky4pb-python3.14-pip-25.3-dist" } ], - "store_path": "/nix/store/d00iad83k88x0fq045h5pzhfj9ibdd3a-process-compose-1.78.0" + "store_path": "/nix/store/i5bvx5c65pyid4lkhbdlz6gbkhq4ap1l-python3.14-pip-25.3" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/zx59c11mchqfjpl0yvv2fad87pf3p8mn-process-compose-1.78.0", + "path": "/nix/store/331m51mc13qxrfvgna91xzy0pv9dba1q-python3.14-pip-25.3", + "default": true + }, + { + "name": "man", + "path": "/nix/store/b8msyck1i4w5r9dmjwprms7092yskziq-python3.14-pip-25.3-man", "default": true + }, + { + "name": "dist", + "path": "/nix/store/sjybvdxdrzsimldjfc18ns4yy2p5ijbn-python3.14-pip-25.3-dist" } ], - "store_path": "/nix/store/zx59c11mchqfjpl0yvv2fad87pf3p8mn-process-compose-1.78.0" + "store_path": "/nix/store/331m51mc13qxrfvgna91xzy0pv9dba1q-python3.14-pip-25.3" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/hwipxpsgzpv8d51xvqdjyx3vbzaaf3rr-process-compose-1.78.0", + "path": "/nix/store/y7ssxlkd9rbl2ynrc06hxi84hw7f2wn5-python3.14-pip-25.3", "default": true + }, + { + "name": "man", + "path": "/nix/store/1wqh7rcax8csm71675wkpgq15fg01i5k-python3.14-pip-25.3-man", + "default": true + }, + { + "name": "dist", + "path": "/nix/store/64317pswbrqjm8a0cr7zx2lnrdmb49wj-python3.14-pip-25.3-dist" } ], - "store_path": "/nix/store/hwipxpsgzpv8d51xvqdjyx3vbzaaf3rr-process-compose-1.78.0" + "store_path": "/nix/store/y7ssxlkd9rbl2ynrc06hxi84hw7f2wn5-python3.14-pip-25.3" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/vw9d8v9r0kp44lmizysj7idmqyf9747l-process-compose-1.78.0", + "path": "/nix/store/zj0wzy5ga7h4arwh5iy6cvnr9lwlal6c-python3.14-pip-25.3", + "default": true + }, + { + "name": "man", + "path": "/nix/store/hway1fnhzpq8hzkn5fmz6wwdrn2fcqfb-python3.14-pip-25.3-man", "default": true + }, + { + "name": "dist", + "path": "/nix/store/1f2a9sxbs0xzxd37fwsfi76wzh2hma6k-python3.14-pip-25.3-dist" } ], - "store_path": "/nix/store/vw9d8v9r0kp44lmizysj7idmqyf9747l-process-compose-1.78.0" + "store_path": "/nix/store/zj0wzy5ga7h4arwh5iy6cvnr9lwlal6c-python3.14-pip-25.3" } } }, - "python@latest": { - "last_modified": "2025-12-31T03:27:36Z", + "python@3.14": { + "last_modified": "2026-01-23T17:20:52Z", "plugin_version": "0.0.4", - "resolved": "github:NixOS/nixpkgs/f665af0cdb70ed27e1bd8f9fdfecaf451260fc55#python314", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#python314", "source": "devbox-search", "version": "3.14.2", "systems": { @@ -373,97 +457,97 @@ "outputs": [ { "name": "out", - "path": "/nix/store/0iw3akzx6ygiqzpxi9s422sc2fl6190g-python3-3.14.2", + "path": "/nix/store/8bwmgvfcyys3kfia055ih7gask3fid7s-python3-3.14.2", "default": true } ], - "store_path": "/nix/store/0iw3akzx6ygiqzpxi9s422sc2fl6190g-python3-3.14.2" + "store_path": "/nix/store/8bwmgvfcyys3kfia055ih7gask3fid7s-python3-3.14.2" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/0ngn8d6wz6nsy68ck3vvv17v14nbnpmr-python3-3.14.2", + "path": "/nix/store/9zj2a40gbfh9scynfmfs1s9wx2q01yv3-python3-3.14.2", "default": true }, { "name": "debug", - "path": "/nix/store/irrzbxypkfg7mzl60m6zcv0wvid0fnz3-python3-3.14.2-debug" + "path": "/nix/store/j8ryg1l3z8ibf9rn848y1vll0f0wcab7-python3-3.14.2-debug" } ], - "store_path": "/nix/store/0ngn8d6wz6nsy68ck3vvv17v14nbnpmr-python3-3.14.2" + "store_path": "/nix/store/9zj2a40gbfh9scynfmfs1s9wx2q01yv3-python3-3.14.2" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/370kbw2s80x3i45jnmvxqc262ba9svib-python3-3.14.2", + "path": "/nix/store/0mnhsagxg8cp6cr9g0fz2dihw469ih8s-python3-3.14.2", "default": true } ], - "store_path": "/nix/store/370kbw2s80x3i45jnmvxqc262ba9svib-python3-3.14.2" + "store_path": "/nix/store/0mnhsagxg8cp6cr9g0fz2dihw469ih8s-python3-3.14.2" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/pmb8clh66mryha2ijzz3dg969drrkgj7-python3-3.14.2", + "path": "/nix/store/hd3h6gjpy7pf5himwf8sfqb3108pz5ld-python3-3.14.2", "default": true }, { "name": "debug", - "path": "/nix/store/jcd9cd12b91ys3axilh1k6pdsys64hhc-python3-3.14.2-debug" + "path": "/nix/store/2wvc4f32yycchbip56mlxixjm2sdzw2y-python3-3.14.2-debug" } ], - "store_path": "/nix/store/pmb8clh66mryha2ijzz3dg969drrkgj7-python3-3.14.2" + "store_path": "/nix/store/hd3h6gjpy7pf5himwf8sfqb3108pz5ld-python3-3.14.2" } } }, "quarkus@latest": { - "last_modified": "2025-08-11T16:06:55Z", - "resolved": "github:NixOS/nixpkgs/4e942f9ef5b35526597c354d1ded817d1c285ef1#quarkus", + "last_modified": "2026-02-03T08:29:05Z", + "resolved": "github:NixOS/nixpkgs/4533d9293756b63904b7238acb84ac8fe4c8c2c4#quarkus", "source": "devbox-search", - "version": "3.24.5", + "version": "3.31.1", "systems": { "aarch64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/jjg2x9jzvqjs5nbjgg1gimghydgnmb2w-quarkus-cli-3.24.5", + "path": "/nix/store/w0avsacdakyrq1v1y1sil2anqs60x6bk-quarkus-cli-3.31.1", "default": true } ], - "store_path": "/nix/store/jjg2x9jzvqjs5nbjgg1gimghydgnmb2w-quarkus-cli-3.24.5" + "store_path": "/nix/store/w0avsacdakyrq1v1y1sil2anqs60x6bk-quarkus-cli-3.31.1" }, "aarch64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/729fg85hyhsf99lvii3w4g4bydbgx8w3-quarkus-cli-3.24.5", + "path": "/nix/store/0k7n8h0dyab7gkbhd6iisz4xv22bzm2l-quarkus-cli-3.31.1", "default": true } ], - "store_path": "/nix/store/729fg85hyhsf99lvii3w4g4bydbgx8w3-quarkus-cli-3.24.5" + "store_path": "/nix/store/0k7n8h0dyab7gkbhd6iisz4xv22bzm2l-quarkus-cli-3.31.1" }, "x86_64-darwin": { "outputs": [ { "name": "out", - "path": "/nix/store/qrinn9yr8h2z5nnlahwvxwb2l5szp704-quarkus-cli-3.24.5", + "path": "/nix/store/sgw7hbkhjvak34zdk0hqf89r9nsbh1d9-quarkus-cli-3.31.1", "default": true } ], - "store_path": "/nix/store/qrinn9yr8h2z5nnlahwvxwb2l5szp704-quarkus-cli-3.24.5" + "store_path": "/nix/store/sgw7hbkhjvak34zdk0hqf89r9nsbh1d9-quarkus-cli-3.31.1" }, "x86_64-linux": { "outputs": [ { "name": "out", - "path": "/nix/store/yr4hd7vjkn8kdq83c042cda4wka1izm4-quarkus-cli-3.24.5", + "path": "/nix/store/c1x63pkryas4k9l9wmswnslcfjkys1wx-quarkus-cli-3.31.1", "default": true } ], - "store_path": "/nix/store/yr4hd7vjkn8kdq83c042cda4wka1izm4-quarkus-cli-3.24.5" + "store_path": "/nix/store/c1x63pkryas4k9l9wmswnslcfjkys1wx-quarkus-cli-3.31.1" } } } diff --git a/docs/testing-prs-with-codespaces.md b/docs/testing-prs-with-codespaces.md new file mode 100644 index 00000000..f0685389 --- /dev/null +++ b/docs/testing-prs-with-codespaces.md @@ -0,0 +1,222 @@ +# Testing Pull Requests with GitHub Codespaces + +This guide walks you through testing new features and bug fixes from pull requests using GitHub Codespaces. No technical setup required—everything runs in your browser. + +## What is Codespaces? + +GitHub Codespaces is a cloud-based development environment. Think of it as a complete computer running in your browser that's already set up with everything needed to run the application. When you start a Codespace, GitHub automatically: + +- Creates a virtual machine in the cloud +- Installs all the required software +- Starts the application for you + +This means you can test changes without installing anything on your own computer. + +## What You'll Need + +- A GitHub account (free to create at [github.com](https://github.com)) +- A modern web browser (Chrome, Firefox, Safari, or Edge) +- About 5-10 minutes for first-time setup + +## Step-by-Step: Starting a Codespace from a Pull Request + +### Step 1: Navigate to the Pull Request + +1. Go to the Benefit Decision Toolkit repository on GitHub +2. Click on the **Pull requests** tab near the top of the page (next to "Issues" and "Actions") +3. Find and click on the pull request you want to test + +You should now see the pull request page with a title, description, and various tabs like "Conversation", "Commits", and "Files changed". + +### Step 2: Open the Code Menu + +1. Look for a green button labeled **Code** on the right side of the page + - This button is typically located above the file list or in the pull request header area +2. Click the **Code** button +3. A dropdown menu will appear with two tabs: **Local** and **Codespaces** + +### Step 3: Create the Codespace + +1. Click on the **Codespaces** tab in the dropdown +2. You'll see a button that says **Create codespace on [branch-name]** + - The branch name will match the pull request's branch (shown in the dropdown) +3. Click that button + +A new browser tab will open and begin setting up your Codespace. + +## Waiting for Setup + +After clicking to create your Codespace, you'll see a loading screen. Here's what to expect: + +### What You'll See + +1. **Initial loading**: A dark screen with "Setting up your codespace" message +2. **Building**: Progress messages about building the development environment +3. **Starting services**: The environment installs dependencies and starts the application + +### How Long Does It Take? + +- **First time**: 3-5 minutes (the environment needs to be built from scratch) +- **Subsequent times**: 1-2 minutes (if you've used this Codespace before) + +### How to Know When It's Ready + +The setup is complete when: + +1. You see an interface that looks like VS Code (a code editor) in your browser +2. A terminal panel appears at the bottom of the screen +3. The terminal shows services starting up with colored status indicators +4. The browser pops up the frontend application in a new tab + +## Accessing the Application + +Once the Codespace is ready and services are running, you can access the application: + +### Step 0: You Might Already Be There + +If all went well during setup and you waited patiently for everything to spin up, then the +browser should have automatically opened the frontend application. + +If the frontend didn't pop up for whatever reason, then proceed to step 1. + +### Step 1: Find the Ports Panel + +1. Look at the bottom of the VS Code interface +2. Find the tab labeled **Ports** (next to "Terminal" and "Problems") +3. Click on **Ports** to open the ports panel + +### Step 2: Open the Application + +1. In the Ports panel, find the row showing port **5173** (this is the Frontend) + - You'll see columns for Port, Local Address, and Running Process +2. Look for a small globe icon in that row + - Hovering over it may show "Open in Browser" +3. Click the globe icon + +A new browser tab will open with the application. + +Note: this will only work if there is already a Running Process listed for port 5173 (be patient). + +#### Alternative Method + +If you don't see the globe icon: +1. Right-click on the port 5173 row +2. Select **Open in Browser** from the context menu + +### What URL to Expect + +The URL will look something like: +``` +https://[random-name]-5173.app.github.dev +``` + +This is your personal test instance of the application. + +### Browser Popup Blockers + +If clicking the globe icon doesn't open a new tab: +1. Check if your browser blocked a popup +2. Look for a popup blocker notification in your browser's address bar +3. Allow popups from `github.dev` domains + +## Logging In to the Application + +The application uses a test authentication system, so you don't need any real accounts. + +1. On the application's login page, click the **Continue with Google** +2. A simple login window will pop-up (this is the Firebase Emulator, not real Google Firebase authentication) +3. You can either: + - Click **Add new account** to create a fake google account + - If you've logged in previously with this codespace, pick an existing fake google account +4. Click **Sign in with Google.com**. + +### Important Notes + +- This is a **test environment**—no real Google account is used +- Any data you create is **isolated to this Codespace** +- Your test data won't affect anyone else and will be deleted when the Codespace is deleted + +## Testing the Feature + +Now you're ready to test! Here are some tips: + +1. **Review the pull request description** for specific things to test +2. **Try the new feature** as described in the pull request +3. **Test edge cases** (unusual inputs, empty fields, etc.) +4. **Check for visual issues** (layout problems, missing text, etc.) +5. **Document any issues** you find by commenting on the pull request + +## When You're Done Testing + +### Simply Close the Tab + +When you're finished testing, you can simply close the browser tab. The Codespace will automatically stop (shutdown) after 30 minutes of inactivity. The Codespace will remain available to start again for 30 days (by default). + +### Deleting the Codespace (Recommended when done testing) + +Deleting a Codespace will delete all data you've created during testing. You'll want to do this to save storage costs paid by Code for Philly. + +To clean up: + +1. Go to [github.com/codespaces](https://github.com/codespaces) +2. Find your Codespace in the list on the left and click to select it +3. Click the three dots menu on the right side +4. Select **Delete** + +## Troubleshooting + +### "Application not loading" + +**Symptoms**: The browser shows a blank page or error after clicking the globe icon + +**Solutions**: +1. Wait a minute longer—services may still be starting +2. Check the Terminal panel for error messages +3. Try refreshing the application page +4. In the terminal, type `devbox services list` and press Enter to see a summary of which services are running (or not) +5. In the terminal, type `devbox services attach` and press Enter to show the logs of the running services and look for errors + +### "Port not showing in the Ports panel" + +**Symptoms**: The Ports panel is empty or doesn't show port 5173 + +**Solutions**: +1. Click the refresh icon in the Ports panel header +2. Wait for services to fully start (check the Terminal for progress) +3. In the terminal, type `devbox services up` and press Enter +4. In the terminal, type `devbox services attach` and press Enter to show the logs of the running services and look for errors + +### "Services crashed or stopped" + +**Symptoms**: The terminal shows errors or services have stopped + +**Solutions**: +1. In the terminal at the bottom of VS Code, type: + ``` + devbox services up + ``` +2. Press Enter +3. Wait for services to restart (you'll see status messages) + +### "Codespace won't start" + +**Symptoms**: Stuck on loading screen for more than 10 minutes + +**Solutions**: +1. Close the tab and try again +2. Go to [github.com/codespaces](https://github.com/codespaces) +3. Delete the stuck Codespace +4. Create a new one from the pull request + +### "I need to test a different pull request" + +Each pull request needs its own Codespace. You can either: +1. Delete your current Codespace and create a new one for the other PR +2. Keep multiple Codespaces running (note: this uses more of your free tier hours) + +## Need More Help? + +If you're stuck: +1. Ask in the pull request comments +2. Reach out to the development team +3. Check the [project's main README](../README.md) for additional resources