Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 23 additions & 4 deletions bin/library/load-library-metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
57 changes: 47 additions & 10 deletions bin/library/sync-metadata
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion builder-frontend/src/api/benefit.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
3 changes: 2 additions & 1 deletion builder-frontend/src/api/check.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { authGet, authPatch, authPost, authPut } from "@/api/auth";
import { env } from "@/config/environment";

import type {
EligibilityCheck,
Expand All @@ -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<EligibilityCheck[]> => {
const url = apiUrl + "/library-checks";
Expand Down
3 changes: 2 additions & 1 deletion builder-frontend/src/api/publishedScreener.ts
Original file line number Diff line number Diff line change
@@ -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<PublishedScreener> => {
const url = apiUrl + "/published/screener/" + publishedScreenerId;
Expand Down
3 changes: 2 additions & 1 deletion builder-frontend/src/api/screener.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
56 changes: 56 additions & 0 deletions builder-frontend/src/config/environment.ts
Original file line number Diff line number Diff line change
@@ -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 "<name>-<port>.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();
13 changes: 9 additions & 4 deletions builder-frontend/src/firebase/firebase.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
}
19 changes: 18 additions & 1 deletion builder-frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
}
});
5 changes: 3 additions & 2 deletions devbox.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading
Loading