diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5ece4e2 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +EXPO_PUBLIC_USE_LOCAL_API=1 +EXPO_PUBLIC_LOCAL_API_URL=http://192.168.76.129:8080 +EXPO_PUBLIC_API_URL=https://devbits.ddns.net +EXPO_PUBLIC_LOCAL_API_PORT=8080 +POSTGRES_DB=devbits_dev +POSTGRES_USER=devbits_dev +POSTGRES_PASSWORD=devbits_dev_password \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..061e936 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,54 @@ +name: Test + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_DB: devbits_test + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass123 + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - name: Wait for PostgreSQL + run: | + for i in {1..30}; do + if pg_isready -h localhost -p 5432 -U testuser -d devbits_test; then + exit 0 + fi + sleep 2 + done + exit 1 + + - name: Run tests + working-directory: backend + env: + USE_TEST_DB: true + POSTGRES_TEST_DB: devbits_test + POSTGRES_TEST_USER: testuser + POSTGRES_TEST_PASSWORD: testpass123 + run: go test -v ./api/internal/tests/... diff --git a/.gitignore b/.gitignore index a216fe7..8cbdbfc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ dist/ # Local env files .env .env.* +!.env.example # Expo .expo/ @@ -38,6 +39,7 @@ dist/ .env .env.* *.env +!.env.example # Secrets folder (recommended place for service keys) secrets/ diff --git a/Bash-Scripts/run-db-tests.sh b/Bash-Scripts/run-db-tests.sh new file mode 100644 index 0000000..15c7008 --- /dev/null +++ b/Bash-Scripts/run-db-tests.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# Script: run-db-tests.sh +# Does: Recreates isolated local dev DB/backend containers, waits for DB, then runs Go tests in a temporary golang container. +# Use: ./run-db-tests.sh +# DB: devbits_dev via host.docker.internal in compose project devbits-dev-local. +# Ports: backend default :8080, DB default :5433 (DEVBITS_BACKEND_PORT / DEVBITS_DB_PORT override). +# Modes: Frontend=OFF | Backend=only for local test infra | Live stack untouched | Dev/Test data isolated. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BACKEND_DIR="$ROOT/backend" +COMPOSE_PROJECT="devbits-dev-local" + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: Docker is required." + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "Error: Docker Compose v2 is required." + exit 1 +fi + +is_port_in_use() { + local port="$1" + if command -v ss >/dev/null 2>&1; then + ss -ltn "sport = :${port}" | grep -q LISTEN + return $? + fi + if command -v lsof >/dev/null 2>&1; then + lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 + return $? + fi + netstat -an 2>/dev/null | grep -E "[:.]${port}[[:space:]]" | grep -qi LISTEN +} + +resolve_port() { + local label="$1" + local default_port="$2" + local chosen="$default_port" + + while is_port_in_use "$chosen"; do + echo "Port ${chosen} is already in use for ${label}." + read -r -p "Enter alternate port for ${label} (blank to exit): " chosen + if [[ -z "$chosen" ]]; then + echo "Exiting. Free port ${default_port} or choose an alternate port next run." + exit 1 + fi + if ! [[ "$chosen" =~ ^[0-9]+$ ]] || (( chosen < 1 || chosen > 65535 )); then + echo "Invalid port: ${chosen}" + chosen="$default_port" + fi + done + + echo "$chosen" +} + +DEVBITS_BACKEND_PORT="$(resolve_port "backend" 8080)" +DEVBITS_DB_PORT="$(resolve_port "postgres" 5433)" +export DEVBITS_BACKEND_PORT +export DEVBITS_DB_PORT + +cd "$ROOT" +docker compose -p "$COMPOSE_PROJECT" -f backend/docker-compose.dev.yml down --volumes --remove-orphans +docker compose -p "$COMPOSE_PROJECT" -f backend/docker-compose.dev.yml up -d --build + +echo "Waiting for database readiness..." +for i in $(seq 1 60); do + if docker compose -p "$COMPOSE_PROJECT" -f backend/docker-compose.dev.yml exec -T db pg_isready -U devbits_dev -d devbits_dev >/dev/null 2>&1; then + echo "Database is ready." + break + fi + if [[ "$i" -eq 60 ]]; then + echo "Error: Database did not become ready within 60 seconds." + exit 1 + fi + sleep 1 +done + +set +e +docker run --rm --add-host=host.docker.internal:host-gateway \ + -e USE_TEST_DB=true \ + -e POSTGRES_TEST_DB=devbits_dev \ + -e POSTGRES_TEST_USER=devbits_dev \ + -e POSTGRES_TEST_PASSWORD=devbits_dev_password \ + -e POSTGRES_TEST_HOST=host.docker.internal \ + -e POSTGRES_TEST_PORT="$DEVBITS_DB_PORT" \ + -v "$ROOT/backend:/app" -w /app/api golang:1.24 bash -c "go test ./..." +TEST_EXIT=$? +set -e + +exit $TEST_EXIT \ No newline at end of file diff --git a/Bash-Scripts/run-dev.sh b/Bash-Scripts/run-dev.sh new file mode 100644 index 0000000..cbf2078 --- /dev/null +++ b/Bash-Scripts/run-dev.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + +# Script: run-dev.sh +# Does: Boots local dev DB + local backend (isolated compose project), then launches frontend in local mode. +# Use: ./run-dev.sh [--clear] +# DB: devbits_dev (user/pass: devbits_dev/devbits_dev_password) in compose project devbits-dev-local. +# Ports: backend default :8080, DB default :5433 (DEVBITS_BACKEND_PORT / DEVBITS_DB_PORT override). +# Modes: Frontend=ON(local API) | Backend=ON(local Docker) | Live stack untouched | Test DB untouched. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +BACKEND_DIR="$ROOT/backend" +COMPOSE_PROJECT="devbits-dev-local" + +CLEAR_FRONTEND="" +if [[ "${1:-}" == "--clear" ]]; then + CLEAR_FRONTEND="--clear" +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "Error: Docker is required." + exit 1 +fi + +if ! docker compose version >/dev/null 2>&1; then + echo "Error: Docker Compose v2 is required." + exit 1 +fi + +is_port_in_use() { + local port="$1" + if command -v ss >/dev/null 2>&1; then + ss -ltn "sport = :${port}" | grep -q LISTEN + return $? + fi + if command -v lsof >/dev/null 2>&1; then + lsof -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 + return $? + fi + netstat -an 2>/dev/null | grep -E "[:.]${port}[[:space:]]" | grep -qi LISTEN +} + +resolve_port() { + local label="$1" + local default_port="$2" + local chosen="$default_port" + + while is_port_in_use "$chosen"; do + echo "Port ${chosen} is already in use for ${label}." + read -r -p "Enter alternate port for ${label} (blank to exit): " chosen + if [[ -z "$chosen" ]]; then + echo "Exiting. Free port ${default_port} or choose an alternate port next run." + exit 1 + fi + if ! [[ "$chosen" =~ ^[0-9]+$ ]] || (( chosen < 1 || chosen > 65535 )); then + echo "Invalid port: ${chosen}" + chosen="$default_port" + fi + done + + echo "$chosen" +} + +DEVBITS_BACKEND_PORT="$(resolve_port "backend" 8080)" +DEVBITS_DB_PORT="$(resolve_port "postgres" 5433)" +export DEVBITS_BACKEND_PORT +export DEVBITS_DB_PORT + +echo "Using backend port ${DEVBITS_BACKEND_PORT} and db port ${DEVBITS_DB_PORT}." + +cd "$BACKEND_DIR" +docker compose -p "$COMPOSE_PROJECT" -f docker-compose.dev.yml down --volumes --remove-orphans +docker compose -p "$COMPOSE_PROJECT" -f docker-compose.dev.yml up -d --build + +echo "Waiting for database readiness..." +for i in $(seq 1 60); do + if docker compose -p "$COMPOSE_PROJECT" -f docker-compose.dev.yml exec -T db pg_isready -U devbits_dev -d devbits_dev >/dev/null 2>&1; then + echo "Database is ready." + break + fi + if [[ "$i" -eq 60 ]]; then + echo "Error: Database did not become ready within 60 seconds." + exit 1 + fi + sleep 1 +done + +echo "Waiting for backend health check..." +for i in $(seq 1 60); do + if command -v curl >/dev/null 2>&1; then + if curl -fsS "http://localhost:${DEVBITS_BACKEND_PORT}/health" >/dev/null 2>&1; then + echo "Backend is healthy." + break + fi + elif command -v wget >/dev/null 2>&1; then + if wget -q -O /dev/null "http://localhost:${DEVBITS_BACKEND_PORT}/health"; then + echo "Backend is healthy." + break + fi + fi + + if [[ "$i" -eq 60 ]]; then + echo "Error: Backend did not become healthy within 60 seconds." + docker compose -p "$COMPOSE_PROJECT" -f docker-compose.dev.yml logs backend --tail 100 + exit 1 + fi + sleep 1 +done + +echo "Launching frontend in local backend mode..." +cd "$ROOT" +EXPO_PUBLIC_LOCAL_API_PORT="$DEVBITS_BACKEND_PORT" "$SCRIPT_DIR/run-front.sh" --local $CLEAR_FRONTEND \ No newline at end of file diff --git a/Bash-Scripts/run-front.sh b/Bash-Scripts/run-front.sh new file mode 100644 index 0000000..d3cab0e --- /dev/null +++ b/Bash-Scripts/run-front.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +# Script: run-front.sh +# Does: Starts Expo frontend and lets you choose backend target (Production or Local). +# Use: ./run-front.sh [--local|--production] [--clear] [--dev-client] +# DB: None (frontend only). +# Ports: Metro uses LAN IP; local API defaults to :8080 (EXPO_PUBLIC_LOCAL_API_PORT overrides). +# Modes: Frontend=ON | Backend=Production URL or Local URL | Live stack untouched | Dev/Test DB untouched. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +FRONTEND_DIR="$ROOT/frontend" + +MODE="" +CLEAR_FLAG="" +DEV_CLIENT=false + +for arg in "$@"; do + case "$arg" in + --clear) + CLEAR_FLAG="--clear" + ;; + --dev-client) + DEV_CLIENT=true + ;; + --local) + MODE="local" + ;; + --production) + MODE="production" + ;; + *) + echo "Unknown argument: $arg" + echo "Usage: ./run-front.sh [--local|--production] [--clear] [--dev-client]" + exit 1 + ;; + esac +done + +detect_lan_ip() { + hostname -I 2>/dev/null | tr ' ' '\n' | grep -E '^10\.|^192\.168\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.' | head -n1 +} + +LAN_IP="$(detect_lan_ip || true)" +if [[ -z "$LAN_IP" ]]; then + LAN_IP="127.0.0.1" + echo "Warning: Could not detect private LAN IPv4. Falling back to 127.0.0.1." +fi + +if [[ -z "$MODE" ]]; then + echo "Select backend: 1) Production (devbits.ddns.net) 2) Local (LAN IP:8080)" + read -r -p "Choose [1/2]: " selection + case "$selection" in + 1) MODE="production" ;; + 2) MODE="local" ;; + *) + echo "Invalid selection." + exit 1 + ;; + esac +fi + +if [[ "$LAN_IP" != "127.0.0.1" ]]; then + export REACT_NATIVE_PACKAGER_HOSTNAME="$LAN_IP" + export EXPO_PACKAGER_HOSTNAME="$LAN_IP" +else + unset REACT_NATIVE_PACKAGER_HOSTNAME || true + unset EXPO_PACKAGER_HOSTNAME || true +fi +export EXPO_PUBLIC_API_URL="https://devbits.ddns.net" +export EXPO_PUBLIC_API_FALLBACK_URL="https://devbits.ddns.net" + +if [[ "$MODE" == "local" ]]; then + LOCAL_API_PORT="${EXPO_PUBLIC_LOCAL_API_PORT:-8080}" + export EXPO_PUBLIC_USE_LOCAL_API=1 + export EXPO_PUBLIC_LOCAL_API_URL="http://${LAN_IP}:${LOCAL_API_PORT}" + echo "Using local backend: $EXPO_PUBLIC_LOCAL_API_URL" +else + export EXPO_PUBLIC_USE_LOCAL_API=0 + unset EXPO_PUBLIC_LOCAL_API_URL || true + echo "Using production backend: https://devbits.ddns.net" +fi + +cd "$FRONTEND_DIR" + +EXPO_ARGS=(expo start --host lan) +if [[ "$DEV_CLIENT" == "true" ]]; then + EXPO_ARGS+=(--dev-client) +else + EXPO_ARGS+=(--go) +fi + +if [[ -n "$CLEAR_FLAG" ]]; then + EXPO_ARGS+=(--clear) +fi + +exec npx "${EXPO_ARGS[@]}" diff --git a/Bash-Scripts/run-tests.sh b/Bash-Scripts/run-tests.sh new file mode 100644 index 0000000..8988230 --- /dev/null +++ b/Bash-Scripts/run-tests.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Script: run-tests.sh +# Does: Starts isolated test Postgres, runs backend API tests on host Go toolchain, then tears test DB down. +# Use: ./run-tests.sh (set KEEP_TEST_DB=true to keep DB running) +# DB: devbits_test (from backend/.env.test or defaults) in compose project devbits-test-local. +# Ports: test DB mapped to :5432 by docker-compose.test.yml. +# Modes: Frontend=OFF | Backend=tests only (no live deployment changes) | Live stack untouched | Test DB only. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT/backend" +COMPOSE_PROJECT="devbits-test-local" + +KEEP_DB=${KEEP_TEST_DB:-false} + +echo "=== DevBits Test Suite ===" + +if ! command -v docker &> /dev/null; then + echo "Error: Docker is not installed. Please install Docker first." + exit 1 +fi + +if ! command -v docker compose &> /dev/null; then + echo "Error: Docker Compose is not installed. Please install Docker Compose first." + exit 1 +fi + +if [ -f .env.test ]; then + echo "Loading environment from .env.test..." + set -a + source .env.test + set +a +else + echo "Warning: .env.test not found. Using default test environment values." + export POSTGRES_TEST_DB=devbits_test + export POSTGRES_TEST_USER=testuser + export POSTGRES_TEST_PASSWORD=testpass123 +fi + +echo "Starting test database..." +docker compose -p "$COMPOSE_PROJECT" -f docker-compose.test.yml down -v +docker compose -p "$COMPOSE_PROJECT" -f docker-compose.test.yml up -d + +echo "Waiting for database to be ready..." +sleep 5 + +MAX_ATTEMPTS=30 +ATTEMPT=0 +while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + if docker compose -p "$COMPOSE_PROJECT" -f docker-compose.test.yml exec -T test-db pg_isready -U testuser -d devbits_test > /dev/null 2>&1; then + echo "Database is ready!" + break + fi + ATTEMPT=$((ATTEMPT + 1)) + echo "Waiting for database... ($ATTEMPT/$MAX_ATTEMPTS)" + sleep 2 +done + +if [ $ATTEMPT -eq $MAX_ATTEMPTS ]; then + echo "Error: Database failed to start within timeout" + exit 1 +fi + +export USE_TEST_DB=true + +echo "" +echo "Running tests..." +echo "" + +go test -v ./api/internal/tests/... + +TEST_RESULT=$? + +echo "" +if [ $TEST_RESULT -eq 0 ]; then + echo "All tests passed!" +else + echo "Tests failed!" +fi + +if [ "$KEEP_DB" != "true" ]; then + echo "" + echo "Stopping test database..." + docker compose -p "$COMPOSE_PROJECT" -f docker-compose.test.yml down -v +else + echo "" + echo "Test database is still running." + echo "Run 'docker compose -p $COMPOSE_PROJECT -f docker-compose.test.yml down' to stop it." +fi + +exit $TEST_RESULT diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md index e6640df..dd41481 100644 --- a/INSTRUCTIONS.md +++ b/INSTRUCTIONS.md @@ -1,121 +1,61 @@ -# DevBits Application Instructions +# Deployment Process -This document provides the essential commands for managing the backend services with Docker and for building and deploying the frontend application. +## Backend -## Backend Management (Docker) - -Use separate command sets for each environment. - -### Local DB + Backend (your dev machine) - -Run from project root (`c:\Users\eligf\DevBits`): - -```bash -docker compose -f backend/docker-compose.yml up -d -``` - -Rebuild local backend image: - -```bash -docker compose -f backend/docker-compose.yml up -d --build -``` - -Stop local stack: - -```bash -docker compose -f backend/docker-compose.yml down -``` - -Restart local stack: - -```bash -docker compose -f backend/docker-compose.yml restart -``` - -View local logs: - -```bash -docker compose -f backend/docker-compose.yml logs -f backend -docker compose -f backend/docker-compose.yml logs -f db -docker compose -f backend/docker-compose.yml logs -f nginx -``` - -### Live/Deployed DB + Backend (your server) - -Run these only on your deployed host where DevBits is installed. - -Create backend environment file once (required): - -```bash -cd /path/to/DevBits/backend -cp .env.example .env -# edit .env and set a strong POSTGRES_PASSWORD before first deploy -``` - -```bash -cd /path/to/DevBits/backend -docker compose up -d -docker compose logs -f db -``` - -Rebuild and restart deployment containers: - -```bash -cd /path/to/DevBits/backend -docker compose up -d --build -``` - -Stop deployment stack: - -```bash -cd /path/to/DevBits/backend -docker compose down -``` - -Important safety note: - -- Deployment reset/restore scripts in `backend/scripts` are destructive and should only be run in the environment you intend to modify. -- Never run reset commands against live DB unless you explicitly want a full wipe. - -### Deployment DB scripts - -All deployment database script usage is documented in: - -- `backend/scripts/README.md` - -## Frontend Management (EAS) - -All frontend commands should be run from the `frontend` directory (`c:\Users\eligf\DevBits\frontend`). +> **Live/Deployed Stack** +> +> ```bash +> cd /path/to/DevBits/backend +> docker compose up -d +> docker compose logs -f db +> ``` -### Install Dependencies +> Rebuild and restart: +> +> ```bash +> docker compose up -d --build +> ``` -If you haven't already, or if you've pulled new changes, install the necessary Node.js packages: +> [!TIP] +> Check `backend/scripts/README.md` for database operations. -```bash -npm install -``` +## Build -### Build the Android App. +> **Android Production Build** +> +> ```bash +> npx eas build -p android --profile production +> ``` > [!NOTE] -> If you want to build ios, just replace android with ios and fill out proper credentials. +> This generates an `.aab` file and uploads to your Expo account. + +> **iOS Production Build** +> +> Replace `android` with `ios` and fill out proper credentials. > -> *right now it is just my credentials that work. I need to add yall.* `my = Eli` +> ```bash +> npx eas build -p ios --profile production +> ``` -To create a production build of the Android application for the Google Play Store: +> [!NOTE] +> Currently requires MY credentials. Need to add team credentials. -```bash -npx eas build -p android --profile production -``` +## Submit -This will generate an `.aab` file and upload it to your Expo account. +> **Android to Google Play Store** +> +> Copy file out of expo and create new release on Google Play Console. -### Submit to Google Play Store +> [!NOTE] +> `npx eas submit -p android` failed, so manual submission is required until fix is in place. Im not sure what is happening. -After a successful build, you can submit the latest build to the Google Play Store for internal testing: +> **iOS to App Store** +> +> ```bash +> npx eas submit -p ios --latest --profile production +> ``` -```bash -npx eas submit -p android --latest --profile production -``` +--- -This command will automatically find the latest build, download it, and upload it to the Google Play Console. +`Workflow: Backend Setup → EAS Build → EAS Submit` diff --git a/Powershell-Scripts/run-db-tests.ps1 b/Powershell-Scripts/run-db-tests.ps1 new file mode 100644 index 0000000..c63003b --- /dev/null +++ b/Powershell-Scripts/run-db-tests.ps1 @@ -0,0 +1,100 @@ +# Script: run-db-tests.ps1 +# Does: Recreates isolated local dev DB/backend containers, waits for DB, then runs Go tests in a temporary golang container. +# Use: .\run-db-tests.ps1 +# DB: devbits_dev via host.docker.internal in compose project devbits-dev-local. +# Ports: backend default :8080, DB default :5433 (DEVBITS_BACKEND_PORT / DEVBITS_DB_PORT override). +# Modes: Frontend=OFF | Backend=only for local test infra | Live stack untouched | Dev/Test data isolated. + +$ErrorActionPreference = "Stop" + +function Test-PortInUse { + param([int]$Port) + + try { + $listener = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue + return $null -ne $listener + } + catch { + return $false + } +} + +function Resolve-Port { + param( + [string]$Label, + [int]$DefaultPort + ) + + $port = $DefaultPort + while (Test-PortInUse -Port $port) { + Write-Host "Port $port is already in use for $Label." -ForegroundColor Yellow + $inputPort = Read-Host "Enter alternate port for $Label (blank to exit)" + if ([string]::IsNullOrWhiteSpace($inputPort)) { + Write-Host "Exiting. Free port $DefaultPort or choose an alternate port next run." -ForegroundColor Red + exit 1 + } + + if (-not [int]::TryParse($inputPort, [ref]$port) -or $port -lt 1 -or $port -gt 65535) { + Write-Host "Invalid port: $inputPort" -ForegroundColor Red + $port = $DefaultPort + } + } + + return $port +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$composeProject = "devbits-dev-local" + +$dockerCmd = Get-Command docker -ErrorAction SilentlyContinue +if (-not $dockerCmd) { + Write-Host "Error: Docker is required." -ForegroundColor Red + exit 1 +} + +docker compose version | Out-Null + +$backendDefault = if ($env:DEVBITS_BACKEND_PORT -match '^\d+$') { [int]$env:DEVBITS_BACKEND_PORT } else { 8080 } +$dbDefault = if ($env:DEVBITS_DB_PORT -match '^\d+$') { [int]$env:DEVBITS_DB_PORT } else { 5433 } + +$backendPort = Resolve-Port -Label "backend" -DefaultPort $backendDefault +$dbPort = Resolve-Port -Label "postgres" -DefaultPort $dbDefault + +$env:DEVBITS_BACKEND_PORT = "$backendPort" +$env:DEVBITS_DB_PORT = "$dbPort" + +Push-Location $repoRoot +try { + docker compose -p $composeProject -f backend/docker-compose.dev.yml down --volumes --remove-orphans + docker compose -p $composeProject -f backend/docker-compose.dev.yml up -d --build + + Write-Host "Waiting for database readiness..." -ForegroundColor Yellow + for ($i = 1; $i -le 60; $i++) { + docker compose -p $composeProject -f backend/docker-compose.dev.yml exec -T db pg_isready -U devbits_dev -d devbits_dev *> $null + if ($LASTEXITCODE -eq 0) { + Write-Host "Database is ready." -ForegroundColor Green + break + } + + if ($i -eq 60) { + Write-Host "Error: Database did not become ready within 60 seconds." -ForegroundColor Red + exit 1 + } + Start-Sleep -Seconds 1 + } + + $backendPath = Join-Path $repoRoot "backend" + docker run --rm --add-host=host.docker.internal:host-gateway ` + -e USE_TEST_DB=true ` + -e POSTGRES_TEST_DB=devbits_dev ` + -e POSTGRES_TEST_USER=devbits_dev ` + -e POSTGRES_TEST_PASSWORD=devbits_dev_password ` + -e POSTGRES_TEST_HOST=host.docker.internal ` + -e POSTGRES_TEST_PORT=$dbPort ` + -v "${backendPath}:/app" -w /app/api golang:1.24 bash -c "go test ./..." + exit $LASTEXITCODE +} +finally { + Pop-Location +} \ No newline at end of file diff --git a/Powershell-Scripts/run-dev.ps1 b/Powershell-Scripts/run-dev.ps1 new file mode 100644 index 0000000..e642f2d --- /dev/null +++ b/Powershell-Scripts/run-dev.ps1 @@ -0,0 +1,127 @@ +# Script: run-dev.ps1 +# Does: Boots local dev DB + local backend (isolated compose project), then launches frontend in local mode. +# Use: .\run-dev.ps1 [-Clear] +# DB: devbits_dev (user/pass: devbits_dev/devbits_dev_password) in compose project devbits-dev-local. +# Ports: backend default :8080, DB default :5433 (DEVBITS_BACKEND_PORT / DEVBITS_DB_PORT override). +# Modes: Frontend=ON(local API) | Backend=ON(local Docker) | Live stack untouched | Test DB untouched. + +param( + [switch]$Clear +) + +$ErrorActionPreference = "Stop" + +function Test-PortInUse { + param([int]$Port) + + try { + $listener = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue + return $null -ne $listener + } + catch { + return $false + } +} + +function Resolve-Port { + param( + [string]$Label, + [int]$DefaultPort + ) + + $port = $DefaultPort + while (Test-PortInUse -Port $port) { + Write-Host "Port $port is already in use for $Label." -ForegroundColor Yellow + $inputPort = Read-Host "Enter alternate port for $Label (blank to exit)" + if ([string]::IsNullOrWhiteSpace($inputPort)) { + Write-Host "Exiting. Free port $DefaultPort or choose an alternate port next run." -ForegroundColor Red + exit 1 + } + + if (-not [int]::TryParse($inputPort, [ref]$port) -or $port -lt 1 -or $port -gt 65535) { + Write-Host "Invalid port: $inputPort" -ForegroundColor Red + $port = $DefaultPort + } + } + + return $port +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$backendDir = Join-Path $repoRoot "backend" +$composeProject = "devbits-dev-local" + +$dockerCmd = Get-Command docker -ErrorAction SilentlyContinue +if (-not $dockerCmd) { + Write-Host "Error: Docker is required." -ForegroundColor Red + exit 1 +} + +docker compose version | Out-Null + +$backendDefault = if ($env:DEVBITS_BACKEND_PORT -match '^\d+$') { [int]$env:DEVBITS_BACKEND_PORT } else { 8080 } +$dbDefault = if ($env:DEVBITS_DB_PORT -match '^\d+$') { [int]$env:DEVBITS_DB_PORT } else { 5433 } + +$backendPort = Resolve-Port -Label "backend" -DefaultPort $backendDefault +$dbPort = Resolve-Port -Label "postgres" -DefaultPort $dbDefault + +$env:DEVBITS_BACKEND_PORT = "$backendPort" +$env:DEVBITS_DB_PORT = "$dbPort" + +Write-Host "Using backend port $backendPort and db port $dbPort." -ForegroundColor Cyan + +Push-Location $backendDir +try { + docker compose -p $composeProject -f docker-compose.dev.yml down --volumes --remove-orphans + docker compose -p $composeProject -f docker-compose.dev.yml up -d --build + + Write-Host "Waiting for database readiness..." -ForegroundColor Yellow + for ($i = 1; $i -le 60; $i++) { + docker compose -p $composeProject -f docker-compose.dev.yml exec -T db pg_isready -U devbits_dev -d devbits_dev *> $null + if ($LASTEXITCODE -eq 0) { + Write-Host "Database is ready." -ForegroundColor Green + break + } + + if ($i -eq 60) { + Write-Host "Error: Database did not become ready within 60 seconds." -ForegroundColor Red + exit 1 + } + Start-Sleep -Seconds 1 + } + + Write-Host "Waiting for backend health check..." -ForegroundColor Yellow + for ($i = 1; $i -le 60; $i++) { + try { + $response = Invoke-WebRequest -Uri "http://localhost:$backendPort/health" -UseBasicParsing -TimeoutSec 2 + if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) { + Write-Host "Backend is healthy." -ForegroundColor Green + break + } + } + catch { + } + + if ($i -eq 60) { + Write-Host "Error: Backend did not become healthy within 60 seconds." -ForegroundColor Red + docker compose -p $composeProject -f docker-compose.dev.yml logs backend --tail 100 + exit 1 + } + + Start-Sleep -Seconds 1 + } +} +finally { + Pop-Location +} + +Write-Host "Launching frontend in local backend mode..." -ForegroundColor Cyan +$env:EXPO_PUBLIC_LOCAL_API_PORT = "$backendPort" +if ($Clear) { + & (Join-Path $scriptDir "run-front.ps1") -Local -Clear +} +else { + & (Join-Path $scriptDir "run-front.ps1") -Local +} +exit $LASTEXITCODE \ No newline at end of file diff --git a/Powershell-Scripts/run-front.ps1 b/Powershell-Scripts/run-front.ps1 new file mode 100644 index 0000000..51af854 --- /dev/null +++ b/Powershell-Scripts/run-front.ps1 @@ -0,0 +1,113 @@ +# Script: run-front.ps1 +# Does: Starts Expo frontend and lets you choose backend target (Production or Local). +# Use: .\run-front.ps1 [-Local|-Production] [-Clear] [-DevClient] +# DB: None (frontend only). +# Ports: Metro uses LAN IP; local API defaults to :8080 (EXPO_PUBLIC_LOCAL_API_PORT overrides). +# Modes: Frontend=ON | Backend=Production URL or Local URL | Live stack untouched | Dev/Test DB untouched. + +param( + [switch]$Clear, + [switch]$Local, + [switch]$Production, + [switch]$DevClient +) + +$ErrorActionPreference = "Stop" + +if ($Local -and $Production) { + Write-Host "Choose either -Local or -Production, not both." -ForegroundColor Red + exit 1 +} + +function Get-LanIPv4 { + $ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Where-Object { + $_.PrefixOrigin -eq 'Dhcp' -and + $_.IPAddress -match '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)' + } | + Select-Object -First 1 -ExpandProperty IPAddress + + if (-not $ip) { + $ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Where-Object { + $_.IPAddress -match '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)' + } | + Select-Object -First 1 -ExpandProperty IPAddress + } + + if (-not $ip) { + Write-Host "Warning: Could not detect private LAN IPv4. Falling back to 127.0.0.1." -ForegroundColor Yellow + return "127.0.0.1" + } + + return $ip +} + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$root = Split-Path -Parent $scriptDir +$frontendDir = Join-Path $root "frontend" +$localIp = Get-LanIPv4 + +$mode = "" +if ($Local) { + $mode = "local" +} +elseif ($Production) { + $mode = "production" +} +else { + Write-Host "Select backend: 1) Production (devbits.ddns.net) 2) Local (LAN IP:8080)" + $selection = Read-Host "Choose [1/2]" + switch ($selection) { + "1" { $mode = "production" } + "2" { $mode = "local" } + default { + Write-Host "Invalid selection." -ForegroundColor Red + exit 1 + } + } +} + +if ($localIp -ne "127.0.0.1") { + $env:REACT_NATIVE_PACKAGER_HOSTNAME = $localIp + $env:EXPO_PACKAGER_HOSTNAME = $localIp +} +else { + Remove-Item Env:REACT_NATIVE_PACKAGER_HOSTNAME -ErrorAction SilentlyContinue + Remove-Item Env:EXPO_PACKAGER_HOSTNAME -ErrorAction SilentlyContinue +} +$env:EXPO_PUBLIC_API_URL = "https://devbits.ddns.net" +$env:EXPO_PUBLIC_API_FALLBACK_URL = "https://devbits.ddns.net" + +if ($mode -eq "local") { + $port = if ($env:EXPO_PUBLIC_LOCAL_API_PORT) { $env:EXPO_PUBLIC_LOCAL_API_PORT } else { "8080" } + $env:EXPO_PUBLIC_USE_LOCAL_API = "1" + $env:EXPO_PUBLIC_LOCAL_API_URL = "http://$($localIp):$port" + Write-Host "Using local backend: $($env:EXPO_PUBLIC_LOCAL_API_URL)" -ForegroundColor Green +} +else { + $env:EXPO_PUBLIC_USE_LOCAL_API = "0" + Remove-Item Env:EXPO_PUBLIC_LOCAL_API_URL -ErrorAction SilentlyContinue + Write-Host "Using production backend: https://devbits.ddns.net" -ForegroundColor Green +} + +Push-Location $frontendDir +try { + $expoArgs = @("expo", "start", "--host", "lan") + if ($DevClient) { + $expoArgs += "--dev-client" + } + else { + $expoArgs += "--go" + } + + if ($Clear) { + $expoArgs += "--clear" + } + + & npx @expoArgs + exit $LASTEXITCODE +} +finally { + Pop-Location +} diff --git a/Powershell-Scripts/run-tests.ps1 b/Powershell-Scripts/run-tests.ps1 new file mode 100644 index 0000000..22eed73 --- /dev/null +++ b/Powershell-Scripts/run-tests.ps1 @@ -0,0 +1,106 @@ +# Script: run-tests.ps1 +# Does: Starts isolated test Postgres, runs backend API tests on host Go toolchain, then tears test DB down. +# Use: .\run-tests.ps1 [-KeepDb] +# DB: devbits_test (from backend/.env.test or defaults) in compose project devbits-test-local. +# Ports: test DB mapped to :5432 by docker-compose.test.yml. +# Modes: Frontend=OFF | Backend=tests only (no live deployment changes) | Live stack untouched | Test DB only. + +param( + [switch]$KeepDb +) + +$ErrorActionPreference = "Stop" +$composeProject = "devbits-test-local" + +Write-Host "=== DevBits Test Suite ===" -ForegroundColor Cyan + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = Split-Path -Parent $scriptDir +$backendDir = Resolve-Path (Join-Path $repoRoot "backend") +Push-Location $backendDir +try { + $dockerCmd = Get-Command docker -ErrorAction SilentlyContinue + if (-not $dockerCmd) { + Write-Host "Error: Docker is not installed. Please install Docker first." -ForegroundColor Red + exit 1 + } + + $envTestFile = Join-Path $backendDir ".env.test" + if (Test-Path $envTestFile) { + Write-Host "Loading environment from .env.test..." -ForegroundColor Yellow + Get-Content $envTestFile | Where-Object { $_ -notmatch '^#' -and $_ -match '=' } | ForEach-Object { + $key, $value = $_ -split '=', 2 + [Environment]::SetEnvironmentVariable($key, $value, "Process") + } + } + else { + Write-Host "Warning: .env.test not found. Using default test environment values." -ForegroundColor Yellow + [Environment]::SetEnvironmentVariable("POSTGRES_TEST_DB", "devbits_test", "Process") + [Environment]::SetEnvironmentVariable("POSTGRES_TEST_USER", "testuser", "Process") + [Environment]::SetEnvironmentVariable("POSTGRES_TEST_PASSWORD", "testpass123", "Process") + } + + [Environment]::SetEnvironmentVariable("USE_TEST_DB", "true", "Process") + + $envTestFile = Join-Path $backendDir ".env.test" + $testDbRunning = docker compose -p $composeProject -f docker-compose.test.yml --env-file $envTestFile ps 2>$null | Select-String "Up" -Quiet + if (-not $testDbRunning) { + Write-Host "Starting test database..." -ForegroundColor Yellow + docker compose -p $composeProject -f docker-compose.test.yml --env-file $envTestFile up -d + + Write-Host "Waiting for database to be ready..." -ForegroundColor Yellow + Start-Sleep -Seconds 5 + + $maxAttempts = 30 + $attempt = 0 + while ($attempt -lt $maxAttempts) { + docker compose -p $composeProject -f docker-compose.test.yml --env-file $envTestFile exec -T test-db pg_isready -U testuser -d devbits_test 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Host "Database is ready!" -ForegroundColor Green + break + } + $attempt++ + Write-Host "Waiting for database... ($attempt/$maxAttempts)" -ForegroundColor Yellow + Start-Sleep -Seconds 2 + } + + if ($attempt -eq $maxAttempts) { + Write-Host "Error: Database failed to start within timeout" -ForegroundColor Red + exit 1 + } + } + else { + Write-Host "Test database is already running." -ForegroundColor Green + } + + Write-Host "" + Write-Host "Running tests..." -ForegroundColor Cyan + Write-Host "" + + go test -v ./api/internal/tests/... + $testResult = $LASTEXITCODE + + Write-Host "" + if ($testResult -eq 0) { + Write-Host "All tests passed!" -ForegroundColor Green + } + else { + Write-Host "Tests failed!" -ForegroundColor Red + } + + if (-not $KeepDb) { + Write-Host "" + Write-Host "Stopping test database..." -ForegroundColor Yellow + docker compose -p $composeProject -f docker-compose.test.yml down + } + else { + Write-Host "" + Write-Host "Test database is still running." -ForegroundColor Yellow + Write-Host "Run 'docker compose -p $composeProject -f docker-compose.test.yml down' to stop it." -ForegroundColor Yellow + } + + exit $testResult +} +finally { + Pop-Location +} diff --git a/README.md b/README.md index 39b378e..9329183 100644 --- a/README.md +++ b/README.md @@ -21,117 +21,56 @@ Some quirky names for things (frontend only) - Database: PostgreSQL or MySQL - Host: On AWS, full system design pending -## Local Testing +## Local Development -### Backend Testing +### Quick Start -Install the following packages: - -- [**Go**](https://go.dev/doc/install) (for running the API) -- [**SQLite3**](https://www.sqlite.org/index.html) (for database operations) - -#### 1. Navigate to the Project Root - -Change into the project directory: +Start only the frontend (choose production or local backend at launch): ```bash -cd /path/to/DevBits +./run-front.sh ``` -#### 2. Start the Database - -1. Open a terminal and navigate to the database directory: - - ```bash - cd backend/api/internal/database - ``` - -2. Launch the SQLite database: - - ```bash - sqlite3 dev.sqlite3 - ``` - -3. (Optional) Open the `create_tables.sql` file in a new terminal for reference: - - ```bash - nvim create_tables.sql - ``` - -#### 3. Start the API - -1. Open another terminal and navigate to the backend directory: - - ```bash - cd backend - ``` - -2. Run the API: - - ```bash - go run ./api - ``` - -That's it! You're ready to start working with the DevBits API and database. - ---- - -### Frontend Testing - -Install the following packages: - -- [**Node**](https://nodejs.org/en/download/package-manager) (for running the React Native App) - -#### 1. Navigate to the DevBits Frontend - -Change into the project directory: +Start full local stack (dev PostgreSQL + backend + frontend in local API mode): ```bash -cd /path/to/DevBits/frontend +./run-dev.sh ``` -#### 2. Check required packages are installed +Run backend tests using dockerized Go against the dev DB stack: ```bash -npm install +./run-db-tests.sh ``` -#### 3. Start the app +PowerShell equivalents: -```bash -npm run frontend +```powershell +.\run-front.ps1 +.\run-dev.ps1 +.\run-db-tests.ps1 ``` -In the output, you'll find options to open the app in a - -- [development build](https://docs.expo.dev/develop/development-builds/introduction/) -- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) -- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) -- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo +Scan the QR code with Expo Go on your phone. The app will automatically connect to your local backend. -### Run app +### Verification Checklist -#### 1. Navigate to the DevBits frontend (required to run npm commands) +- Fresh clone frontend check: run `chmod +x run-front.sh run-dev.sh run-db-tests.sh`, then `./run-front.sh`, choose `Production`, and confirm Expo starts. +- Full local stack: run `./run-dev.sh`, confirm backend health at `http://:8080/health`, then validate app API calls from a phone on same WiFi. +- DB tests: run `./run-db-tests.sh` and confirm it exits with code `0`. -Change into the project directory: +### Prerequisites -```bash -cd /path/to/DevBits/frontend -``` - -#### 2. Run all required commands - -```bash -npm run all -``` +1. Install Docker and Docker Compose (v2) +2. Install Node.js/npm for Expo frontend -#### Windows quick start (root script) +### Troubleshooting -From the project root, you can launch backend + frontend together: +- Docker Desktop on Windows: ensure file sharing is enabled for the repo path. +- If `8080` or `5433` is occupied, the scripts prompt for alternate ports (or allow exit with guidance). +- WSL/Docker Desktop/Linux engine differences: run scripts from the environment that owns your Docker daemon and ensure localhost port forwarding is enabled. -```powershell -./run-all.ps1 -``` +For detailed instructions, see [INSTRUCTIONS.md](INSTRUCTIONS.md). ## Deployment DB scripts diff --git a/README_PUBLISHING.md b/README_PUBLISHING.md deleted file mode 100644 index 649327d..0000000 --- a/README_PUBLISHING.md +++ /dev/null @@ -1,106 +0,0 @@ -# DevBits — Publishing & Current Status - -This file summarizes what I changed in the repo to prepare for App Store / Play Store publishing, and the remaining steps to finish iOS release (TestFlight/App Store) and Android (Play Store). - ---- - -## What I changed (completed) - -- Frontend - - `frontend/app.json` - - Set `bundleIdentifier` to `com.devbits.frontend`. - - Added `ios.associatedDomains` with `applinks:devbits.ddns.net`. - - `frontend/eas.json` - - Added production envs: `EXPO_PUBLIC_API_URL`, `EXPO_PUBLIC_API_FALLBACK_URL`, `EXPO_PUBLIC_SITE_URL` (all pointing to `https://devbits.ddns.net`). - - Set `credentialsSource` to `remote` so EAS will use Expo-stored iOS credentials. - - `frontend/public/apple-app-site-association` and `frontend/public/.well-known/assetlinks.json` — templates added for Universal Links / App Links. - - `frontend/DEEP_LINK_SETUP.md` — instructions for extracting Android keystore fingerprints and deep link setup. - -- Backend - - `backend/nginx/nginx.conf` — added explicit locations to serve `/apple-app-site-association` and `/.well-known/assetlinks.json` as JSON. - - `backend/Dockerfile` + `backend/docker-entrypoint.sh` — added entrypoint to auto-generate `DEVBITS_JWT_SECRET` if not provided at runtime. - - `backend/.env.example` — documented `DEVBITS_JWT_SECRET` and APNs environment hints. - -- Repo security/devops - - Root and `frontend` `.gitignore` updated to ignore keystores, service accounts, `.env`, and other secrets. - - `.githooks/pre-commit` added to scan staged files for secrets; enable scripts included. - ---- - -## What remains (high-priority) - -1. Verify TLS for `https://devbits.ddns.net` (Universal Links require a valid public CA certificate). -2. Set a persistent `DEVBITS_JWT_SECRET` in production environment (entrypoint generates a secret if missing, but persist it). -3. Apple Developer / App Store setup: - - Create App ID with bundle id `com.devbits.frontend` and enable capabilities: Associated Domains, Push Notifications, (Sign in with Apple if needed). - - Create an APNs Key (`AuthKey_*.p8`) in developer.apple.com and store it securely. - - Create an App Store Connect API Key (Team/Individual key) and upload the `.p8`, record Key ID + Issuer ID. -4. Upload App Store Connect API Key to the Expo account (Dashboard → Account → Credentials → App Store Connect API Keys) — optional but required for non-interactive CI submission. -5. Upload APNs `.p8` to Expo (optional) and store a copy in your backend secret manager (recommended env: `DEVBITS_APNS_KEY_BASE64`, `DEVBITS_APNS_KEY_ID`, `DEVBITS_APPLE_TEAM_ID`, `DEVBITS_APP_BUNDLE_ID`). -6. Build iOS with EAS and upload to TestFlight. (You can build now and upload manually if you prefer.) -7. Prepare App Store listing (screenshots, descriptions, privacy answers, support URL) and submit to review. - ---- - -## Commands to run now (build & upload flow) - -- Login & check credentials: - -```bash -cd frontend -eas login -eas credentials --platform ios -``` - -- Start an interactive iOS build (will use Expo remote credentials if uploaded): - -```bash -eas build -p ios --profile production -``` - -- Download an artifact and submit manually (or let EAS submit): - -```bash -eas build:list --platform ios -eas build:download --platform ios --id -# then upload `.ipa` via Transporter (macOS) or use `eas submit` with proper keys -``` - ---- - -## APNs & server push (what to store and where) - -- The backend (Go service) is the component that sends push notifications. The mobile frontend only receives device tokens. -- Store the APNs `.p8` in your production secrets; do not commit to git. -- Recommended env variables: - - `DEVBITS_APNS_KEY_BASE64` — base64 encoding of the `.p8` file - - `DEVBITS_APNS_KEY_ID` — the App Store key id - - `DEVBITS_APPLE_TEAM_ID` — your Apple Team ID - - `DEVBITS_APP_BUNDLE_ID` — `com.devbits.frontend` - -Example to create `DEVBITS_APNS_KEY_BASE64`: - -```bash -base64 AuthKey_ABC123.p8 > authkey_b64.txt -export DEVBITS_APNS_KEY_BASE64="$(cat authkey_b64.txt)" -``` - -I can add backend code to decode `DEVBITS_APNS_KEY_BASE64` and initialize an APNs client when you want. - ---- - -## Notes / FAQs - -- Do you need APNs `.p8` to build the iOS .ipa? No — APNs `.p8` is not required to produce a signed build. It's required for server push functionality later, and sometimes for provisioning when manually managing certs. -- Will the bundle show up in App Store Connect automatically after uploading the App Store Connect API key to Expo? No. Uploading the API key to Expo allows EAS/Expo to authenticate to App Store Connect and submit builds. The App ID in Apple Developer either needs to be created manually, or EAS can create it for you during the credential provisioning step if you allow it. -- Can you test in Expo Go? Expo Go runs JS bundles in a shared native app and is not a signed standalone app — Universal Links, push notifications and any custom native code require building a standalone app via EAS. - ---- - -If you want, I can: - -- Run the interactive CLI flow and upload the App Store Connect `.p8` to your Expo account (you'll need to provide the `.p8` path and Key ID + Issuer ID when prompted). -- Start an `eas build -p ios --profile production` now. -- Add backend code to initialize APNs from `DEVBITS_APNS_KEY_BASE64`. - -Tell me which of the three you want next. diff --git a/backend/.gitignore b/backend/.gitignore index f050d0e..40f1929 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,5 +1,5 @@ -.env */.env +*/.env.test *.exe *.exe~ diff --git a/backend/Dockerfile b/backend/Dockerfile index 1b6950d..0861534 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -45,6 +45,7 @@ RUN apk add --no-cache openssl COPY docker-entrypoint.sh ./docker-entrypoint.sh RUN chmod +x ./docker-entrypoint.sh -# Entrypoint generates environment secrets if missing, then runs the app -ENTRYPOINT ["/root/docker-entrypoint.sh"] +# Entrypoint generates environment secrets if missing, then runs the app. +# Use /bin/sh explicitly to avoid shebang/line-ending portability issues. +ENTRYPOINT ["/bin/sh", "/root/docker-entrypoint.sh"] CMD ["/root/main"] diff --git a/backend/api/internal/database/db.go b/backend/api/internal/database/db.go index 3df7872..2a94af8 100644 --- a/backend/api/internal/database/db.go +++ b/backend/api/internal/database/db.go @@ -10,6 +10,7 @@ import ( "time" _ "github.com/lib/pq" // PostgreSQL driver + _ "modernc.org/sqlite" ) var DB *sql.DB // Global database instance @@ -22,9 +23,33 @@ func Connect() { var err error var dsn string - // Prefer PostgreSQL connection if DATABASE_URL is set - dbURL := os.Getenv("DATABASE_URL") - if dbURL != "" { + // Check for test database mode + if os.Getenv("USE_TEST_DB") == "true" { + driverName = "postgres" + db := os.Getenv("POSTGRES_TEST_DB") + if db == "" { + db = "devbits_test" + } + user := os.Getenv("POSTGRES_TEST_USER") + if user == "" { + user = "testuser" + } + password := os.Getenv("POSTGRES_TEST_PASSWORD") + if password == "" { + log.Fatal("POSTGRES_TEST_PASSWORD is required when USE_TEST_DB=true") + } + host := os.Getenv("POSTGRES_TEST_HOST") + if host == "" { + host = "localhost" + } + port := os.Getenv("POSTGRES_TEST_PORT") + if port == "" { + port = "5432" + } + log.Printf("Using test Postgres host: %s", host) + dsn = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", user, password, host, port, db) + } else if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" { + // Production PostgreSQL connection driverName = "postgres" dsn = dbURL } else { @@ -107,10 +132,24 @@ func ensureSqliteSchema() error { } func execSqlFile(filename string) error { - path := filepath.Join("api", "internal", "database", filename) - content, err := os.ReadFile(path) + rel := filepath.Join("api", "internal", "database", filename) + + // Try several locations: current dir, and up to 4 parent dirs. + var content []byte + var err error + tried := []string{} + dir := "." + for i := 0; i < 5; i++ { + path := filepath.Clean(filepath.Join(dir, rel)) + tried = append(tried, path) + content, err = os.ReadFile(path) + if err == nil { + break + } + dir = filepath.Join(dir, "..") + } if err != nil { - return fmt.Errorf("failed to read %s: %w", path, err) + return fmt.Errorf("failed to read %s (tried: %v): %w", rel, tried, err) } statements := strings.Split(string(content), ";") diff --git a/backend/api/internal/database/project_queries.go b/backend/api/internal/database/project_queries.go index f5ef401..4276f26 100644 --- a/backend/api/internal/database/project_queries.go +++ b/backend/api/internal/database/project_queries.go @@ -262,7 +262,8 @@ func QueryGetProjectFollowers(projectID int) ([]int, int, error) { SELECT u.id FROM users u JOIN projectfollows pf ON u.id = pf.user_id - WHERE pf.project_id = $1` + WHERE pf.project_id = $1 + ORDER BY u.id` return getProjectFollowersOrFollowing(query, projectID) } @@ -281,7 +282,8 @@ func QueryGetProjectFollowersUsernames(projectID int) ([]string, int, error) { SELECT u.username FROM users u JOIN projectfollows pf ON u.id = pf.user_id - WHERE pf.project_id = $1` + WHERE pf.project_id = $1 + ORDER BY u.username` return getProjectFollowersOrFollowingUsernames(query, projectID) } @@ -305,7 +307,8 @@ func QueryGetProjectFollowing(username string) ([]int, int, error) { SELECT p.id FROM projects p JOIN projectfollows pf ON p.id = pf.project_id - WHERE pf.user_id = $1` + WHERE pf.user_id = $1 + ORDER BY p.id` return getProjectFollowersOrFollowing(query, userID) } @@ -329,7 +332,8 @@ func QueryGetProjectFollowingNames(username string) ([]string, int, error) { SELECT p.name FROM projects p JOIN projectfollows pf ON p.id = pf.project_id - WHERE pf.user_id = $1` + WHERE pf.user_id = $1 + ORDER BY p.name` return getProjectFollowersOrFollowingUsernames(query, userID) } diff --git a/backend/api/internal/database/user_queries.go b/backend/api/internal/database/user_queries.go index 701637d..5e2a993 100644 --- a/backend/api/internal/database/user_queries.go +++ b/backend/api/internal/database/user_queries.go @@ -334,7 +334,8 @@ func GetUserFollowers(username string) ([]*ApiUser, error) { SELECT u.id, u.username, u.picture, u.bio, u.links, u.settings, u.creation_date FROM users u JOIN userfollows f ON u.id = f.follower_id - WHERE f.followed_id = $1; + WHERE f.followed_id = $1 + ORDER BY u.id; ` rows, err := DB.Query(query, user.Id) if err != nil { @@ -384,7 +385,8 @@ func GetUserFollowing(username string) ([]*ApiUser, error) { SELECT u.id, u.username, u.picture, u.bio, u.links, u.settings, u.creation_date FROM users u JOIN userfollows f ON u.id = f.followed_id - WHERE f.follower_id = $1; + WHERE f.follower_id = $1 + ORDER BY u.id; ` rows, err := DB.Query(query, user.Id) if err != nil { @@ -434,7 +436,8 @@ func GetUserFollowersUsernames(username string) ([]string, error) { SELECT u.username FROM users u JOIN userfollows f ON u.id = f.follower_id - WHERE f.followed_id = $1; + WHERE f.followed_id = $1 + ORDER BY u.username; ` rows, err := DB.Query(query, user.Id) if err != nil { @@ -468,7 +471,8 @@ func GetUserFollowingUsernames(username string) ([]string, error) { SELECT u.username FROM users u JOIN userfollows f ON u.id = f.followed_id - WHERE f.follower_id = $1; + WHERE f.follower_id = $1 + ORDER BY u.username; ` rows, err := DB.Query(query, user.Id) if err != nil { @@ -537,4 +541,3 @@ func CreateUserLoginInfo(info *UserLoginInfo) error { } return nil } - diff --git a/backend/api/internal/tests/main_test.go b/backend/api/internal/tests/main_test.go index 7c8983c..38baf53 100644 --- a/backend/api/internal/tests/main_test.go +++ b/backend/api/internal/tests/main_test.go @@ -14,6 +14,8 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" + "runtime" "strings" "testing" @@ -24,8 +26,8 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + _ "github.com/lib/pq" "github.com/stretchr/testify/assert" - _ "modernc.org/sqlite" ) // TestCase describes a single HTTP request/response test. @@ -137,7 +139,10 @@ func setupTestRouter() *gin.Engine { // loadSQLFile executes all statements from a SQL file on db. // An optional series of old/new string pairs can be passed to rewrite // the SQL before execution (e.g. to make PostgreSQL DDL run on SQLite). -func loadSQLFile(db *sql.DB, path string, replacements ...string) error { +func loadSQLFile(db *sql.DB, filename string, replacements ...string) error { + _, callerFile, _, _ := runtime.Caller(0) + root := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(callerFile)))) + path := filepath.Join(root, "api", "internal", "database", filename) sqlBytes, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read %s: %v", path, err) @@ -225,34 +230,22 @@ func (tc *TestCase) Run(t *testing.T, serverURL string) { } func TestAPI(t *testing.T) { - // Initialize logger so handler utilities don't panic on first use. logger.InitLogger() - // Set up an in-memory SQLite database so tests never touch disk or - // require a running server process. - db, err := sql.Open("sqlite", ":memory:") - if err != nil { - t.Fatalf("Failed to open test database: %v", err) - } - defer db.Close() - - database.DB = db - - if _, err := db.Exec("PRAGMA foreign_keys=ON;"); err != nil { - t.Fatalf("Failed to enable foreign keys: %v", err) + database.DB = nil + database.Connect() + db := database.DB + if db == nil { + t.Fatal("Failed to connect to database") } - if err := loadSQLFile(db, "../database/create_tables.sql", - // create_tables.sql uses PostgreSQL SERIAL; convert to SQLite syntax - "SERIAL PRIMARY KEY", "INTEGER PRIMARY KEY AUTOINCREMENT", - ); err != nil { + if err := loadSQLFile(db, "create_tables.sql"); err != nil { t.Fatalf("Failed to load schema: %v", err) } - if err := loadSQLFile(db, "../database/create_test_data.sql"); err != nil { + if err := loadSQLFile(db, "create_test_data.sql"); err != nil { t.Fatalf("Failed to load test data: %v", err) } - // Insert sentinel "deleted" user (id=-1) required by comment soft-delete logic. if _, err := db.Exec(`INSERT INTO users (id, username, picture, bio, links, settings, creation_date) VALUES (-1, 'deleted_user', '', '', '[]', '{}', '1970-01-01 00:00:00')`); err != nil { t.Fatalf("Failed to insert sentinel deleted user: %v", err) } diff --git a/backend/api/internal/tests/user_test.go b/backend/api/internal/tests/user_test.go index 342c6a7..27a187e 100644 --- a/backend/api/internal/tests/user_test.go +++ b/backend/api/internal/tests/user_test.go @@ -38,13 +38,13 @@ var user_tests = []TestCase{ AuthAs: "dev_user1:1", }, - // creating same user again – UNIQUE constraint error (SQLite lowercase table name) + // creating same user again – UNIQUE constraint error (PostgreSQL) { Method: http.MethodPost, Endpoint: "/users", Input: `{"username":"new_user","bio":"This is a test user.","links":["https://example.com","https://another-link.com"],"picture":"https://example.com/profile.jpg"}`, ExpectedStatus: http.StatusInternalServerError, - ExpectedBody: `{"error":"Internal Server Error","message":"Failed to create user: failed to insert user: constraint failed: UNIQUE constraint failed: users.username (2067)"}`, + ExpectedBody: `{"error":"Internal Server Error","message":"Failed to create user: failed to insert user: pq: duplicate key value violates unique constraint \"users_username_key\" (23505)"}`, AuthAs: "dev_user1:1", }, diff --git a/backend/backups/db/devbits-db-20260226-030000.sql b/backend/backups/db/devbits-db-20260226-030000.sql new file mode 100644 index 0000000..b1cbe2f --- /dev/null +++ b/backend/backups/db/devbits-db-20260226-030000.sql @@ -0,0 +1,1472 @@ +-- +-- PostgreSQL database dump +-- + +\restrict nJGpB5uSaQ5fCmjJfoGSoRiAXQCJU5J1JZgY5TlSg4ValCxlsLWEM8UKNr4haNs + +-- Dumped from database version 15.16 (Debian 15.16-1.pgdg13+1) +-- Dumped by pg_dump version 15.16 (Debian 15.16-1.pgdg13+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: commentlikes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.commentlikes ( + user_id integer NOT NULL, + comment_id integer NOT NULL +); + + +-- +-- Name: comments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.comments ( + id integer NOT NULL, + parent_comment_id integer, + user_id integer NOT NULL, + content text NOT NULL, + media json, + likes integer DEFAULT 0, + creation_date timestamp without time zone NOT NULL +); + + +-- +-- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.comments_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id; + + +-- +-- Name: directmessages; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directmessages ( + id integer NOT NULL, + sender_id integer NOT NULL, + recipient_id integer NOT NULL, + content text NOT NULL, + creation_date timestamp without time zone NOT NULL, + read_at timestamp without time zone +); + + +-- +-- Name: directmessages_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directmessages_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directmessages_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directmessages_id_seq OWNED BY public.directmessages.id; + + +-- +-- Name: notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.notifications ( + id integer NOT NULL, + user_id integer NOT NULL, + actor_id integer NOT NULL, + type character varying(50) NOT NULL, + post_id integer, + project_id integer, + comment_id integer, + created_at timestamp without time zone NOT NULL, + read_at timestamp without time zone +); + + +-- +-- Name: notifications_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.notifications_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: notifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.notifications_id_seq OWNED BY public.notifications.id; + + +-- +-- Name: postcomments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.postcomments ( + post_id integer NOT NULL, + comment_id integer NOT NULL, + user_id integer NOT NULL +); + + +-- +-- Name: postlikes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.postlikes ( + user_id integer NOT NULL, + post_id integer NOT NULL +); + + +-- +-- Name: posts; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.posts ( + id integer NOT NULL, + content text NOT NULL, + media json, + project_id integer, + creation_date timestamp without time zone NOT NULL, + user_id integer NOT NULL, + likes integer DEFAULT 0 +); + + +-- +-- Name: posts_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.posts_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: posts_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.posts_id_seq OWNED BY public.posts.id; + + +-- +-- Name: postsaves; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.postsaves ( + user_id integer NOT NULL, + post_id integer NOT NULL +); + + +-- +-- Name: projectbuilders; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projectbuilders ( + project_id integer NOT NULL, + user_id integer NOT NULL +); + + +-- +-- Name: projectcomments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projectcomments ( + project_id integer NOT NULL, + comment_id integer NOT NULL, + user_id integer NOT NULL +); + + +-- +-- Name: projectfollows; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projectfollows ( + user_id integer NOT NULL, + project_id integer NOT NULL +); + + +-- +-- Name: projectlikes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projectlikes ( + user_id integer NOT NULL, + project_id integer NOT NULL +); + + +-- +-- Name: projects; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.projects ( + id integer NOT NULL, + name character varying(255) NOT NULL, + description text, + about_md text, + status integer, + likes integer DEFAULT 0, + links json, + tags json, + media json, + owner integer NOT NULL, + creation_date timestamp without time zone NOT NULL +); + + +-- +-- Name: projects_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.projects_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: projects_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.projects_id_seq OWNED BY public.projects.id; + + +-- +-- Name: userfollows; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.userfollows ( + follower_id integer NOT NULL, + followed_id integer NOT NULL +); + + +-- +-- Name: userlogininfo; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.userlogininfo ( + username character varying(50) NOT NULL, + password_hash character varying(255) NOT NULL +); + + +-- +-- Name: userpushtokens; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.userpushtokens ( + id integer NOT NULL, + user_id integer NOT NULL, + token text NOT NULL, + platform text, + created_at timestamp without time zone NOT NULL +); + + +-- +-- Name: userpushtokens_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.userpushtokens_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: userpushtokens_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.userpushtokens_id_seq OWNED BY public.userpushtokens.id; + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + username character varying(50) NOT NULL, + picture text, + bio text, + links json, + settings json, + creation_date timestamp without time zone NOT NULL +); + + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: comments id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass); + + +-- +-- Name: directmessages id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directmessages ALTER COLUMN id SET DEFAULT nextval('public.directmessages_id_seq'::regclass); + + +-- +-- Name: notifications id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notifications ALTER COLUMN id SET DEFAULT nextval('public.notifications_id_seq'::regclass); + + +-- +-- Name: posts id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.posts ALTER COLUMN id SET DEFAULT nextval('public.posts_id_seq'::regclass); + + +-- +-- Name: projects id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects ALTER COLUMN id SET DEFAULT nextval('public.projects_id_seq'::regclass); + + +-- +-- Name: userpushtokens id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userpushtokens ALTER COLUMN id SET DEFAULT nextval('public.userpushtokens_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Data for Name: commentlikes; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.commentlikes (user_id, comment_id) FROM stdin; +2 1 +12 3 +2 3 +12 5 +\. + + +-- +-- Data for Name: comments; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.comments (id, parent_comment_id, user_id, content, media, likes, creation_date) FROM stdin; +1 \N 2 ***Welcome!*** [] 1 2026-02-22 06:13:57.140633 +2 \N 2 hey [] 0 2026-02-24 06:02:43.563579 +8 \N 2 Hopefully. [] 0 2026-02-26 03:59:47.343148 +4 \N 2 How nice [] 0 2026-02-25 12:12:46.208948 +9 \N 2 LetΓÇÖs go it is out! [] 0 2026-02-26 07:42:09.638077 +3 \N 12 Very cool [] 2 2026-02-25 04:22:50.328314 +6 \N 12 # **_I guess iΓÇÖm just better_** [] 0 2026-02-25 14:37:45.700912 +5 \N 2 Devbits doesnΓÇÖt have this yet :( [] 1 2026-02-25 12:14:33.85307 +7 \N 2 > [!IMPORTANT]\n>***WOW*** [] 0 2026-02-25 15:29:55.813755 +\. + + +-- +-- Data for Name: directmessages; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directmessages (id, sender_id, recipient_id, content, creation_date, read_at) FROM stdin; +1 3 2 hey 2026-02-22 08:10:56.112458 \N +2 3 2 how are you 2026-02-22 08:11:00.098256 \N +3 3 2 whay are you doing 2026-02-22 08:11:14.220429 \N +4 3 2 hey 2026-02-22 08:12:51.507236 \N +5 3 2 you around 2026-02-22 08:12:57.274295 \N +6 3 2 what are you soing 2026-02-22 08:13:06.610077 \N +7 2 2 hey bro 2026-02-22 08:15:54.28045 \N +8 2 3 hi 2026-02-22 08:32:48.431032 \N +9 2 3 how are you 2026-02-22 08:32:58.835777 \N +10 2 3 send notify 2026-02-22 08:33:08.195841 \N +11 3 2 what do you want 2026-02-22 10:42:29.699899 \N +12 2 4 Hello did it work? 2026-02-22 23:45:51.541105 \N +13 2 7 hey 2026-02-24 03:22:44.030372 \N +14 2 7 what is up 2026-02-24 03:22:49.169012 \N +15 7 2 what the fuck 2026-02-24 03:22:50.547058 \N +16 7 2 you made a chat system 2026-02-24 03:22:56.130422 \N +17 7 2 you bastard 2026-02-24 03:23:00.33064 \N +18 2 7 yes 2026-02-24 03:23:15.673677 \N +19 2 7 im sorry 2026-02-24 03:23:21.155377 \N +20 7 2 wowowow 2026-02-24 03:23:22.010633 \N +21 7 2 crazy work 2026-02-24 03:23:31.057022 \N +22 2 7 why do notificstions still pop up 2026-02-24 03:23:36.229472 \N +23 7 2 ok tell me here rn 2026-02-24 03:23:37.876188 \N +24 7 2 how much of this is copilot 2026-02-24 03:23:44.505446 \N +25 7 2 i can report it lol 2026-02-24 03:23:52.206225 \N +26 2 7 i have never talked to someone before and 30% co pilot 20% comunity code and 50% staying up and being late to work 2026-02-24 03:24:17.708223 \N +27 2 7 why is chat actually working 2026-02-24 03:24:31.642664 \N +28 7 2 well its worth it 2026-02-24 03:24:32.431845 \N +29 7 2 you're crazyyyyy 2026-02-24 03:24:38.65296 \N +30 2 7 i have only done this with myself lol 2026-02-24 03:24:53.316298 \N +31 7 2 and ur crazy 2026-02-24 03:25:17.546545 \N +32 2 7 fav feature is theme changing btw 2026-02-24 03:25:28.805951 \N +33 2 7 check her out 2026-02-24 03:25:32.765308 \N +34 7 2 ok ok 2026-02-24 03:25:38.054775 \N +35 7 2 are these messages persisted 2026-02-24 03:25:45.046406 \N +36 2 7 also markdown. speant the longest on thsy 2026-02-24 03:25:55.731376 \N +37 2 7 and yes 2026-02-24 03:25:59.227035 \N +38 7 2 wowowow 2026-02-24 03:26:05.472512 \N +39 2 7 dont add a photo to a stream byte or comment. might crash app idk. it is touch and go 2026-02-24 03:27:10.431592 \N +40 7 2 lmao that can wait for later 2026-02-24 03:27:21.06114 \N +41 2 7 want me to blow your mind? im on the expo go app rn on iphone. i synced it up to the backend 2026-02-24 03:27:50.650943 \N +42 7 2 ur crazy 2026-02-24 03:27:59.257013 \N +43 7 2 insane 2026-02-24 03:28:03.330793 \N +44 2 7 shit is nice for testing 2026-02-24 03:28:09.658743 \N +45 7 2 btw i have no way to get to this chat (i think)? from the homepage 2026-02-24 03:28:17.691824 \N +46 2 7 home page, terminal, then type chat elifouts 2026-02-24 03:28:36.899297 \N +47 7 2 ah 2026-02-24 03:28:42.509447 \N +48 7 2 that is cool but semi-non-trivial 2026-02-24 03:28:56.757885 \N +49 2 7 i know 2026-02-24 03:29:06.20785 \N +50 7 2 also is the font huge for you or no lol 2026-02-24 03:29:19.410148 \N +51 2 7 i know may be too incinvenient. also notifications are like out of control 2026-02-24 03:29:45.167748 \N +52 7 2 lol 2026-02-24 03:29:54.158539 \N +53 7 2 test 2026-02-24 03:30:46.299876 \N +54 2 7 test 2026-02-24 03:30:57.988357 \N +55 7 2 weird, if i hit enter on my keyboard, it will not focus the box, but if i hit the key on the side here it does 2026-02-24 03:31:15.88166 \N +56 2 7 hmmm sorry i didnt design it for comp it kinda sucks. 2026-02-24 03:32:43.538126 \N +57 7 2 i added an issue, no biggie 2026-02-24 03:34:20.195667 \N +58 7 2 i also found that i was completely on the wrong page, i couldnt find the terminal button lol 2026-02-24 03:34:43.245808 \N +59 2 7 ahhh 2026-02-24 03:35:01.86916 \N +60 7 2 im gonna add issues, up to you if you care enough about them 2026-02-24 03:35:18.782106 \N +61 7 2 as the designer lol 2026-02-24 03:35:24.140855 \N +62 2 7 ill take a look at some point 2026-02-24 03:35:33.268409 \N +63 2 7 okk 2026-02-24 03:35:39.277043 \N +64 7 2 notifications are so boinked youre so right 2026-02-24 03:37:22.96889 \N +65 7 2 i keep seeing notification pins but there is nothing there lol 2026-02-24 03:37:38.970374 \N +66 7 2 known issue? or should i add one on github 2026-02-24 03:37:46.911071 \N +67 2 7 yeaa too many and page doesnt show then live 2026-02-24 03:37:49.443457 \N +68 2 7 add it 2026-02-24 03:37:58.516022 \N +69 7 2 hear 2026-02-24 03:38:02.933545 \N +70 7 2 heard* 2026-02-24 03:38:07.249757 \N +71 2 7 if you want you coukd also add uploads not working. other than profile pic 2026-02-24 03:38:26.578174 \N +72 7 2 yaya 2026-02-24 03:38:34.846156 \N +73 2 7 if you dont see the image in that one md test page is cause it isnt a real link. i just copy and pasted for the readme translate test site i found 2026-02-24 03:39:19.214642 \N +74 7 2 lol ok 2026-02-24 03:39:41.955142 \N +75 2 7 also if you type exit in this chat without / it still extis 2026-02-24 03:40:46.317609 \N +76 2 7 :( 2026-02-24 03:40:53.836938 \N +77 7 2 wdym what still exists 2026-02-24 03:41:02.880823 \N +78 7 2 also i dig the aurora theme :) 2026-02-24 03:41:11.905893 \N +79 2 7 exit and yea i made those presets from my favs i was playing around with 2026-02-24 03:41:50.63279 \N +80 7 2 oh ur saying that doesnt clear the messaes? 2026-02-24 03:42:27.342191 \N +81 7 2 messages? 2026-02-24 03:42:32.444241 \N +82 2 7 im saying you can just type "exit" and it will leave chat. i think you should have to type /exit to get back to term commands 2026-02-24 03:43:16.53486 \N +83 2 7 and i made it that way 2026-02-24 03:43:26.668807 \N +84 7 2 ohhhhhh 2026-02-24 03:43:57.055817 \N +85 2 7 but i dont i added an exeption 2026-02-24 03:43:59.455814 \N +86 2 7 think ^ 2026-02-24 03:44:06.178758 \N +87 7 2 you did, just tested, will report 2026-02-24 03:44:23.488765 \N +88 2 7 go to settings > help and nav > md so see how im rendering markdown. i have a lot of issues with rendering md and 2026-02-24 03:45:50.29706 \N +89 2 7 yea 2026-02-24 03:45:53.566217 \N +90 7 2 ok will look 2026-02-24 03:46:05.113246 \N +91 7 2 i dont even see a help option in the settings lol 2026-02-24 03:49:12.87261 \N +92 7 2 nvmd im dumb 2026-02-24 03:49:49.296126 \N +93 7 2 see the md rendering has some issues but it looks very good overall 2026-02-24 03:51:32.829629 \N +94 7 2 i will add an issue though 2026-02-24 03:51:41.435514 \N +95 7 2 but dude seriously awesome job, i wouldnt have ever imagined it this good 2026-02-24 03:54:01.034306 \N +96 2 7 ur vision lol im just trying to carry it out :) 2026-02-24 04:55:40.792781 \N +97 2 5 Hello 2026-02-24 14:26:04.784453 \N +98 5 8 penis 2026-02-24 14:28:01.824466 \N +99 5 8 feedback 2026-02-24 14:28:10.055026 \N +100 2 11 hello 2026-02-25 00:40:33.340134 \N +101 11 2 heyo 2026-02-25 00:41:12.631377 \N +102 2 12 Heyyyyyyyyy 2026-02-25 03:01:09.747674 \N +103 2 12 hi 2026-02-25 03:01:12.357361 \N +104 2 12 how are you 2026-02-25 03:01:18.800528 \N +105 12 2 heyyyy 2026-02-25 04:08:15.61367 \N +106 2 3 test chat 2026-02-26 04:11:31.118818 \N +107 2 3 why so much space 2026-02-26 04:11:41.98759 \N +108 3 2 hey brother 2026-02-26 04:26:41.296631 \N +109 3 2 how are yo u 2026-02-26 04:26:53.007846 \N +110 3 2 you ok? 2026-02-26 04:26:58.146489 \N +111 2 3 im good 2026-02-26 04:27:26.372042 \N +\. + + +-- +-- Data for Name: notifications; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.notifications (id, user_id, actor_id, type, post_id, project_id, comment_id, created_at, read_at) FROM stdin; +22 4 2 follow_user \N \N \N 2026-02-22 23:37:12.499656 \N +23 4 2 direct_message \N \N \N 2026-02-22 23:45:51.549256 \N +25 4 5 follow_user \N \N \N 2026-02-23 14:05:31.617678 \N +113 7 2 direct_message \N \N \N 2026-02-24 04:55:40.797338 \N +114 5 2 direct_message \N \N \N 2026-02-24 14:26:04.787561 \N +115 8 5 direct_message \N \N \N 2026-02-24 14:28:01.829198 \N +116 8 5 direct_message \N \N \N 2026-02-24 14:28:10.060184 \N +117 5 8 builder_added \N 4 \N 2026-02-24 14:28:48.935545 \N +121 5 2 follow_user \N \N \N 2026-02-24 14:59:38.597783 \N +122 8 2 follow_user \N \N \N 2026-02-24 14:59:42.545036 \N +123 8 2 follow_user \N \N \N 2026-02-24 16:42:09.933943 \N +124 8 2 follow_user \N \N \N 2026-02-24 16:42:14.999132 \N +125 8 2 follow_user \N \N \N 2026-02-24 16:42:17.831356 \N +126 8 2 follow_user \N \N \N 2026-02-24 16:42:20.600543 \N +128 9 2 follow_user \N \N \N 2026-02-24 18:59:50.771187 \N +150 13 2 follow_user \N \N \N 2026-02-26 03:56:22.471112 \N +151 8 2 save_project \N 4 \N 2026-02-26 03:58:22.992793 \N +152 9 2 save_project \N 6 \N 2026-02-26 03:58:25.521494 \N +153 12 2 save_project \N 8 \N 2026-02-26 03:58:27.247643 \N +154 12 2 save_post 5 \N \N 2026-02-26 03:58:46.882382 \N +138 7 12 follow_user \N \N \N 2026-02-25 04:29:35.707022 \N +161 12 3 save_project \N 8 \N 2026-02-26 04:32:34.671987 \N +141 12 2 comment_post 5 \N 4 2026-02-25 12:12:46.213931 \N +142 12 2 comment_post 5 \N 5 2026-02-25 12:14:33.856195 \N +162 9 3 save_project \N 6 \N 2026-02-26 04:32:36.360604 \N +163 8 3 save_project \N 4 \N 2026-02-26 04:32:37.793972 \N +146 12 2 comment_post 5 \N 7 2026-02-25 15:29:55.81608 \N +165 12 3 save_post 5 \N \N 2026-02-26 04:32:46.91211 \N +\. + + +-- +-- Data for Name: postcomments; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.postcomments (post_id, comment_id, user_id) FROM stdin; +1 1 2 +1 2 2 +5 3 12 +5 4 2 +5 5 2 +5 6 12 +5 7 2 +8 8 2 +8 9 2 +\. + + +-- +-- Data for Name: postlikes; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.postlikes (user_id, post_id) FROM stdin; +2 1 +2 3 +2 2 +12 5 +9 7 +2 7 +10 7 +10 6 +10 5 +2 6 +2 5 +10 3 +10 2 +10 1 +3 8 +3 7 +3 6 +3 5 +3 3 +3 2 +3 1 +2 8 +\. + + +-- +-- Data for Name: posts; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.posts (id, content, media, project_id, creation_date, user_id, likes) FROM stdin; +7 Add a *Comment* if you have an issue or go to the link below to add an issue\n\n[Devbits Issues on GitHub](github.com/devbits-go/DevBits/issues)\n\nIf you have any other feedback please feel free to message me!\n\n-Eli [] 1 2026-02-25 20:39:50.441679 2 4 +6 Try markdown features now!\n\n> *Go to:*\n> `settings > help & Nav > MD help`\n> ___\n> - *For more info on Md checkout `devbits.md`* [] 1 2026-02-25 16:52:42.58316 2 3 +5 Got OAuth working! [] 8 2026-02-25 04:07:44.256025 12 4 +3 Go to `settings > Help` for documentation on site MD rendering [] 2 2026-02-22 23:54:39.762739 2 3 +2 > [!Tip] Set up your Bio!! [] 1 2026-02-22 23:51:46.051741 2 3 +1 Welcome to Devbits!\n>- To start - create your first `Stream`\n>- Then add a `Byte`!\n---\n>- *Any user can comment or chat with other users!*\n\n-- I hope you enjoy! :) [] 1 2026-02-22 06:11:36.478826 2 4 +9 > [!CAUTION]\n> please do not upload media to stream byte or comment. It will break your app. [] 1 2026-02-26 07:39:18.693523 2 0 +8 > [!Note]Update 16 incoming!\n___\n\n* feat: update privacy policy and topbar design\n\n- Changed the title of the privacy policy page to "Become a Tester".\n- Redesigned the topbar layout using CSS Grid for better alignment.\n- Added a "Become a Tester" call-to-action button in the topbar.\n- Introduced a new shared topbar component with associated CSS and JS files.\n- Updated the tester application page to reflect the new topbar design.\n- Enhanced API request handling with improved timeout settings and error management.\n- Removed the reset project script as it is no longer needed.\n- Added scripts for running the frontend and enabling Git hooks.\n- Created a PowerShell script for formatting videos for the App Store.\n\n* Removed github hookss not needed. screw you co-piolot\n\n* Initial plan\n\n* Restore jest tests without --watchAll to prevent connectivity slowdown\n\nCo-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>\n\n* Initial plan\n\n* Fix HTTP/2 directive in nginx.conf to use listen 443 ssl http2\n\nCo-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>\n\n* Initial plan\n\n* Initial plan\n\n* Initial plan\n\n* Use FileSystem.EncodingType.Base64 instead of "base64" as any\n\nCo-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>\n\n* Fix PowerShell syntax errors in FormatForAppStore.ps1\n\nCo-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>\n\n* Fix backend compatibility issues: Links type, NOW(), GREATEST, concurrent cursors, logger init\n\nCo-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>\n\n* Fixed http2\n\n* Fix all backend tests: update expected values, add empty body skip, fix dual-cursor bug and GREATEST()\n\nCo-authored-by: elifouts <116454864+elifouts@users.noreply.github.com>\n\n* screw this. im recreating it.\n\n* Backup DB\n\n* Fixed Developement frontend connection and also fixed post likes to match the same method used for streams.\n\n* update icons\n\n---------\n\nCo-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> [] 1 2026-02-26 03:59:04.048423 2 2 +\. + + +-- +-- Data for Name: postsaves; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.postsaves (user_id, post_id) FROM stdin; +2 2 +2 5 +3 1 +3 5 +\. + + +-- +-- Data for Name: projectbuilders; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.projectbuilders (project_id, user_id) FROM stdin; +1 3 +\. + + +-- +-- Data for Name: projectcomments; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.projectcomments (project_id, comment_id, user_id) FROM stdin; +\. + + +-- +-- Data for Name: projectfollows; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.projectfollows (user_id, project_id) FROM stdin; +7 1 +2 4 +12 8 +2 6 +2 8 +3 8 +3 6 +3 4 +\. + + +-- +-- Data for Name: projectlikes; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.projectlikes (user_id, project_id) FROM stdin; +7 2 +7 1 +2 2 +9 8 +8 4 +2 6 +2 4 +12 8 +12 1 +10 8 +10 2 +10 1 +10 4 +10 6 +2 1 +3 2 +3 4 +3 6 +3 8 +3 1 +2 8 +\. + + +-- +-- Data for Name: projects; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.projects (id, name, description, about_md, status, likes, links, tags, media, owner, creation_date) FROM stdin; +6 i found issue Issue when i try to add link to my profile my keyboard covers the box and i cannot see what i typing to thpost i cant see what im typing on this as well keyboard covers it also caps are being weird my image wont upload and after i post it being glitchy 0 3 [] [] ["https://devbits.ddns.net/uploads/u9_912c35d6bb863bcf54d1b5c7.png"] 9 2026-02-24 17:50:48.488426 +1 Devbits `DevBits`\n___\n> An app for *Devs* and people with cool projects\n*Or if you are just taking a browse that is cool too* ![DevBits](https://github.com/devbits-go/.github/blob/main/profile/svg/DevBits.svg)\n\n![Purpose](https://github.com/devbits-go/.github/blob/main/profile/svg/Purpose.svg)\n\n> Welcome To *DevBits*, The App for Developers and people in tech.\n\n> Looking to create a healthy social environment for people to share their ideas.\n\n![Members](https://github.com/devbits-go/.github/blob/main/profile/svg/Members.svg)\n\n![Cards](https://github.com/devbits-go/.github/blob/main/profile/svg/Cards.svg)\n\n![ArchBTW](https://github.com/devbits-go/.github/blob/main/profile/svg/ArchBTW.svg) 0 6 ["devbits.ddns.net/tester-application"] ["documentation"] [] 2 2026-02-22 05:56:36.110313 +8 iNQueue Whats next iN Queue? Find out by bringing together all of your favorite media watches in one place. NQ aims to develop a cross-platform media recommendation system that unifies user preferences across movies, shows, games, books, and more to deliver personalized "next in queue" suggestions. By aggregating and restructuring data from multiple APIs, the system will create a graph of your media consumption history. 0 5 ["https://github.com/grillinr/nq"] ["go","react native"] [] 12 2026-02-25 04:07:07.834917 +2 Devbits.md A place to show off MD support # Markdown Syntax Guide\n\nA complete reference for Markdown formatting with interactive examples.\n\n---\n\n## Headers\n\n```\n# This is a Heading h1\n## This is a Heading h2\n### This is a Heading h3\n#### This is a Heading h4\n##### This is a Heading h5\n###### This is a Heading h6\n```\n\n# This is a Heading h1\n## This is a Heading h2\n### This is a Heading h3\n#### This is a Heading h4\n##### This is a Heading h5\n###### This is a Heading h6\n\n---\n\n## Emphasis\n\n*This text will be italic* \n_This will also be italic_\n\n**This text will be bold** \n__This will also be bold__\n\n_You **can** combine them_\n\n~~This text is strikethrough~~\n\n---\n\n## Lists\n\n### Unordered Lists\n\n* Item 1\n* Item 2\n* Item 2a\n* Item 2b\n * Item 3a\n * Item 3b\n\n### Ordered Lists\n\n1. Item 1\n2. Item 2\n3. Item 3\n 1. Item 3a\n 2. Item 3b\n\n### Task Lists (Checkboxes)\n\n- [x] Completed task\n- [x] Another completed task\n- [ ] Incomplete task\n- [ ] Another incomplete task\n - [x] Subtask completed\n - [ ] Subtask incomplete\n\n---\n\n## Images\n\n![This is an alt text.](/image/Markdown-mark.svg "This is a sample image.")\n\n---\n\n## Links\n\nYou may be using [Markdown Live Preview](https://markdownlivepreview.com/).\n\n[Link with title](https://example.com "This is a title")\n\n\n\n---\n\n## Blockquotes\n\n> Markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by John Gruber with Aaron Swartz.\n\n> Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor.\n\n> **Note:** You can use other markdown syntax within blockquotes.\n\n---\n\n## Tables\n\n| Left columns | Center columns | Right columns |\n| ------------- |:-------------:|:-----------:|\n| left foo | center foo | right foo |\n| left bar | center bar | right bar |\n| left baz | center baz | right baz |\n\n---\n\n## Code\n\n### Blocks of Code\n\n```javascript\nlet message = 'Hello world';\nalert(message);\n```\n\n```python\ndef hello_world():\n print("Hello, World!")\n return True\n```\n\n```html\n
\n

