Files
Brummel 2c6062a53e refactor: drop /web/ URL prefix from browser routes
The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.

The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.

Pre-production: no transition redirects.
2026-05-04 18:36:10 +02:00

200 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Test helper: record a dictation via ffmpeg, upload it to a running
# doctate-server, then display transcript + oneliner. Stand-in for the
# smartwatch until hardware arrives.
#
# Usage:
# ./scripts/dictate.sh interactive (c/n/r/q)
# ./scripts/dictate.sh -c continue current case (record)
# ./scripts/dictate.sh -n new case (record)
# ./scripts/dictate.sh -r refresh — show current case state, no recording
#
# State: /tmp/doctate-current-case (transient, fine for testing).
set -euo pipefail
STATE_FILE="/tmp/doctate-current-case"
SERVER_URL="${SERVER_URL:-http://localhost:3000}"
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ENV_FILE="$REPO_ROOT/server/.env"
USERS_FILE="$REPO_ROOT/server/users.toml"
TRANSCRIPT_TIMEOUT_SECS=120
die() { echo "error: $*" >&2; exit 1; }
# -- Config: DATA_PATH + first api_key/slug from the server's config --
[ -f "$ENV_FILE" ] || die ".env not found at $ENV_FILE"
[ -f "$USERS_FILE" ] || die "users.toml not found at $USERS_FILE"
DATA_PATH=$(grep -E '^DATA_PATH=' "$ENV_FILE" | head -1 | cut -d= -f2-)
[ -n "$DATA_PATH" ] || die "DATA_PATH not set in $ENV_FILE"
if [ -n "${API_KEY:-}" ]; then
SLUG=$(awk -v key="$API_KEY" '
/^\[\[users\]\]/ { cur_slug=""; cur_key=""; next }
/^slug[[:space:]]*=/ { gsub(/"/, "", $3); cur_slug=$3 }
/^api_key[[:space:]]*=/ { gsub(/"/, "", $3); cur_key=$3; if (cur_key==key) { print cur_slug; exit } }
' "$USERS_FILE")
[ -n "$SLUG" ] || die "API_KEY not found in $USERS_FILE"
else
SLUG=$(awk '/^slug[[:space:]]*=/ { gsub(/"/, "", $3); print $3; exit }' "$USERS_FILE")
API_KEY=$(awk '/^api_key[[:space:]]*=/ { gsub(/"/, "", $3); print $3; exit }' "$USERS_FILE")
fi
# Locate a case directory under open/ or done/.
case_dir_for() {
local case_id="$1"
local d="$DATA_PATH/$SLUG/$case_id"
if [ -d "$d" ] && [ ! -f "$d/.deleted" ]; then
echo "$d"
return 0
fi
return 1
}
# Print everything we know about a case: recordings, transcripts, oneliner.
show_case() {
local case_id="$1"
local dir
dir=$(case_dir_for "$case_id") || { echo "Case $case_id: no files on disk yet."; return; }
local recs
recs=$(ls "$dir"/*.m4a 2>/dev/null | wc -l)
echo "Case $case_id ($recs recording$([ "$recs" = 1 ] || echo s))"
echo "Dir: $dir"
echo
echo "=== Transcripts ==="
if ls "$dir"/*.transcript.txt >/dev/null 2>&1; then
for t in "$dir"/*.transcript.txt; do
echo "--- $(basename "$t") ---"
cat "$t"
echo
done
else
echo "(none yet)"
fi
echo "=== Oneliner ==="
if [ -f "$dir/oneliner.txt" ]; then
cat "$dir/oneliner.txt"
echo
else
echo "(noch nicht generiert — später mit -r erneut prüfen)"
fi
echo
echo "Browse: $SERVER_URL/cases"
}
# Record + upload for the given case, then display the case state.
# Bails out (non-zero) on recording/upload errors so the loop can continue.
record_and_show() {
local case_id="$1"
local audio="/tmp/doctate-dictate-$case_id-$(date +%s).m4a"
echo
echo "Recording to $audio — press Ctrl-C to stop."
ffmpeg -loglevel error -f pulse -i default -c:a aac -b:a 64k "$audio" || true
if [ ! -s "$audio" ]; then
echo "error: recording is empty — skipped" >&2
return 1
fi
echo "Recorded $(du -h "$audio" | cut -f1)."
local recorded_at
recorded_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "Uploading to $SERVER_URL/api/upload ..."
local resp
resp=$(curl -sS -X POST "$SERVER_URL/api/upload" \
-H "X-API-Key: $API_KEY" \
-F "case_id=$case_id" \
-F "recorded_at=$recorded_at" \
-F "audio=@$audio")
echo "Response: $resp"
local stamp
stamp=$(echo "$recorded_at" | tr ':' '-')
local case_dir
case_dir=$(case_dir_for "$case_id") || { echo "error: case dir not found" >&2; return 1; }
local transcript="$case_dir/$stamp.transcript.txt"
echo "Waiting for transcript ..."
local i
for i in $(seq 1 $((TRANSCRIPT_TIMEOUT_SECS / 2))); do
[ -f "$transcript" ] && break
sleep 2
done
if [ ! -f "$transcript" ]; then
echo "error: timeout waiting for transcript (>${TRANSCRIPT_TIMEOUT_SECS}s)" >&2
return 1
fi
echo
show_case "$case_id"
}
# Execute one action: c=continue, n=new, r=refresh.
run_action() {
local action="$1"
case "$action" in
c)
[ -f "$STATE_FILE" ] || { echo "error: no current case — start a new one" >&2; return 1; }
local case_id
case_id=$(cat "$STATE_FILE")
echo "Continuing case $case_id"
record_and_show "$case_id"
;;
n)
local case_id
case_id=$(uuidgen)
echo "$case_id" > "$STATE_FILE"
echo "New case $case_id"
record_and_show "$case_id"
;;
r)
[ -f "$STATE_FILE" ] || { echo "error: no current case" >&2; return 1; }
show_case "$(cat "$STATE_FILE")"
;;
*) echo "error: invalid choice: $action" >&2; return 1 ;;
esac
}
# Interactive menu prompt. Echoes the chosen mode letter on stdout.
interactive_prompt() {
local current count dir mode
if [ -f "$STATE_FILE" ]; then
current=$(cat "$STATE_FILE")
count=0
dir=$(case_dir_for "$current") && count=$(ls "$dir"/*.m4a 2>/dev/null | wc -l)
echo "Current case: $current ($count recordings)" >&2
read -rp "[c] continue, [n] new, [r] refresh, [q] quit → " mode
else
echo "No current case — starting a new one." >&2
mode="n"
fi
echo "$mode"
}
# -- Parse flag (single-shot when given; interactive loop otherwise) --
case "${1:-}" in
-c|--continue) run_action c; exit $? ;;
-n|--new) run_action n; exit $? ;;
-r|--refresh) run_action r; exit $? ;;
-h|--help)
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
exit 0
;;
"") ;;
*) die "unknown flag: $1" ;;
esac
# -- Interactive loop --
while true; do
mode=$(interactive_prompt)
case "$mode" in
q|"") exit 0 ;;
c|n|r) run_action "$mode" || true ;;
*) echo "error: invalid choice: $mode" >&2 ;;
esac
echo
done