Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66bdb5ef41 | ||
|
|
9dfaa154ca | ||
|
|
8b51d920da | ||
|
|
4d00b15f7b |
@@ -28,6 +28,9 @@ ORIGIN=http://localhost:5173
|
|||||||
# Docker Compose (NPM → frontend nginx → backend): TRUST_PROXY=1
|
# Docker Compose (NPM → frontend nginx → backend): TRUST_PROXY=1
|
||||||
# TRUST_PROXY=1
|
# TRUST_PROXY=1
|
||||||
|
|
||||||
|
# Local dev (./scripts/start-dev.sh): Prisma + backend API
|
||||||
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/daagbox?schema=public"
|
||||||
|
|
||||||
# Docker Compose database (required for production deploy)
|
# Docker Compose database (required for production deploy)
|
||||||
# Generate: openssl rand -hex 24
|
# Generate: openssl rand -hex 24
|
||||||
# Rotate on running server: ./scripts/rotate-postgres-password.sh (see docs/deployment/postgres-password.md)
|
# Rotate on running server: ./scripts/rotate-postgres-password.sh (see docs/deployment/postgres-password.md)
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import {
|
|||||||
type GeolocationErrorReason,
|
type GeolocationErrorReason,
|
||||||
type GpsSignalQuality
|
type GpsSignalQuality
|
||||||
} from '../utils/geolocation.js'
|
} from '../utils/geolocation.js'
|
||||||
|
import { formatCourseAngle } from '../utils/courseAngle.js'
|
||||||
import { sortLogEventsByTime, type LogEventPayload } from '../utils/logEntryPayload.js'
|
import { sortLogEventsByTime, type LogEventPayload } from '../utils/logEntryPayload.js'
|
||||||
import {
|
import {
|
||||||
dedupeSailNames,
|
dedupeSailNames,
|
||||||
@@ -633,16 +634,18 @@ export default function LiveLogView({
|
|||||||
setModal(type)
|
setModal(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const liveLogGpsPrefillOptions = {
|
||||||
|
timeoutMs: 8000,
|
||||||
|
enableHighAccuracy: false,
|
||||||
|
maximumAge: 60_000
|
||||||
|
} as const
|
||||||
|
|
||||||
const openSogModal = async () => {
|
const openSogModal = async () => {
|
||||||
let prefill = ''
|
let prefill = ''
|
||||||
try {
|
try {
|
||||||
const permission = await queryGeolocationPermission()
|
const permission = await queryGeolocationPermission()
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
const pos = await getCurrentPosition({
|
const pos = await getCurrentPosition(liveLogGpsPrefillOptions)
|
||||||
timeoutMs: 8000,
|
|
||||||
enableHighAccuracy: false,
|
|
||||||
maximumAge: 60_000
|
|
||||||
})
|
|
||||||
if (pos.speedKn != null) prefill = String(pos.speedKn)
|
if (pos.speedKn != null) prefill = String(pos.speedKn)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -651,6 +654,20 @@ export default function LiveLogView({
|
|||||||
openValueModal('sog', prefill)
|
openValueModal('sog', prefill)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openCourseModal = async () => {
|
||||||
|
let prefill = lastCourseFromEvents(events)
|
||||||
|
try {
|
||||||
|
const permission = await queryGeolocationPermission()
|
||||||
|
if (permission === 'granted') {
|
||||||
|
const pos = await getCurrentPosition(liveLogGpsPrefillOptions)
|
||||||
|
if (pos.headingDeg != null) prefill = formatCourseAngle(pos.headingDeg, true)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to last logged course or manual entry
|
||||||
|
}
|
||||||
|
openValueModal('course', prefill)
|
||||||
|
}
|
||||||
|
|
||||||
const handleMotorToggle = () => {
|
const handleMotorToggle = () => {
|
||||||
hapticPulse()
|
hapticPulse()
|
||||||
const starting = !motorRunning
|
const starting = !motorRunning
|
||||||
@@ -1433,7 +1450,7 @@ export default function LiveLogView({
|
|||||||
onCastOff={handleCastOff}
|
onCastOff={handleCastOff}
|
||||||
onMoor={handleMoor}
|
onMoor={handleMoor}
|
||||||
onOpenSails={() => { setSelectedSails([]); setModal('sails') }}
|
onOpenSails={() => { setSelectedSails([]); setModal('sails') }}
|
||||||
onOpenCourse={() => openValueModal('course', lastCourseFromEvents(events))}
|
onOpenCourse={() => void openCourseModal()}
|
||||||
onOpenSog={() => void openSogModal()}
|
onOpenSog={() => void openSogModal()}
|
||||||
onOpenStw={() => openValueModal('stw')}
|
onOpenStw={() => openValueModal('stw')}
|
||||||
onOpenFuel={() => openValueModal('fuel')}
|
onOpenFuel={() => openValueModal('fuel')}
|
||||||
@@ -1796,6 +1813,7 @@ export default function LiveLogView({
|
|||||||
<div className="live-log-modal-backdrop" onClick={() => setModal('none')}>
|
<div className="live-log-modal-backdrop" onClick={() => setModal('none')}>
|
||||||
<div className="live-log-modal live-log-modal--dial" onClick={(e) => e.stopPropagation()}>
|
<div className="live-log-modal live-log-modal--dial" onClick={(e) => e.stopPropagation()}>
|
||||||
<h3>{t('logs.live_course_btn')}</h3>
|
<h3>{t('logs.live_course_btn')}</h3>
|
||||||
|
<p className="live-log-modal-hint">{t('logs.live_course_hint')}</p>
|
||||||
<div className="live-log-dial-field">
|
<div className="live-log-dial-field">
|
||||||
<label>{t('logs.event_mgk')}</label>
|
<label>{t('logs.event_mgk')}</label>
|
||||||
<CourseDialInput
|
<CourseDialInput
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
"live_sog_placeholder": "f.eks. 5,2",
|
"live_sog_placeholder": "f.eks. 5,2",
|
||||||
"live_stw_placeholder": "f.eks. 4,8",
|
"live_stw_placeholder": "f.eks. 4,8",
|
||||||
"live_sog_hint": "Afstand over jordoverfladen (kn) — GPS-værdien udfyldes automatisk, hvis den er tilgængelig.",
|
"live_sog_hint": "Afstand over jordoverfladen (kn) — GPS-værdien udfyldes automatisk, hvis den er tilgængelig.",
|
||||||
|
"live_course_hint": "Kurs over jord (°) — GPS-værdien udfyldes automatisk, hvis den er tilgængelig.",
|
||||||
"delete_entry": "Slet dag",
|
"delete_entry": "Slet dag",
|
||||||
"delete_confirm": "Er du sikker på, at du vil slette denne rejsedag endeligt?",
|
"delete_confirm": "Er du sikker på, at du vil slette denne rejsedag endeligt?",
|
||||||
"carry_over_tanks_title": "Skal data fra i går overføres?",
|
"carry_over_tanks_title": "Skal data fra i går overføres?",
|
||||||
|
|||||||
@@ -395,6 +395,7 @@
|
|||||||
"live_sog_placeholder": "z. B. 5,2",
|
"live_sog_placeholder": "z. B. 5,2",
|
||||||
"live_stw_placeholder": "z. B. 4,8",
|
"live_stw_placeholder": "z. B. 4,8",
|
||||||
"live_sog_hint": "Fahrt über Grund (kn) — GPS-Wert wird vorgefüllt, wenn verfügbar.",
|
"live_sog_hint": "Fahrt über Grund (kn) — GPS-Wert wird vorgefüllt, wenn verfügbar.",
|
||||||
|
"live_course_hint": "Kurs über Grund (°) — GPS-Wert wird vorgefüllt, wenn verfügbar.",
|
||||||
"delete_entry": "Tag löschen",
|
"delete_entry": "Tag löschen",
|
||||||
"delete_confirm": "Bist du sicher, dass du diesen Reisetag unwiderruflich löschen möchtest?",
|
"delete_confirm": "Bist du sicher, dass du diesen Reisetag unwiderruflich löschen möchtest?",
|
||||||
"carry_over_tanks_title": "Daten vom Vortag übernehmen?",
|
"carry_over_tanks_title": "Daten vom Vortag übernehmen?",
|
||||||
|
|||||||
@@ -395,6 +395,7 @@
|
|||||||
"live_sog_placeholder": "e.g. 5.2",
|
"live_sog_placeholder": "e.g. 5.2",
|
||||||
"live_stw_placeholder": "e.g. 4.8",
|
"live_stw_placeholder": "e.g. 4.8",
|
||||||
"live_sog_hint": "Speed over ground (kn) — prefilled from GPS when available.",
|
"live_sog_hint": "Speed over ground (kn) — prefilled from GPS when available.",
|
||||||
|
"live_course_hint": "Course over ground (°) — prefilled from GPS when available.",
|
||||||
"delete_entry": "Delete Day",
|
"delete_entry": "Delete Day",
|
||||||
"delete_confirm": "Are you sure you want to permanently delete this travel day?",
|
"delete_confirm": "Are you sure you want to permanently delete this travel day?",
|
||||||
"carry_over_tanks_title": "Carry over from previous day?",
|
"carry_over_tanks_title": "Carry over from previous day?",
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
"live_sog_placeholder": "p. ej., 5,2",
|
"live_sog_placeholder": "p. ej., 5,2",
|
||||||
"live_stw_placeholder": "p. ej., 4,8",
|
"live_stw_placeholder": "p. ej., 4,8",
|
||||||
"live_sog_hint": "Distancia recorrida (km) — El valor del GPS se rellena automáticamente si está disponible.",
|
"live_sog_hint": "Distancia recorrida (km) — El valor del GPS se rellena automáticamente si está disponible.",
|
||||||
|
"live_course_hint": "Rumbo sobre el fondo (°) — El valor del GPS se rellena automáticamente si está disponible.",
|
||||||
"delete_entry": "Eliminar etiqueta",
|
"delete_entry": "Eliminar etiqueta",
|
||||||
"delete_confirm": "¿Estás seguro de que quieres eliminar definitivamente este día de viaje?",
|
"delete_confirm": "¿Estás seguro de que quieres eliminar definitivamente este día de viaje?",
|
||||||
"carry_over_tanks_title": "¿Importar los datos del día anterior?",
|
"carry_over_tanks_title": "¿Importar los datos del día anterior?",
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
"live_sog_placeholder": "par exemple 5,2",
|
"live_sog_placeholder": "par exemple 5,2",
|
||||||
"live_stw_placeholder": "par exemple 4,8",
|
"live_stw_placeholder": "par exemple 4,8",
|
||||||
"live_sog_hint": "Vitesse par rapport au sol (kn) — La valeur GPS est préremplie si elle est disponible.",
|
"live_sog_hint": "Vitesse par rapport au sol (kn) — La valeur GPS est préremplie si elle est disponible.",
|
||||||
|
"live_course_hint": "Cap sur le fond (°) — La valeur GPS est préremplie si elle est disponible.",
|
||||||
"delete_entry": "Supprimer la balise",
|
"delete_entry": "Supprimer la balise",
|
||||||
"delete_confirm": "Es-tu sûr de vouloir supprimer définitivement cette date de voyage ?",
|
"delete_confirm": "Es-tu sûr de vouloir supprimer définitivement cette date de voyage ?",
|
||||||
"carry_over_tanks_title": "Reprendre les données de la veille ?",
|
"carry_over_tanks_title": "Reprendre les données de la veille ?",
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
"live_sog_placeholder": "f.eks. 5,2",
|
"live_sog_placeholder": "f.eks. 5,2",
|
||||||
"live_stw_placeholder": "f.eks. 4,8",
|
"live_stw_placeholder": "f.eks. 4,8",
|
||||||
"live_sog_hint": "Fart over grunn (kn) — GPS-verdien fylles ut automatisk hvis den er tilgjengelig.",
|
"live_sog_hint": "Fart over grunn (kn) — GPS-verdien fylles ut automatisk hvis den er tilgjengelig.",
|
||||||
|
"live_course_hint": "Kurs over grunn (°) — GPS-verdien fylles ut automatisk hvis den er tilgjengelig.",
|
||||||
"delete_entry": "Slett dag",
|
"delete_entry": "Slett dag",
|
||||||
"delete_confirm": "Er du sikker på at du vil slette denne reisedagen uten mulighet for å angre?",
|
"delete_confirm": "Er du sikker på at du vil slette denne reisedagen uten mulighet for å angre?",
|
||||||
"carry_over_tanks_title": "Skal data fra i går overføres?",
|
"carry_over_tanks_title": "Skal data fra i går overføres?",
|
||||||
|
|||||||
@@ -389,6 +389,7 @@
|
|||||||
"live_sog_placeholder": "t.ex. 5,2",
|
"live_sog_placeholder": "t.ex. 5,2",
|
||||||
"live_stw_placeholder": "t.ex. 4,8",
|
"live_stw_placeholder": "t.ex. 4,8",
|
||||||
"live_sog_hint": "Avstånd över marken (kn) — GPS-värdet fylls i automatiskt om det finns tillgängligt.",
|
"live_sog_hint": "Avstånd över marken (kn) — GPS-värdet fylls i automatiskt om det finns tillgängligt.",
|
||||||
|
"live_course_hint": "Kurs över mark (°) — GPS-värdet fylls i automatiskt om det finns tillgängligt.",
|
||||||
"delete_entry": "Ta bort dag",
|
"delete_entry": "Ta bort dag",
|
||||||
"delete_confirm": "Är du säker på att du vill radera den här resedagen utan möjlighet att ångra dig?",
|
"delete_confirm": "Är du säker på att du vill radera den här resedagen utan möjlighet att ångra dig?",
|
||||||
"carry_over_tanks_title": "Överföra data från föregående dag?",
|
"carry_over_tanks_title": "Överföra data från föregående dag?",
|
||||||
|
|||||||
@@ -77,11 +77,29 @@ describe('geolocation helpers', () => {
|
|||||||
lat: '59.910000',
|
lat: '59.910000',
|
||||||
lng: '10.750000',
|
lng: '10.750000',
|
||||||
speedKn: 4.9,
|
speedKn: 4.9,
|
||||||
|
headingDeg: null,
|
||||||
accuracyM: 12,
|
accuracyM: 12,
|
||||||
signalQuality: 'excellent'
|
signalQuality: 'excellent'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('resolves heading from getCurrentPosition when reported', async () => {
|
||||||
|
vi.stubGlobal('navigator', {
|
||||||
|
geolocation: {
|
||||||
|
getCurrentPosition: (success: PositionCallback) => {
|
||||||
|
success({
|
||||||
|
coords: { latitude: 59.91, longitude: 10.75, heading: 245.6, accuracy: 8 }
|
||||||
|
} as GeolocationPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(getCurrentPosition({ timeoutMs: 1000, enableHighAccuracy: false })).resolves.toMatchObject({
|
||||||
|
headingDeg: 246,
|
||||||
|
signalQuality: 'excellent'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('formats GPS accuracy for display', () => {
|
it('formats GPS accuracy for display', () => {
|
||||||
expect(formatGpsAccuracyMeters(12.4)).toBe('12')
|
expect(formatGpsAccuracyMeters(12.4)).toBe('12')
|
||||||
expect(formatGpsAccuracyMeters(87)).toBe('87')
|
expect(formatGpsAccuracyMeters(87)).toBe('87')
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export interface GeoCoordinates {
|
|||||||
lng: string
|
lng: string
|
||||||
/** SOG from GPS when available (kn), otherwise null. */
|
/** SOG from GPS when available (kn), otherwise null. */
|
||||||
speedKn: number | null
|
speedKn: number | null
|
||||||
|
/** COG from GPS when available (degrees, 0–359), otherwise null. */
|
||||||
|
headingDeg: number | null
|
||||||
/** Estimated horizontal accuracy in metres, when reported by the browser. */
|
/** Estimated horizontal accuracy in metres, when reported by the browser. */
|
||||||
accuracyM: number | null
|
accuracyM: number | null
|
||||||
/** Derived signal quality indicator for UI hints. */
|
/** Derived signal quality indicator for UI hints. */
|
||||||
@@ -142,10 +144,16 @@ function normalizeGetPositionOptions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHeadingDeg(heading: number | null | undefined): number | null {
|
||||||
|
if (heading == null || !Number.isFinite(heading)) return null
|
||||||
|
return Math.round(((heading % 360) + 360) % 360)
|
||||||
|
}
|
||||||
|
|
||||||
function positionFromGeolocationPosition(pos: GeolocationPosition): GeoCoordinates {
|
function positionFromGeolocationPosition(pos: GeolocationPosition): GeoCoordinates {
|
||||||
const speedKn = pos.coords.speed != null && Number.isFinite(pos.coords.speed)
|
const speedKn = pos.coords.speed != null && Number.isFinite(pos.coords.speed)
|
||||||
? Number((pos.coords.speed * MPS_TO_KNOTS).toFixed(1))
|
? Number((pos.coords.speed * MPS_TO_KNOTS).toFixed(1))
|
||||||
: null
|
: null
|
||||||
|
const headingDeg = normalizeHeadingDeg(pos.coords.heading)
|
||||||
const accuracyM = pos.coords.accuracy != null && Number.isFinite(pos.coords.accuracy)
|
const accuracyM = pos.coords.accuracy != null && Number.isFinite(pos.coords.accuracy)
|
||||||
? pos.coords.accuracy
|
? pos.coords.accuracy
|
||||||
: null
|
: null
|
||||||
@@ -153,6 +161,7 @@ function positionFromGeolocationPosition(pos: GeolocationPosition): GeoCoordinat
|
|||||||
lat: formatAppCoordinate(pos.coords.latitude),
|
lat: formatAppCoordinate(pos.coords.latitude),
|
||||||
lng: formatAppCoordinate(pos.coords.longitude),
|
lng: formatAppCoordinate(pos.coords.longitude),
|
||||||
speedKn,
|
speedKn,
|
||||||
|
headingDeg,
|
||||||
accuracyM,
|
accuracyM,
|
||||||
signalQuality: classifyGpsAccuracyMeters(accuracyM)
|
signalQuality: classifyGpsAccuracyMeters(accuracyM)
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-14
@@ -38,6 +38,32 @@ resolve_node_toolchain() {
|
|||||||
command -v npm >/dev/null 2>&1
|
command -v npm >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
load_root_env() {
|
||||||
|
local env_file="$REPO_ROOT/.env"
|
||||||
|
if [ ! -f "$env_file" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$env_file"
|
||||||
|
set +a
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_database_url() {
|
||||||
|
if [ -n "${DATABASE_URL:-}" ]; then
|
||||||
|
printf '%s' "$DATABASE_URL"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local user="${POSTGRES_USER:-postgres}"
|
||||||
|
local password="${POSTGRES_PASSWORD:-}"
|
||||||
|
local db="${POSTGRES_DB:-daagbox}"
|
||||||
|
[ -z "$user" ] && user=postgres
|
||||||
|
[ -z "$password" ] && password=postgres
|
||||||
|
[ -z "$db" ] && db=daagbox
|
||||||
|
printf 'postgresql://%s:%s@localhost:5432/%s?schema=public' "$user" "$password" "$db"
|
||||||
|
}
|
||||||
|
|
||||||
check_dev_env() {
|
check_dev_env() {
|
||||||
local env_file="$REPO_ROOT/.env"
|
local env_file="$REPO_ROOT/.env"
|
||||||
if [ ! -f "$env_file" ]; then
|
if [ ! -f "$env_file" ]; then
|
||||||
@@ -91,38 +117,84 @@ echo "========================================"
|
|||||||
echo "Preparing to (re)start services..."
|
echo "Preparing to (re)start services..."
|
||||||
|
|
||||||
require_node_toolchain
|
require_node_toolchain
|
||||||
|
load_root_env
|
||||||
|
export DATABASE_URL="$(resolve_database_url)"
|
||||||
check_dev_env
|
check_dev_env
|
||||||
|
|
||||||
# Clean up processes running on ports
|
# Clean up processes running on ports
|
||||||
|
stop_process_tree() {
|
||||||
|
local pid=$1
|
||||||
|
[ -z "$pid" ] && return 0
|
||||||
|
if ! kill -0 "$pid" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local child
|
||||||
|
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
|
||||||
|
stop_process_tree "$child"
|
||||||
|
done
|
||||||
|
|
||||||
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_process_group() {
|
||||||
|
local pgid=$1
|
||||||
|
[ -z "$pgid" ] && return 0
|
||||||
|
kill -TERM "-$pgid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
force_stop_process_group() {
|
||||||
|
local pgid=$1
|
||||||
|
[ -z "$pgid" ] && return 0
|
||||||
|
kill -KILL "-$pgid" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
cleanup_port() {
|
cleanup_port() {
|
||||||
local port=$1
|
local port=$1
|
||||||
if command -v lsof >/dev/null 2>&1; then
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
local pid=$(lsof -t -i:$port)
|
local pids
|
||||||
if [ ! -z "$pid" ]; then
|
pids=$(lsof -t -i:"$port" 2>/dev/null || true)
|
||||||
echo "Port $port is currently in use by PID $pid. Stopping process..."
|
if [ -n "$pids" ]; then
|
||||||
kill -9 $pid 2>/dev/null
|
echo "Port $port is currently in use. Stopping process(es)..."
|
||||||
|
local pid
|
||||||
|
for pid in $pids; do
|
||||||
|
stop_process_tree "$pid"
|
||||||
|
done
|
||||||
|
sleep 0.3
|
||||||
|
for pid in $pids; do
|
||||||
|
kill -KILL "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
elif command -v fuser >/dev/null 2>&1; then
|
elif command -v fuser >/dev/null 2>&1; then
|
||||||
echo "Port $port is currently in use. Stopping process..."
|
echo "Port $port is currently in use. Stopping process..."
|
||||||
fuser -k $port/tcp 2>/dev/null
|
fuser -k "$port"/tcp 2>/dev/null
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup_port $SERVER_PORT
|
BACKEND_PGID=""
|
||||||
cleanup_port $CLIENT_PORT
|
CLIENT_PGID=""
|
||||||
|
|
||||||
# Clean exit handler
|
|
||||||
cleanup_all() {
|
cleanup_all() {
|
||||||
echo ""
|
echo ""
|
||||||
echo "Stopping all dev servers..."
|
echo "Stopping all dev servers..."
|
||||||
# Kill all child jobs
|
|
||||||
kill $(jobs -p) 2>/dev/null
|
stop_process_group "$BACKEND_PGID"
|
||||||
|
stop_process_group "$CLIENT_PGID"
|
||||||
|
sleep 0.5
|
||||||
|
force_stop_process_group "$BACKEND_PGID"
|
||||||
|
force_stop_process_group "$CLIENT_PGID"
|
||||||
|
|
||||||
|
cleanup_port "$SERVER_PORT"
|
||||||
|
cleanup_port "$CLIENT_PORT"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Trap termination signals
|
# Trap termination signals
|
||||||
trap cleanup_all SIGINT SIGTERM EXIT
|
trap cleanup_all SIGINT SIGTERM EXIT
|
||||||
|
|
||||||
|
cleanup_port "$SERVER_PORT"
|
||||||
|
cleanup_port "$CLIENT_PORT"
|
||||||
|
|
||||||
# Manage PostgreSQL Docker container
|
# Manage PostgreSQL Docker container
|
||||||
if command -v docker >/dev/null 2>&1; then
|
if command -v docker >/dev/null 2>&1; then
|
||||||
echo "Checking PostgreSQL Docker container (postgres-daagbox)..."
|
echo "Checking PostgreSQL Docker container (postgres-daagbox)..."
|
||||||
@@ -189,8 +261,9 @@ if [ ! -d node_modules ]; then
|
|||||||
echo "Error: server/node_modules missing. Run: cd server && npm ci"
|
echo "Error: server/node_modules missing. Run: cd server && npm ci"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
npm run dev &
|
setsid npm run dev &
|
||||||
BACKEND_PID=$!
|
BACKEND_PID=$!
|
||||||
|
BACKEND_PGID=$BACKEND_PID
|
||||||
cd "$REPO_ROOT" || exit 1
|
cd "$REPO_ROOT" || exit 1
|
||||||
|
|
||||||
# Sleep briefly to let server start up
|
# Sleep briefly to let server start up
|
||||||
@@ -205,7 +278,9 @@ echo "Starting frontend dev server..."
|
|||||||
cd "$REPO_ROOT/client" || exit 1
|
cd "$REPO_ROOT/client" || exit 1
|
||||||
if [ ! -d node_modules ]; then
|
if [ ! -d node_modules ]; then
|
||||||
echo "Error: client/node_modules missing. Run: cd client && npm ci"
|
echo "Error: client/node_modules missing. Run: cd client && npm ci"
|
||||||
kill "$BACKEND_PID" 2>/dev/null
|
stop_process_group "$BACKEND_PGID"
|
||||||
|
sleep 0.3
|
||||||
|
force_stop_process_group "$BACKEND_PGID"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# Vite 6+ via plugin-react 4; refresh lockfile after package.json changes
|
# Vite 6+ via plugin-react 4; refresh lockfile after package.json changes
|
||||||
@@ -213,14 +288,17 @@ if ! node -e "require.resolve('vite/package.json')" 2>/dev/null; then
|
|||||||
echo "Client dependencies incomplete — running npm ci..."
|
echo "Client dependencies incomplete — running npm ci..."
|
||||||
npm ci || exit 1
|
npm ci || exit 1
|
||||||
fi
|
fi
|
||||||
npm run dev &
|
setsid npm run dev &
|
||||||
CLIENT_PID=$!
|
CLIENT_PID=$!
|
||||||
|
CLIENT_PGID=$CLIENT_PID
|
||||||
cd "$REPO_ROOT" || exit 1
|
cd "$REPO_ROOT" || exit 1
|
||||||
|
|
||||||
sleep 1.5
|
sleep 1.5
|
||||||
if ! kill -0 "$CLIENT_PID" 2>/dev/null; then
|
if ! kill -0 "$CLIENT_PID" 2>/dev/null; then
|
||||||
echo "Error: Frontend dev server exited immediately. Check client logs above."
|
echo "Error: Frontend dev server exited immediately. Check client logs above."
|
||||||
kill "$BACKEND_PID" 2>/dev/null
|
stop_process_group "$BACKEND_PGID"
|
||||||
|
sleep 0.3
|
||||||
|
force_stop_process_group "$BACKEND_PGID"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Generated
+1
@@ -7,6 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.10.2",
|
"@prisma/client": "^5.10.2",
|
||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user