HTML Example

\n
\n```\n\n### Inline Code\n\nThis web site is using `markedjs/marked` for rendering markdown.\n\nUse the `console.log()` function to debug your code.\n\n---\n\n## Horizontal Rules\n\n---\n\n***\n\n___\n\n---\n\n## Line Breaks\n\nLine 1 \nLine 2 (with two spaces before)\n\nLine 1\n\nLine 2 (with blank line between)\n\n---\n\n
\n???? Advanced Features (Click to expand)\n\n### Definition Lists\n\nTerm 1\n: Definition 1\n\nTerm 2\n: Definition 2a\n: Definition 2b\n\n### Footnotes\n\nThis is a statement[^1] with a footnote.\n\n[^1]: This is the footnote content.\n\n### Superscript & Subscript\n\nH~2~O\n\nE=mc^2^\n\n
\n\n---\n\n
\n??? Markdown Best Practices (Click to expand)\n\n- Use consistent heading hierarchy\n- Add blank lines between sections\n- Use code fences for better readability\n- Link to external resources when relevant\n- Keep lists simple and organized\n- Use emphasis sparingly for impact\n- Always provide alt text for images\n- Test your markdown before sharing\n\n
\n\n---\n\n
\n???? Tips & Tricks (Click to expand)\n\n**Combining Styles:**\n- ***Bold and Italic*** combined\n- **Bold with `code`**\n- > Blockquote with **bold**\n\n**Escaping Characters:**\n\\*This will not be italic\\*\n\n\\[This is not a link\\]\n\n**HTML Embedding:**\nYou can embed raw HTML for more control over formatting.\n\n
\n\n---\n\n## Quick Reference Checklist\n\n- [x] Headers learned\n- [x] Emphasis mastered\n- [x] Lists understood\n- [x] Images added\n- [x] Links created\n- [x] Blockquotes used\n- [x] Tables created\n- [x] Code blocks formatted\n- [ ] Advanced features explored\n- [ ] Ready to write markdown!\n\n---\n\n**Last Updated:** February 20, 2026 \n**Format:** GitHub Flavored Markdown (GFM) 0 5 [] ["markdown","documentation","devbits"] [] 2 2026-02-22 06:12:55.58306 +4 Ball growing machine machine that grows my balls Currently it does not grow my balls. Further testing required 0 4 [] ["Balls","grow","big"] [] 8 2026-02-24 14:27:57.974905 +\. + + +-- +-- Data for Name: userfollows; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.userfollows (follower_id, followed_id) FROM stdin; +2 4 +5 4 +2 7 +7 2 +2 5 +2 8 +2 3 +2 9 +2 12 +12 3 +12 7 +2 13 +\. + + +-- +-- Data for Name: userlogininfo; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.userlogininfo (username, password_hash) FROM stdin; +elifouts $2a$10$eMXYEH8bfTiaam5F/fMKxeR/SM2P8GZCaB8IGeqUoleSqSLrHKjqa +Whiteshadow73 $2a$10$uMix6AqdN7p1BhvfzV0rS.k3cyUU1QumwUB1wvgxCx72xwhhxeF5O +drtimfouts $2a$10$EwIsRelUKZkFYvHxK.ZYYu4ib4KtaCsSlfn5BoYa7tzhS2h.lsgkW +bonerbob $2a$10$XN1FnCZSWkXpASpFbNc7/eqn88x2.MPziOfOqZBw5m9ZmaVhAD5D2 +DerekCornDev $2a$10$RSpHVp.8.htmbvLfhPd3P.cEetp9dMeUbjhlrzlNJAfHw34vfExZ2 +Reginald $2a$10$EEWsKL8waqCTNHvHdpBgDuF.eEBBuBafPZGkE5dCXbgjh2bne0QYm +kylie $2a$10$9qt8DwUNFH64REPD7/IJ1uL8I.28WCQArmnwn8I.DfFaXW7J/RZOC +blackshadow73 $2a$10$viWbouQxrYtejFRFCaN3deAYqeAHhdz.qVY4.sICEHix0AeCFElYi +DerekCorniello $2a$10$mDxxy3R/iYekVnrLSPGIG..aMM4oxk3jGa.J1grKbP5QchlCbpRWq +nrgrill2003@gmail.com $2a$10$gq8FGjdTtG5B2HKOT6HG9ufWHycjniQSb.LZc.F2nE/8rf6jBY356 +test $2a$10$162xpBa4Z.Go57g2ddm/A.Y7yIVdbRClGK1ZkODXaYZQBlbguUOze +\. + + +-- +-- Data for Name: userpushtokens; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.userpushtokens (id, user_id, token, platform, created_at) FROM stdin; +38 11 ExponentPushToken[ajsresPbvPduk3-PPpLAqX] ios 2026-02-25 00:36:19.420879 +39 12 ExponentPushToken[UO1B5eOKkqI6iPAUj9rX2t] ios 2026-02-25 00:39:12.85981 +1 2 ExponentPushToken[0swYQCMnwgv2U7vm3X51pW] ios 2026-02-24 05:34:44.586288 +\. + + +-- +-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.users (id, username, picture, bio, links, settings, creation_date) FROM stdin; +4 drtimfouts /uploads/bfbe036460f2602afb275915.htm Dev dad {} {"accentColor":"","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"monoAccent","zenMode":false} 2026-02-22 19:11:39.453595 +7 DerekCornDev {} {"accentColor":"#4A8DFF","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"monoAccent","zenMode":false} 2026-02-24 03:19:40.762558 +9 kylie /uploads/u9_c1724846b8f93da55f421234.jpg Hi {"link_0":"Kylie-Merz"} {"accentColor":"","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"monoAccent","zenMode":false} 2026-02-24 17:46:13.221158 +8 Reginald {} {"accentColor":"","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"monoAccent","zenMode":false} 2026-02-24 14:25:18.538131 +5 bonerbob /uploads/u5_d8d0b793924c9834ebcabf88.jpg {} {"accentColor":"","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"monoAccent","zenMode":false} 2026-02-23 14:05:00.130288 +10 blackshadow73 {} {} 2026-02-24 23:57:35.326365 +11 DerekCorniello {} {"accentColor":"","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"monoAccent","zenMode":false} 2026-02-25 00:36:17.630979 +13 test [] {"accentColor":"","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"monoAccent","zenMode":false} 2026-02-26 03:28:05 +12 nrgrill2003@gmail.com /uploads/u12_3ccfa4701ae0e035bc5d3c07.jpg 10x dev {"link_0":"nathangrilliot.com","link_1":"github.com/grillinr","link_2":"nathangrilliot.com"} {"accentColor":"#00F329","backgroundRefreshEnabled":true,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"promptScheme","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":0.55,"visualizationMode":"classic","zenMode":false} 2026-02-25 00:39:09.951406 +3 Whiteshadow73 /uploads/u3_8e213dd74950c62c9df55994.jpg I ranked Plat in Valorant.\n\n~~Flexing~~ [] {"accentColor":"#4A8DFF","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":60000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"typewriter","visualizationIntensity":1,"visualizationMode":"cinematic","zenMode":false} 2026-02-22 08:06:14.433232 +2 elifouts /uploads/u2_182a392d5837c19a1ad894b5.jpg > [!Note] Hello World!\n\nI am a 10x developer for `devbits` [] {"accentColor":"#FFFFFF","backgroundRefreshEnabled":false,"compactMode":false,"hasSeenWelcomeTour":true,"imageRevealEffect":"smooth","linkOpenMode":"asTyped","pageTransitionEffect":"fade","refreshIntervalMs":120000,"rgbShiftEnabled":false,"rgbShiftSpeedMs":3200,"rgbShiftStep":0.85,"rgbShiftTheme":"rainbow","rgbShiftTickMs":44,"rgbUserTheme1":["#00F329","#06B6D4","#A855F7"],"rgbUserTheme2":["#FF6B6B","#F59E0B","#FDE047"],"textRenderEffect":"smooth","visualizationIntensity":1,"visualizationMode":"monoAccent","zenMode":false} 2026-02-22 05:28:00.259995 +\. + + +-- +-- Name: comments_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.comments_id_seq', 9, true); + + +-- +-- Name: directmessages_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directmessages_id_seq', 111, true); + + +-- +-- Name: notifications_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.notifications_id_seq', 165, true); + + +-- +-- Name: posts_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.posts_id_seq', 9, true); + + +-- +-- Name: projects_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.projects_id_seq', 8, true); + + +-- +-- Name: userpushtokens_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.userpushtokens_id_seq', 117, true); + + +-- +-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.users_id_seq', 13, true); + + +-- +-- Name: commentlikes commentlikes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.commentlikes + ADD CONSTRAINT commentlikes_pkey PRIMARY KEY (user_id, comment_id); + + +-- +-- Name: comments comments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments + ADD CONSTRAINT comments_pkey PRIMARY KEY (id); + + +-- +-- Name: directmessages directmessages_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directmessages + ADD CONSTRAINT directmessages_pkey PRIMARY KEY (id); + + +-- +-- Name: notifications notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notifications + ADD CONSTRAINT notifications_pkey PRIMARY KEY (id); + + +-- +-- Name: postcomments postcomments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postcomments + ADD CONSTRAINT postcomments_pkey PRIMARY KEY (post_id, comment_id); + + +-- +-- Name: postlikes postlikes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postlikes + ADD CONSTRAINT postlikes_pkey PRIMARY KEY (user_id, post_id); + + +-- +-- Name: posts posts_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT posts_pkey PRIMARY KEY (id); + + +-- +-- Name: postsaves postsaves_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postsaves + ADD CONSTRAINT postsaves_pkey PRIMARY KEY (user_id, post_id); + + +-- +-- Name: projectbuilders projectbuilders_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectbuilders + ADD CONSTRAINT projectbuilders_pkey PRIMARY KEY (project_id, user_id); + + +-- +-- Name: projectcomments projectcomments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectcomments + ADD CONSTRAINT projectcomments_pkey PRIMARY KEY (project_id, comment_id); + + +-- +-- Name: projectfollows projectfollows_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectfollows + ADD CONSTRAINT projectfollows_pkey PRIMARY KEY (user_id, project_id); + + +-- +-- Name: projectlikes projectlikes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectlikes + ADD CONSTRAINT projectlikes_pkey PRIMARY KEY (user_id, project_id); + + +-- +-- Name: projects projects_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects + ADD CONSTRAINT projects_pkey PRIMARY KEY (id); + + +-- +-- Name: userfollows userfollows_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userfollows + ADD CONSTRAINT userfollows_pkey PRIMARY KEY (follower_id, followed_id); + + +-- +-- Name: userlogininfo userlogininfo_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userlogininfo + ADD CONSTRAINT userlogininfo_pkey PRIMARY KEY (username); + + +-- +-- Name: userpushtokens userpushtokens_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userpushtokens + ADD CONSTRAINT userpushtokens_pkey PRIMARY KEY (id); + + +-- +-- Name: userpushtokens userpushtokens_token_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userpushtokens + ADD CONSTRAINT userpushtokens_token_key UNIQUE (token); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: users users_username_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_username_key UNIQUE (username); + + +-- +-- Name: idx_commentlikes_comment_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_commentlikes_comment_id ON public.commentlikes USING btree (comment_id); + + +-- +-- Name: idx_comments_creation_date; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_comments_creation_date ON public.comments USING btree (creation_date DESC); + + +-- +-- Name: idx_comments_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_comments_user_id ON public.comments USING btree (user_id); + + +-- +-- Name: idx_directmessages_created_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_directmessages_created_at ON public.directmessages USING btree (creation_date DESC, id DESC); + + +-- +-- Name: idx_directmessages_recipient_created; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_directmessages_recipient_created ON public.directmessages USING btree (recipient_id, creation_date DESC); + + +-- +-- Name: idx_directmessages_sender_recipient; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_directmessages_sender_recipient ON public.directmessages USING btree (sender_id, recipient_id); + + +-- +-- Name: idx_notifications_user_created_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_notifications_user_created_at ON public.notifications USING btree (user_id, created_at DESC); + + +-- +-- Name: idx_notifications_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_notifications_user_id ON public.notifications USING btree (user_id); + + +-- +-- Name: idx_notifications_user_read_at; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_notifications_user_read_at ON public.notifications USING btree (user_id, read_at); + + +-- +-- Name: idx_postlikes_post_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_postlikes_post_id ON public.postlikes USING btree (post_id); + + +-- +-- Name: idx_posts_creation_date; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_posts_creation_date ON public.posts USING btree (creation_date DESC); + + +-- +-- Name: idx_posts_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_posts_project_id ON public.posts USING btree (project_id); + + +-- +-- Name: idx_posts_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_posts_user_id ON public.posts USING btree (user_id); + + +-- +-- Name: idx_postsaves_post_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_postsaves_post_id ON public.postsaves USING btree (post_id); + + +-- +-- Name: idx_postsaves_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_postsaves_user_id ON public.postsaves USING btree (user_id); + + +-- +-- Name: idx_projectbuilders_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_projectbuilders_user_id ON public.projectbuilders USING btree (user_id); + + +-- +-- Name: idx_projectfollows_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_projectfollows_project_id ON public.projectfollows USING btree (project_id); + + +-- +-- Name: idx_projectfollows_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_projectfollows_user_id ON public.projectfollows USING btree (user_id); + + +-- +-- Name: idx_projectlikes_project_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_projectlikes_project_id ON public.projectlikes USING btree (project_id); + + +-- +-- Name: idx_projects_creation_date; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_projects_creation_date ON public.projects USING btree (creation_date DESC); + + +-- +-- Name: idx_projects_owner; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_projects_owner ON public.projects USING btree (owner); + + +-- +-- Name: idx_userfollows_followed_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_userfollows_followed_id ON public.userfollows USING btree (followed_id); + + +-- +-- Name: idx_userfollows_follower_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_userfollows_follower_id ON public.userfollows USING btree (follower_id); + + +-- +-- Name: idx_userpushtokens_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_userpushtokens_user_id ON public.userpushtokens USING btree (user_id); + + +-- +-- Name: idx_users_username; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX idx_users_username ON public.users USING btree (username); + + +-- +-- Name: commentlikes commentlikes_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.commentlikes + ADD CONSTRAINT commentlikes_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id) ON DELETE CASCADE; + + +-- +-- Name: commentlikes commentlikes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.commentlikes + ADD CONSTRAINT commentlikes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: comments comments_parent_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments + ADD CONSTRAINT comments_parent_comment_id_fkey FOREIGN KEY (parent_comment_id) REFERENCES public.comments(id) ON DELETE CASCADE; + + +-- +-- Name: comments comments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments + ADD CONSTRAINT comments_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: directmessages directmessages_recipient_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directmessages + ADD CONSTRAINT directmessages_recipient_id_fkey FOREIGN KEY (recipient_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: directmessages directmessages_sender_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directmessages + ADD CONSTRAINT directmessages_sender_id_fkey FOREIGN KEY (sender_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: notifications notifications_actor_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notifications + ADD CONSTRAINT notifications_actor_id_fkey FOREIGN KEY (actor_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: notifications notifications_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notifications + ADD CONSTRAINT notifications_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id) ON DELETE CASCADE; + + +-- +-- Name: notifications notifications_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notifications + ADD CONSTRAINT notifications_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: notifications notifications_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notifications + ADD CONSTRAINT notifications_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: notifications notifications_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notifications + ADD CONSTRAINT notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: postcomments postcomments_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postcomments + ADD CONSTRAINT postcomments_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id) ON DELETE CASCADE; + + +-- +-- Name: postcomments postcomments_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postcomments + ADD CONSTRAINT postcomments_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: postcomments postcomments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postcomments + ADD CONSTRAINT postcomments_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: postlikes postlikes_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postlikes + ADD CONSTRAINT postlikes_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: postlikes postlikes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postlikes + ADD CONSTRAINT postlikes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: posts posts_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT posts_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: posts posts_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.posts + ADD CONSTRAINT posts_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: postsaves postsaves_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postsaves + ADD CONSTRAINT postsaves_post_id_fkey FOREIGN KEY (post_id) REFERENCES public.posts(id) ON DELETE CASCADE; + + +-- +-- Name: postsaves postsaves_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.postsaves + ADD CONSTRAINT postsaves_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: projectbuilders projectbuilders_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectbuilders + ADD CONSTRAINT projectbuilders_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: projectbuilders projectbuilders_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectbuilders + ADD CONSTRAINT projectbuilders_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: projectcomments projectcomments_comment_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectcomments + ADD CONSTRAINT projectcomments_comment_id_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id) ON DELETE CASCADE; + + +-- +-- Name: projectcomments projectcomments_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectcomments + ADD CONSTRAINT projectcomments_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: projectcomments projectcomments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectcomments + ADD CONSTRAINT projectcomments_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: projectfollows projectfollows_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectfollows + ADD CONSTRAINT projectfollows_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: projectfollows projectfollows_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectfollows + ADD CONSTRAINT projectfollows_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: projectlikes projectlikes_project_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectlikes + ADD CONSTRAINT projectlikes_project_id_fkey FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; + + +-- +-- Name: projectlikes projectlikes_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projectlikes + ADD CONSTRAINT projectlikes_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: projects projects_owner_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.projects + ADD CONSTRAINT projects_owner_fkey FOREIGN KEY (owner) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: userfollows userfollows_followed_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userfollows + ADD CONSTRAINT userfollows_followed_id_fkey FOREIGN KEY (followed_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: userfollows userfollows_follower_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userfollows + ADD CONSTRAINT userfollows_follower_id_fkey FOREIGN KEY (follower_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- Name: userpushtokens userpushtokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.userpushtokens + ADD CONSTRAINT userpushtokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE; + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict nJGpB5uSaQ5fCmjJfoGSoRiAXQCJU5J1JZgY5TlSg4ValCxlsLWEM8UKNr4haNs + diff --git a/backend/backups/db/devbits-uploads-20260226-030000.zip b/backend/backups/db/devbits-uploads-20260226-030000.zip new file mode 100644 index 0000000..c8ef963 Binary files /dev/null and b/backend/backups/db/devbits-uploads-20260226-030000.zip differ diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml new file mode 100644 index 0000000..49fc9be --- /dev/null +++ b/backend/docker-compose.dev.yml @@ -0,0 +1,36 @@ +name: devbits-dev-local + +services: + db: + image: postgres:15 + environment: + POSTGRES_DB: devbits_dev + POSTGRES_USER: devbits_dev + POSTGRES_PASSWORD: devbits_dev_password + ports: + - "${DEVBITS_DB_PORT:-5433}:5432" + volumes: + - postgres-dev-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U devbits_dev -d devbits_dev"] + interval: 2s + timeout: 3s + retries: 30 + + backend: + build: + context: . + dockerfile: Dockerfile + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: postgres://devbits_dev:devbits_dev_password@db:5432/devbits_dev?sslmode=disable + DEVBITS_DEBUG: "1" + ports: + - "${DEVBITS_BACKEND_PORT:-8080}:8080" + volumes: + - ./uploads:/root/uploads + +volumes: + postgres-dev-data: \ No newline at end of file diff --git a/backend/docker-compose.test.yml b/backend/docker-compose.test.yml new file mode 100644 index 0000000..7dee29c --- /dev/null +++ b/backend/docker-compose.test.yml @@ -0,0 +1,31 @@ +name: devbits-test-local + +services: + test-db: + image: postgres:15 + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_TEST_DB:-devbits_test} + POSTGRES_USER: ${POSTGRES_TEST_USER:-testuser} + POSTGRES_PASSWORD: ${POSTGRES_TEST_PASSWORD:-testpass123} + ports: + - "5432:5432" + volumes: + - test-postgres-data:/var/lib/postgresql/data + security_opt: + - no-new-privileges:true + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_TEST_USER:-testuser} -d $${POSTGRES_TEST_DB:-devbits_test}"] + interval: 5s + timeout: 5s + retries: 10 + networks: + - test-network + +networks: + test-network: + driver: bridge + +volumes: + test-postgres-data: + driver: local diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index bd7a876..e77af63 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -28,6 +28,8 @@ services: volumes: # Mount the uploads directory to persist data - ./uploads:/root/uploads + ports: + - "8080:8080" security_opt: - no-new-privileges:true networks: diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 4ffdf76..02cc626 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -1,8 +1,13 @@ #!/bin/sh set -e -# Ensure a DEVBITS_JWT_SECRET exists. If not set, generate a 32-byte hex secret. -if [ -z "$DEVBITS_JWT_SECRET" ]; then +# By default do NOT generate a random DEVBITS_JWT_SECRET here. Generating a +# new random secret at container start causes tokens signed by the backend to +# change on each restart which breaks client sessions during local development. +# +# If you explicitly want a random secret (production automation), set +# DEVBITS_FORCE_RANDOM_JWT_SECRET=1 and a 32-byte hex secret will be generated. +if [ -z "$DEVBITS_JWT_SECRET" ] && [ "$DEVBITS_FORCE_RANDOM_JWT_SECRET" = "1" ]; then if command -v openssl >/dev/null 2>&1; then export DEVBITS_JWT_SECRET="$(openssl rand -hex 32)" else @@ -10,6 +15,10 @@ if [ -z "$DEVBITS_JWT_SECRET" ]; then export DEVBITS_JWT_SECRET=$(head -c 32 /dev/urandom | od -An -v -tx1 | tr -d ' \n') fi echo "Generated DEVBITS_JWT_SECRET" +else + if [ -z "$DEVBITS_JWT_SECRET" ]; then + echo "DEVBITS_JWT_SECRET not set; using backend default secret for development" + fi fi # Execute the container command diff --git a/backend/nginx/nginx.conf b/backend/nginx/nginx.conf index 5e7b4e9..3ebe561 100644 --- a/backend/nginx/nginx.conf +++ b/backend/nginx/nginx.conf @@ -63,6 +63,7 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; client_max_body_size 64m; + proxy_set_header Authorization $http_authorization; server_tokens off; # HTTP server for local development and health checks diff --git a/backend/uploads/u12_fc3b18c7d1fcfd6606ba379c.jpg b/backend/uploads/u12_fc3b18c7d1fcfd6606ba379c.jpg new file mode 100644 index 0000000..0d46450 Binary files /dev/null and b/backend/uploads/u12_fc3b18c7d1fcfd6606ba379c.jpg differ diff --git a/backend/uploads/u14_f92e0ac91475079901c87220.heic b/backend/uploads/u14_f92e0ac91475079901c87220.heic new file mode 100644 index 0000000..e9c64f6 Binary files /dev/null and b/backend/uploads/u14_f92e0ac91475079901c87220.heic differ diff --git a/backend/uploads/u1_fcf5c03fd1caf85967fc72c7.png b/backend/uploads/u1_fcf5c03fd1caf85967fc72c7.png new file mode 100644 index 0000000..2816eff Binary files /dev/null and b/backend/uploads/u1_fcf5c03fd1caf85967fc72c7.png differ diff --git a/backend/uploads/u2_ae17bcd29c6f6e22e6afb0b9.jpg b/backend/uploads/u2_ae17bcd29c6f6e22e6afb0b9.jpg new file mode 100644 index 0000000..ff41674 Binary files /dev/null and b/backend/uploads/u2_ae17bcd29c6f6e22e6afb0b9.jpg differ diff --git a/backend/uploads/u2_e6541e277902a9bbfe95b888.heic b/backend/uploads/u2_e6541e277902a9bbfe95b888.heic new file mode 100644 index 0000000..89e8e65 Binary files /dev/null and b/backend/uploads/u2_e6541e277902a9bbfe95b888.heic differ diff --git a/backend/uploads/u4_2f0b37d49e48abff2e2d4bc2.jpg b/backend/uploads/u4_2f0b37d49e48abff2e2d4bc2.jpg new file mode 100644 index 0000000..bc32d39 Binary files /dev/null and b/backend/uploads/u4_2f0b37d49e48abff2e2d4bc2.jpg differ diff --git a/extracted-keys/KEYS_INDEX.md b/extracted-keys/KEYS_INDEX.md new file mode 100644 index 0000000..7304840 --- /dev/null +++ b/extracted-keys/KEYS_INDEX.md @@ -0,0 +1,9 @@ +# KEYS_INDEX + +- `devbits-play-service-account.json`: contains `private_key` field (PEM) starting at [devbits-play-service-account.json](devbits-play-service-account.json#L5). Copied files: + - `extracted-keys/devbits-play-service-account.json` (exact copy of original file) + - `extracted-keys/devbits-play-service-account_private_key.pem` (PEM extracted from the JSON `private_key` field) + +--- + +Security note: These are sensitive secrets. Consider rotating keys and removing them from the repository history if these were committed publicly. diff --git a/frontend/app.config.js b/frontend/app.config.js new file mode 100644 index 0000000..8fd1cb8 --- /dev/null +++ b/frontend/app.config.js @@ -0,0 +1,21 @@ +const appJson = require('./app.json'); + +module.exports = () => { + const base = appJson.expo || {}; + const extras = { + ...(base.extra || {}), + EXPO_PUBLIC_API_URL: process.env.EXPO_PUBLIC_API_URL || process.env.API_URL || null, + EXPO_PUBLIC_USE_LOCAL_API: + typeof process.env.EXPO_PUBLIC_USE_LOCAL_API !== 'undefined' + ? process.env.EXPO_PUBLIC_USE_LOCAL_API + : undefined, + EXPO_PUBLIC_LOCAL_API_URL: process.env.EXPO_PUBLIC_LOCAL_API_URL || undefined, + }; + + return { + expo: { + ...base, + extra: extras, + }, + }; +}; diff --git a/frontend/app/(tabs)/_layout.tsx b/frontend/app/(tabs)/_layout.tsx index 4657488..90ae5d3 100644 --- a/frontend/app/(tabs)/_layout.tsx +++ b/frontend/app/(tabs)/_layout.tsx @@ -14,6 +14,8 @@ export default function TabLayout() { return ( ( ), @@ -48,7 +51,7 @@ export default function TabLayout() { ( ), @@ -57,7 +60,7 @@ export default function TabLayout() { ( ), diff --git a/frontend/app/_layout.tsx b/frontend/app/_layout.tsx index d7bfd9e..66db708 100644 --- a/frontend/app/_layout.tsx +++ b/frontend/app/_layout.tsx @@ -29,6 +29,7 @@ import { useColorScheme } from "@/hooks/useColorScheme"; import { HyprBackdrop } from "@/components/HyprBackdrop"; import { BootScreen } from "@/components/BootScreen"; import { InAppNotificationBanner } from "@/components/InAppNotificationBanner"; +import { API_BASE_URL } from "@/services/api"; // Prevent the splash screen from auto-hiding before asset loading is complete SplashScreen.preventAutoHideAsync(); @@ -185,6 +186,22 @@ function RootLayoutNav() { } }, [fontsReady]); + useEffect(() => { + // Debug: print resolved API base URL and expo extras so we can confirm + // the JS runtime is seeing the intended backend when running via Expo. + try { + // eslint-disable-next-line no-console + console.log("Resolved API_BASE_URL:", API_BASE_URL); + // eslint-disable-next-line no-console + console.log( + "Expo extras:", + Constants.expoConfig?.extra ?? Constants.manifest?.extra ?? null, + ); + } catch (e) { + // ignore + } + }, []); + useEffect(() => { if (!fontsReady || isLoading) { return; diff --git a/frontend/contexts/AuthContext.tsx b/frontend/contexts/AuthContext.tsx index bf5d5f9..446d885 100644 --- a/frontend/contexts/AuthContext.tsx +++ b/frontend/contexts/AuthContext.tsx @@ -53,6 +53,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const loadSession = async () => { try { const storedToken = await SecureStore.getItemAsync(TOKEN_KEY); + try { + // eslint-disable-next-line no-console + console.log("AuthProvider: storedToken present=", !!storedToken); + } catch {} if (!storedToken) { await clearSession(); return; @@ -74,12 +78,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const signIn = useCallback(async (payload: AuthLoginRequest) => { const response = await loginUser(payload); + try { + // eslint-disable-next-line no-console + console.log("AuthProvider.signIn: received token=", !!response?.token); + } catch {} setAuthToken(response.token); setUser(response.user); setToken(response.token); setJustSignedUp(false); try { await SecureStore.setItemAsync(TOKEN_KEY, response.token); + try { + // eslint-disable-next-line no-console + console.log("AuthProvider.signIn: stored token in SecureStore"); + } catch {} } catch { // Continue with in-memory session even if secure store is unavailable. } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8fcd3fc..0df82d1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1555,7 +1555,7 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@egjs/hammerjs": { @@ -2383,7 +2383,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -2401,7 +2401,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -2457,17 +2457,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -2487,7 +2476,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "expect": "^29.7.0", @@ -2501,7 +2490,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" @@ -2527,22 +2516,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/@jest/globals": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -2558,7 +2536,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", @@ -2602,7 +2580,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -2614,7 +2592,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -2635,7 +2613,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2660,7 +2638,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", @@ -2675,7 +2653,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -2691,7 +2669,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -3417,128 +3395,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@testing-library/react-native": { - "version": "13.3.3", - "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz", - "integrity": "sha512-k6Mjsd9dbZgvY4Bl7P1NIpePQNi+dfYtlJ5voi9KQlynxSyQkfOgJmYGCYmw/aSgH/rUcFvG8u5gd4npzgRDyg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "jest-matcher-utils": "^30.0.5", - "picocolors": "^1.1.1", - "pretty-format": "^30.0.5", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "jest": ">=29.0.0", - "react": ">=18.2.0", - "react-native": ">=0.71", - "react-test-renderer": ">=18.2.0" - }, - "peerDependenciesMeta": { - "jest": { - "optional": true - } - } - }, - "node_modules/@testing-library/react-native/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@testing-library/react-native/node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@testing-library/react-native/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/react-native/node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@testing-library/react-native/node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@testing-library/react-native/node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@testing-library/react-native/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -5198,7 +5054,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5262,7 +5118,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -5328,7 +5184,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/cli-cursor": { @@ -5388,7 +5244,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "iojs": ">= 1.0.0", @@ -5399,7 +5255,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/color": { @@ -5617,7 +5473,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -5836,7 +5692,7 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -5969,7 +5825,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5985,7 +5841,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -6075,7 +5931,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6125,7 +5981,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -6923,7 +6779,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", @@ -6947,7 +6803,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "devOptional": true, + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -6956,7 +6812,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", @@ -8053,7 +7909,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -8361,7 +8217,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/http-errors": { @@ -8426,7 +8282,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.17.0" @@ -8526,7 +8382,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", @@ -8551,17 +8407,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8656,7 +8501,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -8851,7 +8696,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9015,7 +8860,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9158,7 +9003,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", @@ -9175,7 +9020,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9188,7 +9033,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", @@ -9203,7 +9048,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", @@ -9218,7 +9063,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", @@ -9250,7 +9095,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -9277,7 +9122,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "execa": "^5.0.0", @@ -9292,7 +9137,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -9324,7 +9169,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", @@ -9358,7 +9203,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -9404,7 +9249,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9416,7 +9261,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -9437,7 +9282,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9450,7 +9295,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9466,7 +9311,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" @@ -9479,7 +9324,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", @@ -9611,7 +9456,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", @@ -9625,7 +9470,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9675,7 +9520,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -9702,7 +9547,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -9723,7 +9568,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", @@ -9737,7 +9582,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", @@ -9770,7 +9615,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", @@ -9804,7 +9649,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9816,7 +9661,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -9837,7 +9682,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -9850,7 +9695,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", @@ -9882,7 +9727,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10071,7 +9916,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", @@ -10217,7 +10062,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -10748,7 +10593,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "semver": "^7.5.3" @@ -10764,7 +10609,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11271,23 +11116,12 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/minimatch": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", @@ -11400,7 +11234,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -11521,7 +11355,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.0.0" @@ -11725,7 +11559,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" @@ -11966,7 +11800,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -12104,7 +11938,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "find-up": "^4.0.0" @@ -12316,7 +12150,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, + "dev": true, "funding": [ { "type": "individual", @@ -12900,7 +12734,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-19.1.0.tgz", "integrity": "sha512-jXkSl3CpvPYEF+p/eGDLB4sPoDX8pKkYvRl9+rR8HxLY0X04vW7hCm1/0zHoUSjPZ3bDa+wXWNTDVIw/R8aDVw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "react-is": "^19.1.0", @@ -12910,21 +12744,6 @@ "react": "^19.1.0" } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -13099,7 +12918,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" @@ -13744,7 +13563,7 @@ "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -13909,7 +13728,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "char-regex": "^1.0.2", @@ -14047,7 +13866,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14057,31 +13876,17 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14961,7 +14766,7 @@ "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", diff --git a/scripts/imagescale.ps1 b/frontend/scripts/imagescale.ps1 similarity index 96% rename from scripts/imagescale.ps1 rename to frontend/scripts/imagescale.ps1 index bf5ab97..d13d3a4 100644 --- a/scripts/imagescale.ps1 +++ b/frontend/scripts/imagescale.ps1 @@ -1,38 +1,38 @@ -# ============================================= -# Resize images to 2064x2752 for App Store -# Preserves aspect ratio + pads if needed -# ============================================= - -# --- Edit filenames here --- -$images = @( - "input.PNG" -) - -$width = 2064 -$height = 2752 - -foreach ($file in $images) { - - if (-Not (Test-Path $file)) { - Write-Host "File not found: $file" -ForegroundColor Red - continue - } - - $baseName = [System.IO.Path]::GetFileNameWithoutExtension($file) - $extension = [System.IO.Path]::GetExtension($file) - $output = "${baseName}_AppStore${extension}" - - Write-Host "Processing $file → $output ..." -ForegroundColor Cyan - - ffmpeg -i $file ` - -vf "scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2" ` - $output - - if ($LASTEXITCODE -eq 0) { - Write-Host "Finished: $output" -ForegroundColor Green - } else { - Write-Host "Error processing $file" -ForegroundColor Red - } -} - +# ============================================= +# Resize images to 2064x2752 for App Store +# Preserves aspect ratio + pads if needed +# ============================================= + +# --- Edit filenames here --- +$images = @( + "input.PNG" +) + +$width = 2064 +$height = 2752 + +foreach ($file in $images) { + + if (-Not (Test-Path $file)) { + Write-Host "File not found: $file" -ForegroundColor Red + continue + } + + $baseName = [System.IO.Path]::GetFileNameWithoutExtension($file) + $extension = [System.IO.Path]::GetExtension($file) + $output = "${baseName}_AppStore${extension}" + + Write-Host "Processing $file → $output ..." -ForegroundColor Cyan + + ffmpeg -i $file ` + -vf "scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2" ` + $output + + if ($LASTEXITCODE -eq 0) { + Write-Host "Finished: $output" -ForegroundColor Green + } else { + Write-Host "Error processing $file" -ForegroundColor Red + } +} + Write-Host "All done!" -ForegroundColor Yellow \ No newline at end of file diff --git a/scripts/install-adb.ps1 b/frontend/scripts/install-adb.ps1 similarity index 100% rename from scripts/install-adb.ps1 rename to frontend/scripts/install-adb.ps1 diff --git a/scripts/install-adb.sh b/frontend/scripts/install-adb.sh similarity index 100% rename from scripts/install-adb.sh rename to frontend/scripts/install-adb.sh diff --git a/scripts/readme.md b/frontend/scripts/readme.md similarity index 100% rename from scripts/readme.md rename to frontend/scripts/readme.md diff --git a/scripts/screenrecord.ps1 b/frontend/scripts/screenrecord.ps1 similarity index 96% rename from scripts/screenrecord.ps1 rename to frontend/scripts/screenrecord.ps1 index 4070bf5..149abbd 100644 --- a/scripts/screenrecord.ps1 +++ b/frontend/scripts/screenrecord.ps1 @@ -1,67 +1,67 @@ -# ============================================= -# App Store App Preview Formatter -# - Trims to 30 seconds max -# - Errors if under 15 seconds -# - 886x1920 | 30fps | 10-12 Mbps -# ============================================= - -# -------- EDIT THIS -------- -$inputFile = "input.mp4" -# --------------------------- - -$width = 886 -$height = 1920 - -if (-Not (Test-Path $inputFile)) { - Write-Host "Input file not found: $inputFile" -ForegroundColor Red - exit -} - -# Get duration using ffprobe -$duration = ffprobe -v error -show_entries format=duration ` - -of default=noprint_wrappers=1:nokey=1 "$inputFile" - -$duration = [math]::Round([double]$duration,2) - -Write-Host "Video duration: $duration seconds" - -# Fail if under 15 seconds -if ($duration -lt 15) { - Write-Host "ERROR: Video must be at least 15 seconds long." -ForegroundColor Red - exit -} - -# Trim if over 30 seconds -$trimDuration = 30 -if ($duration -le 30) { - $trimDuration = $duration -} - -$baseName = [System.IO.Path]::GetFileNameWithoutExtension($inputFile) -$outputFile = "${baseName}_AppPreview.mp4" - -Write-Host "Formatting for App Store..." -ForegroundColor Cyan - -ffmpeg -y -i "$inputFile" ` - -t $trimDuration ` - -vf "scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,fps=30" ` - -c:v libx264 ` - -profile:v high ` - -level 4.0 ` - -pix_fmt yuv420p ` - -b:v 11M ` - -maxrate 12M ` - -bufsize 24M ` - -movflags +faststart ` - -c:a aac ` - -b:a 256k ` - -ac 2 ` - -ar 44100 ` - "$outputFile" - -if ($LASTEXITCODE -eq 0) { - Write-Host "Success! Output created:" -ForegroundColor Green - Write-Host "$outputFile" -} else { - Write-Host "Error during processing." -ForegroundColor Red +# ============================================= +# App Store App Preview Formatter +# - Trims to 30 seconds max +# - Errors if under 15 seconds +# - 886x1920 | 30fps | 10-12 Mbps +# ============================================= + +# -------- EDIT THIS -------- +$inputFile = "input.mp4" +# --------------------------- + +$width = 886 +$height = 1920 + +if (-Not (Test-Path $inputFile)) { + Write-Host "Input file not found: $inputFile" -ForegroundColor Red + exit +} + +# Get duration using ffprobe +$duration = ffprobe -v error -show_entries format=duration ` + -of default=noprint_wrappers=1:nokey=1 "$inputFile" + +$duration = [math]::Round([double]$duration,2) + +Write-Host "Video duration: $duration seconds" + +# Fail if under 15 seconds +if ($duration -lt 15) { + Write-Host "ERROR: Video must be at least 15 seconds long." -ForegroundColor Red + exit +} + +# Trim if over 30 seconds +$trimDuration = 30 +if ($duration -le 30) { + $trimDuration = $duration +} + +$baseName = [System.IO.Path]::GetFileNameWithoutExtension($inputFile) +$outputFile = "${baseName}_AppPreview.mp4" + +Write-Host "Formatting for App Store..." -ForegroundColor Cyan + +ffmpeg -y -i "$inputFile" ` + -t $trimDuration ` + -vf "scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,fps=30" ` + -c:v libx264 ` + -profile:v high ` + -level 4.0 ` + -pix_fmt yuv420p ` + -b:v 11M ` + -maxrate 12M ` + -bufsize 24M ` + -movflags +faststart ` + -c:a aac ` + -b:a 256k ` + -ac 2 ` + -ar 44100 ` + "$outputFile" + +if ($LASTEXITCODE -eq 0) { + Write-Host "Success! Output created:" -ForegroundColor Green + Write-Host "$outputFile" +} else { + Write-Host "Error during processing." -ForegroundColor Red } \ No newline at end of file diff --git a/frontend/services/api.ts b/frontend/services/api.ts index 8297d54..2f6d11c 100644 --- a/frontend/services/api.ts +++ b/frontend/services/api.ts @@ -22,17 +22,16 @@ type ApiUserWire = ApiUser & { links?: unknown; }; -type CachedEntry = { - value: T; - cachedAt: number; -}; - export type ApiDirectMessageThread = { peer_username: string; last_content: string; last_at: string; }; +type CachedEntry = { + value: T; + cachedAt: number; +}; const userCache = new Map>(); const inFlightGetRequests = new Map>(); const MAX_USER_CACHE_ENTRIES = 300; @@ -49,6 +48,58 @@ const REFRESH_READ_WINDOW_MS = 2500; let forceFreshReadsUntil = 0; let forceFreshReadToken: number | null = null; +// Dev-only mitigations for Expo Go's limited networking environment. +// These reduce burstiness, add small random staggering, and limit concurrent +// requests so Expo Go doesn't abort requests aggressively during heavy loads. +const DEV_MAX_CONCURRENT_REQUESTS = 6; +const DEV_MAX_GET_ATTEMPTS = 1; +const DEV_STAGGER_MS = 120; // max random stagger before starting a request +const DEV_REQUEST_SHAPING_ENV_KEY = "EXPO_PUBLIC_ENABLE_DEV_REQUEST_SHAPING"; +let devInFlightCount = 0; +const devQueue: Array<() => void> = []; + +const parseTruthyFlag = (value: unknown) => { + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (!normalized) return false; + return normalized !== "0" && normalized !== "false"; + } + return value === true; +}; + +const isDevRequestShapingEnabled = (() => { + try { + const extras = (Constants as any)?.expoConfig?.extra ?? + (Constants as any)?.manifest2?.extra ?? + (Constants as any)?.manifest?.extra ?? + null; + + const raw = extras?.[DEV_REQUEST_SHAPING_ENV_KEY] ?? + (typeof process !== "undefined" ? process.env?.[DEV_REQUEST_SHAPING_ENV_KEY] : undefined); + + return parseTruthyFlag(raw); + } catch { + return false; + } +})(); + +const acquireDevSlot = async () => { + if (!__DEV__) return; + if (devInFlightCount < DEV_MAX_CONCURRENT_REQUESTS) { + devInFlightCount += 1; + return; + } + await new Promise((resolve) => devQueue.push(resolve)); + devInFlightCount += 1; +}; + +const releaseDevSlot = () => { + if (!__DEV__) return; + devInFlightCount = Math.max(0, devInFlightCount - 1); + const next = devQueue.shift(); + if (next) next(); +}; + type RequestOptions = { timeoutMs?: number; forceFresh?: boolean; @@ -116,6 +167,10 @@ export const invalidateCachedUserById = (userId: number) => { }; export const setAuthToken = (token: string | null) => { + try { + // eslint-disable-next-line no-console + console.log("setAuthToken: token present=", !!token); + } catch {} const changed = authToken !== token; authToken = token; if (changed) { @@ -153,39 +208,70 @@ const getHostFromUri = (uri?: string | null) => { return host || null; }; -const getLocalDevBaseUrl = () => { - if (!__DEV__) { - return null; - } +// Removed `getLocalDevBaseUrl` — it was unused and duplicated logic in `getDefaultBaseUrl`. - const legacyConstants = Constants as unknown as { - manifest?: { debuggerHost?: string }; - manifest2?: { extra?: { expoClient?: { hostUri?: string } } }; - }; +const getDefaultBaseUrl = () => { + // Check for overrides provided via Expo `extra` or environment variables. + // This allows running the Expo client locally while targeting a remote + // backend (for example devbits.ddns.net) from any developer machine. + try { + const extras = (Constants as any)?.expoConfig?.extra ?? + (Constants as any)?.manifest2?.extra ?? + (Constants as any)?.manifest?.extra ?? null; + + const envCandidate = + extras?.EXPO_PUBLIC_API_URL ?? + extras?.API_URL ?? + extras?.API_BASE_URL ?? + (typeof process !== "undefined" ? process.env?.EXPO_PUBLIC_API_URL ?? process.env?.API_URL ?? null : null); + + const useLocalRaw = + extras?.EXPO_PUBLIC_USE_LOCAL_API ?? + (typeof process !== "undefined" ? process.env?.EXPO_PUBLIC_USE_LOCAL_API : undefined); + + const useLocal = typeof useLocalRaw === "string" + ? !/^(0|false)$/i.test(useLocalRaw.trim()) + : true; + + // If the developer has provided an explicit local API URL and opted into + // using the local API, prefer that URL so devices on the same Wi‑Fi can + // connect directly to the local server instead of hairpinning through + // the router's external DDNS address. Validate the candidate so malformed + // values like "http://" are ignored. + const localCandidate = extras?.EXPO_PUBLIC_LOCAL_API_URL ?? + (typeof process !== "undefined" ? process.env?.EXPO_PUBLIC_LOCAL_API_URL : undefined); + try { + if (useLocal && typeof localCandidate === "string" && localCandidate.trim()) { + const parsed = new URL(localCandidate.trim()); + if (parsed.hostname) { + return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`; + } + } + } catch { + // ignore invalid local candidate and fall back + } - const hostUri = - Constants.expoConfig?.hostUri ?? - legacyConstants.manifest?.debuggerHost ?? - legacyConstants.manifest2?.extra?.expoClient?.hostUri ?? - null; + if (typeof envCandidate === "string" && envCandidate.trim()) { + return envCandidate.trim(); + } - const host = getHostFromUri(hostUri); - if (!host) { - return null; + // If the developer explicitly disables using the local API in dev, use + // the production DDNS endpoint even when __DEV__ is true. + if (!useLocal) { + return "https://devbits.ddns.net"; + } + } catch { + // ignore and fall back to defaults below } - return `http://${host}`; -}; - -const getDefaultBaseUrl = () => { - // Use local API only when explicitly requested. - const shouldUseLocalApi = process.env.EXPO_PUBLIC_USE_LOCAL_API?.trim() === "1"; - if (__DEV__ && shouldUseLocalApi) { + // In development mode, by default connect to local backend (auto-detect computer's IP) + if (__DEV__) { const legacyConstants = Constants as unknown as { manifest?: { debuggerHost?: string }; manifest2?: { extra?: { expoClient?: { hostUri?: string } } }; }; + // Get the host IP from Expo's configuration (Metro bundler's IP) const hostUri = Constants.expoConfig?.hostUri ?? legacyConstants.manifest?.debuggerHost ?? @@ -194,17 +280,19 @@ const getDefaultBaseUrl = () => { const host = getHostFromUri(hostUri); if (host) { - return `http://${host}`; + return `http://${host}:8080`; } + // Fallbacks for different platforms if (Platform.OS === "android") { - return "http://10.0.2.2"; + return "http://10.0.2.2:8080"; } - return "http://localhost"; + // iOS simulator or web - try localhost + return "http://localhost:8080"; } - // Default to the live server for all other cases. + // Production: use live server return "https://devbits.ddns.net"; }; @@ -229,24 +317,41 @@ const buildBaseUrlList = (...candidates: Array) => { return urls; }; -export const API_BASE_URL = normalizeBaseUrl( - process.env.EXPO_PUBLIC_API_URL?.trim() || getDefaultBaseUrl(), -); - -const LOCAL_DEV_BASE_URL = getLocalDevBaseUrl(); - -const IS_LOCAL_API_MODE = - __DEV__ && process.env.EXPO_PUBLIC_USE_LOCAL_API?.trim() === "1"; +export const API_BASE_URL = normalizeBaseUrl(getDefaultBaseUrl()); + +// Defensive runtime validation: if the resolved URL is malformed (e.g. "http://" +// with no host) fall back to the public DDNS host and log an error so developers +// can see the problem in the Metro/Expo console. +try { + const checkUrl = new URL(API_BASE_URL); + if (!checkUrl.hostname) { + // eslint-disable-next-line no-console + console.error("Invalid API_BASE_URL resolved; falling back to https://devbits.ddns.net", API_BASE_URL); + // normalize and overwrite + (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net"); + } +} catch (e) { + // If parsing fails entirely, fallback and log. + try { + // eslint-disable-next-line no-console + console.error("Failed to parse API_BASE_URL; falling back to https://devbits.ddns.net", API_BASE_URL, String(e)); + } catch {} + (exports as any).API_BASE_URL = normalizeBaseUrl("https://devbits.ddns.net"); +} const API_FALLBACK_URL = normalizeBaseUrl( - process.env.EXPO_PUBLIC_API_FALLBACK_URL?.trim() || - (IS_LOCAL_API_MODE ? "" : "https://devbits.ddns.net"), + __DEV__ ? "" : "https://devbits.ddns.net", ); -const API_REQUEST_BASE_URLS = buildBaseUrlList(LOCAL_DEV_BASE_URL, API_BASE_URL, API_FALLBACK_URL); +const API_REQUEST_BASE_URLS = buildBaseUrlList(API_BASE_URL, API_FALLBACK_URL); const API_UPLOAD_BASE_URLS = API_REQUEST_BASE_URLS; const BASE_URL_FAILURE_COOLDOWN_MS = 12000; +// Cooldown for repeated failures to the same request path to avoid tight +// infinite re-queue loops from components that aggressively retry on error. +const PATH_FAILURE_COOLDOWN_MS = 5000; +const pathFailState = new Map(); + type BaseUrlFailState = { failedAt: number; cooldownUntil: number; @@ -256,6 +361,8 @@ type BaseUrlFailState = { const baseUrlFailState = new Map(); let preferredBaseUrl: string | null = API_REQUEST_BASE_URLS[0] ?? null; +const EFFECTIVE_REQUEST_TIMEOUT_MS = REQUEST_TIMEOUT_MS; + const getOrderedBaseUrls = (baseUrls: string[] = API_REQUEST_BASE_URLS) => { const now = Date.now(); const healthy: string[] = []; @@ -327,7 +434,7 @@ const isBlockedManagedUploadPath = (pathValue: string) => { return BLOCKED_MANAGED_UPLOAD_EXTENSIONS.has(getPathExtension(pathValue)); }; -const isTransientRequestError = (error: unknown) => { +const isTransientNetworkError = (error: unknown) => { if (error instanceof Error && error.name === "AbortError") { return true; } @@ -349,6 +456,11 @@ const fetchWithFailover = async ( ) => { const orderedBaseUrls = getOrderedBaseUrls(baseUrls); let lastError: unknown = null; + // Debug: show which base URLs we'll try + try { + // eslint-disable-next-line no-console + console.log("fetchWithFailover: orderedBaseUrls=", orderedBaseUrls); + } catch {} for (const baseUrl of orderedBaseUrls) { const controller = new AbortController(); @@ -356,7 +468,14 @@ const fetchWithFailover = async ( timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null; try { - const response = await fetch(buildPath(baseUrl), { + const url = buildPath(baseUrl); + // Debug: log attempted request URL + try { + // eslint-disable-next-line no-console + console.log("fetchWithFailover: trying", url); + } catch {} + + const response = await fetch(url, { ...init, signal: controller.signal, }); @@ -364,6 +483,10 @@ const fetchWithFailover = async ( return response; } catch (error) { lastError = error; + try { + // eslint-disable-next-line no-console + console.error("fetchWithFailover: baseUrl failed", baseUrl, String(error)); + } catch {} markBaseUrlFailure(baseUrl); } finally { if (timeoutId) { @@ -375,51 +498,9 @@ const fetchWithFailover = async ( throw lastError ?? new Error("No API base URL available"); }; -const isTransientUploadError = (error: unknown) => { - if (error instanceof Error && error.name === "AbortError") { - return true; - } - const message = - error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase(); - return ( - message.includes("network") || - message.includes("timed out") || - message.includes("failed to fetch") || - message.includes("connection") - ); -}; +// Reuse isTransientNetworkError for upload errors as well. -const fetchUploadWithFallback = async ( - path: string, - init: RequestInit, -) => { - let lastError: unknown = null; - - for (const baseUrl of API_UPLOAD_BASE_URLS) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS); - try { - const response = await fetch(`${baseUrl}${path}`, { - ...init, - signal: controller.signal, - }); - return response; - } catch (error) { - lastError = error; - continue; - } finally { - clearTimeout(timeoutId); - } - } - - if (lastError instanceof Error && lastError.name === "AbortError") { - throw new Error( - `Upload timed out after ${UPLOAD_TIMEOUT_MS / 1000}s. Check API connectivity to ${API_BASE_URL}.`, - ); - } - const detail = lastError instanceof Error ? lastError.message : String(lastError); - throw new Error(`Upload failed while connecting to ${API_BASE_URL}: ${detail}`); -}; +// removed unused fetchUploadWithFallback — uploadMultipartWithFallback is used instead const uploadMultipartWithFallback = async ( path: string, @@ -427,11 +508,21 @@ const uploadMultipartWithFallback = async ( headers: Headers, ) => { let lastError: unknown = null; + try { + // Debug: show upload multipart base urls + // eslint-disable-next-line no-console + console.log("uploadMultipartWithFallback: upload base urls=", API_UPLOAD_BASE_URLS, "path=", path); + } catch {} for (const baseUrl of API_UPLOAD_BASE_URLS) { const targetUrl = `${baseUrl}${path}`; try { + try { + // eslint-disable-next-line no-console + console.log("uploadMultipartWithFallback: trying multipart upload to", targetUrl); + } catch {} + const result = await new Promise<{ status: number; responseText: string; @@ -504,10 +595,13 @@ const uploadMultipartWithFallback = async ( xhr.send(body); }); - return result; } catch (error) { lastError = error; + try { + // eslint-disable-next-line no-console + console.error("uploadMultipartWithFallback: multipart upload failed to", baseUrl, String(error)); + } catch {} continue; } } @@ -628,7 +722,7 @@ export const resolveMediaUrl = (value?: string | null) => { return `${API_BASE_URL}${trimmed}`; } - const normalized = trimmed.startsWith("/") ? trimmed.slice(1) : trimmed; + const normalized = trimmed; if (normalized.startsWith("uploads/")) { if (isBlockedManagedUploadPath(normalized)) { return ""; @@ -656,9 +750,14 @@ const request = async ( const timeoutMs = typeof options?.timeoutMs === "number" ? Math.max(0, options.timeoutMs) - : REQUEST_TIMEOUT_MS; + : EFFECTIVE_REQUEST_TIMEOUT_MS; if (isGetRequest) { + const cooldownUntil = pathFailState.get(requestPath); + if (cooldownUntil && cooldownUntil > Date.now()) { + throw new Error(`Recent failures for ${requestPath}; throttling requests briefly.`); + } + const existing = inFlightGetRequests.get(inFlightKey); if (existing) { return existing as Promise; @@ -666,71 +765,100 @@ const request = async ( } const execute = async (): Promise => { - const headers = new Headers(init?.headers as HeadersInit | undefined); - const hasBody = !!init?.body; - const isFormData = typeof FormData !== "undefined" && init?.body instanceof FormData; - if (hasBody && !isFormData && !headers.has("Content-Type")) { - headers.set("Content-Type", "application/json"); - } - if (authToken) { - headers.set("Authorization", `Bearer ${authToken}`); - } - if (isGetRequest) { - headers.set("Cache-Control", "no-cache"); - headers.set("Pragma", "no-cache"); + let didAcquire = false; + + // Stagger + acquire dev slot if running in dev + if (__DEV__ && isDevRequestShapingEnabled) { + const stagger = Math.floor(Math.random() * DEV_STAGGER_MS); + if (stagger > 0) { + // eslint-disable-next-line no-await-in-loop + await new Promise((r) => setTimeout(r, stagger)); + } + await acquireDevSlot(); + didAcquire = true; } - const maxAttempts = isGetRequest ? 2 : 1; - let response: Response | null = null; - let lastError: unknown = null; + // Wrap headers setup, fetch attempts and parsing in a try/finally so the + // dev slot is always released even if the retry loop or parsing throws. + try { + const headers = new Headers(init?.headers as HeadersInit | undefined); + const hasBody = !!init?.body; + const isFormData = typeof FormData !== "undefined" && init?.body instanceof FormData; + if (hasBody && !isFormData && !headers.has("Content-Type")) { + headers.set("Content-Type", "application/json"); + } + if (authToken) { + headers.set("Authorization", `Bearer ${authToken}`); + } + if (isGetRequest) { + headers.set("Cache-Control", "no-cache"); + headers.set("Pragma", "no-cache"); + } - for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - try { - response = await fetchWithFailover( - (baseUrl) => `${baseUrl}${requestPath}`, - { - ...init, - headers, - }, - timeoutMs, - ); - lastError = null; - break; - } catch (error) { - lastError = error; - const canRetry = - attempt < maxAttempts && isGetRequest && isTransientRequestError(error); - if (canRetry) { - await new Promise((resolve) => setTimeout(resolve, 250)); - continue; + const maxAttempts = isGetRequest + ? (__DEV__ && isDevRequestShapingEnabled ? DEV_MAX_GET_ATTEMPTS : 2) + : 1; + let response: Response | null = null; + let lastError: unknown = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + response = await fetchWithFailover( + (baseUrl) => `${baseUrl}${requestPath}`, + { + ...init, + headers, + }, + timeoutMs, + ); + lastError = null; + break; + } catch (error) { + lastError = error; + const canRetry = + attempt < maxAttempts && isGetRequest && isTransientNetworkError(error); + if (canRetry) { + const backoff = Math.min(2000, 250 * Math.pow(2, attempt - 1)); + const jitter = Math.floor(Math.random() * 200); + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, backoff + jitter)); + continue; + } } } - } - if (!response) { - if (lastError instanceof Error && lastError.name === "AbortError") { - throw new Error( - `Request timed out after ${timeoutMs / 1000}s. Check API connectivity to ${API_BASE_URL}.`, - ); + if (!response) { + // Throttle repeated failures to the same path to avoid tight retry loops + // from higher-level UI code. + try { + pathFailState.set(requestPath, Date.now() + PATH_FAILURE_COOLDOWN_MS); + } catch {} + if (lastError instanceof Error && lastError.name === "AbortError") { + throw new Error( + `Request timed out after ${timeoutMs / 1000}s. Check API connectivity to ${API_BASE_URL}.`, + ); + } + const detail = lastError instanceof Error ? lastError.message : String(lastError); + throw new Error(`Network error connecting to ${API_BASE_URL}: ${detail}`); } - const detail = lastError instanceof Error ? lastError.message : String(lastError); - throw new Error(`Network error connecting to ${API_BASE_URL}: ${detail}`); - } - const text = await response.text(); + const text = await response.text(); - if (!response.ok) { - throw new Error(text || `Request failed (${response.status})`); - } + if (!response.ok) { + throw new Error(text || `Request failed (${response.status})`); + } - if (!text) { - return (null as unknown) as T; - } + if (!text) { + return (null as unknown) as T; + } - try { return JSON.parse(text) as T; - } catch { - throw new Error(`Invalid JSON response: ${text}`); + } finally { + if (didAcquire) { + try { + releaseDevSlot(); + } catch {} + } } }; @@ -1096,7 +1224,7 @@ export const updateUser = async (username: string, payload: UpdateUserRequest) = const pictureValue = typeof payload.picture === "string" ? payload.picture.trim() : ""; const timeoutMs = pictureValue.startsWith("data:") ? LARGE_REQUEST_TIMEOUT_MS - : REQUEST_TIMEOUT_MS; + : EFFECTIVE_REQUEST_TIMEOUT_MS; // Use POST /users/:username/update instead of PUT /users/:username // because iOS CFNetwork intermittently drops PUT request bodies, // causing silent 400 errors. POST bodies are delivered reliably. @@ -1328,7 +1456,7 @@ export const uploadMedia = async (file: { lastError = err; const retryableError = (err as { retryable?: boolean } | null)?.retryable === true || - isTransientUploadError(err); + isTransientNetworkError(err); if (attempt < MAX_RETRIES && retryableError) { await new Promise((r) => setTimeout(r, 250)); diff --git a/run-front.ps1 b/run-front.ps1 deleted file mode 100644 index 4d378b7..0000000 --- a/run-front.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -param( - [string]$ApiUrl = "" -) - -$ErrorActionPreference = "Stop" - -$root = Split-Path -Parent $MyInvocation.MyCommand.Path - -if ([string]::IsNullOrWhiteSpace($ApiUrl)) { - $lanIp = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | - Where-Object { - $_.IPAddress -match '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)' - } | - Select-Object -First 1 -ExpandProperty IPAddress - - if ([string]::IsNullOrWhiteSpace($lanIp)) { - $lanIp = "localhost" - } - - $ApiUrl = "http://$lanIp" -} - -Write-Host "Starting frontend against API: $ApiUrl" -ForegroundColor Green - -# Launch frontend in a new PowerShell window so logs stay visible -Start-Process powershell -WorkingDirectory "$root\frontend" -ArgumentList "-NoExit", "-Command", "cd '$root\frontend'; `$env:EXPO_PUBLIC_USE_LOCAL_API='1'; `$env:EXPO_PUBLIC_API_URL='$ApiUrl'; `$env:EXPO_PUBLIC_API_FALLBACK_URL='$ApiUrl'; npm run frontend" - -Write-Host "===== Summary =====" -ForegroundColor Cyan -Write-Host "Action: Started frontend" -Write-Host "Frontend API URL: $ApiUrl" - diff --git a/run-front.sh b/run-front.sh deleted file mode 100644 index 9cd96d6..0000000 --- a/run-front.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Start frontend against hosted API -set -e - -ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -if [[ -z "${API_URL:-}" ]]; then - LAN_IP=$(hostname -I 2>/dev/null | awk '{print $1}') - if [[ -z "$LAN_IP" ]]; then - LAN_IP="localhost" - fi - API_URL="http://$LAN_IP" -fi - -echo "Starting frontend against API: $API_URL" -( - cd "$ROOT/frontend" - EXPO_PUBLIC_USE_LOCAL_API="1" EXPO_PUBLIC_API_URL="$API_URL" EXPO_PUBLIC_API_FALLBACK_URL="$API_URL" npm run frontend -) & - -wait - -echo -echo "===== Summary =====" -echo "Action: Started frontend" -echo "Frontend API URL: $API_URL"