diff --git a/.env.example b/.env.example index 94b5289..875c502 100644 --- a/.env.example +++ b/.env.example @@ -1,38 +1,38 @@ -# Compose settings for the vocaphone gateway. +# Compose settings for the VocaGateway. # # Copy this file, then fill in a token: # # umask 077 # cp .env.example .env -# printf 'VOCAPHONE_TOKEN=%s\n' "$(openssl rand -hex 32)" >> .env +# printf 'VOCAGATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" >> .env # # Only `docker compose` reads .env. A gateway started natively (`just run`) -# takes its token from ~/.config/vocaphone/token instead, and direnv is set up +# takes its token from ~/.config/vocagateway/token instead, and direnv is set up # not to export anything from here. Never commit the populated .env. # Bearer token the phone apps present on every request. At least 32 characters. # Compose passes it to the container as a secret, not as an environment # variable. Generate one with: openssl rand -hex 32 -VOCAPHONE_TOKEN= +VOCAGATEWAY_TOKEN= # Host interface and port Docker publishes the gateway on. # 127.0.0.1 keeps it on loopback, which is what Tailscale Serve expects. Use # 0.0.0.0 only for a trusted LAN, with the host firewall up. Never forward this # port from the public internet. -VOCAPHONE_PUBLISH_HOST=127.0.0.1 -VOCAPHONE_PUBLISH_PORT=8765 +VOCAGATEWAY_PUBLISH_HOST=127.0.0.1 +VOCAGATEWAY_PUBLISH_PORT=8765 # Uncomment on Linux Docker Engine to share the host's network namespace, so # the pairing QR can discover the machine's real LAN address instead of the -# container's bridge IP. This ignores VOCAPHONE_PUBLISH_HOST and _PORT, so lock +# container's bridge IP. This ignores VOCAGATEWAY_PUBLISH_HOST and _PORT, so lock # the port down with the host firewall first. Docker Desktop does not support # it. -#VOCAPHONE_NETWORK_MODE=host +#VOCAGATEWAY_NETWORK_MODE=host # Use a prebuilt multi-architecture image instead of building the local tag. -#VOCAPHONE_IMAGE=ghcr.io/your-user/vocaphone-gateway:latest +#VOCAGATEWAY_IMAGE=ghcr.io/your-user/vocaphone-gateway:latest # Serve the Swagger UI at /docs and the schema at /openapi.json. Off by # default: the schema and UI are extra surface area, so enable this only for # local development. -#VOCAPHONE_DEBUG=true +#VOCAGATEWAY_DEBUG=true diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 66fdbef..a1ae8ed 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -53,4 +53,4 @@ jobs: - name: Validate Compose configuration run: docker compose config --quiet env: - VOCAPHONE_TOKEN: test-token-with-at-least-thirty-two-characters + VOCAGATEWAY_TOKEN: test-token-with-at-least-thirty-two-characters diff --git a/Dockerfile b/Dockerfile index 9c8b008..2b01dc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,14 +48,14 @@ RUN uv sync --frozen --no-dev --extra engines \ ENV PATH="/app/.venv/bin:${PATH}" \ PYTHONUNBUFFERED=1 \ - VOCAPHONE_BIND_HOST=0.0.0.0 \ - VOCAPHONE_PORT=8765 \ - VOCAPHONE_DATA_DIR=/data \ - VOCAPHONE_MODELS_DIR=/data/models \ - VOCAPHONE_CONFIG_FILE=/data/config/config.json \ - VOCAPHONE_TOKEN_FILE=/run/secrets/vocaphone_token \ - VOCAPHONE_WHISPER_BINARY=/usr/local/bin/whisper-cli \ - VOCAPHONE_ENGINE=auto + VOCAGATEWAY_BIND_HOST=0.0.0.0 \ + VOCAGATEWAY_PORT=8765 \ + VOCAGATEWAY_DATA_DIR=/data \ + VOCAGATEWAY_MODELS_DIR=/data/models \ + VOCAGATEWAY_CONFIG_FILE=/data/config/config.json \ + VOCAGATEWAY_TOKEN_FILE=/run/secrets/vocagateway_token \ + VOCAGATEWAY_WHISPER_BINARY=/usr/local/bin/whisper-cli \ + VOCAGATEWAY_ENGINE=auto USER vocaphone EXPOSE 8765 diff --git a/Dockerfile.cuda b/Dockerfile.cuda index 6192824..462ce77 100644 --- a/Dockerfile.cuda +++ b/Dockerfile.cuda @@ -44,14 +44,14 @@ RUN uv sync --frozen --no-dev --extra engines \ && chown --recursive vocaphone:vocaphone /data /app ENV PATH="/app/.venv/bin:${PATH}" \ PYTHONUNBUFFERED=1 \ - VOCAPHONE_BIND_HOST=0.0.0.0 \ - VOCAPHONE_PORT=8765 \ - VOCAPHONE_DATA_DIR=/data \ - VOCAPHONE_MODELS_DIR=/data/models \ - VOCAPHONE_CONFIG_FILE=/data/config/config.json \ - VOCAPHONE_TOKEN_FILE=/run/secrets/vocaphone_token \ - VOCAPHONE_WHISPER_BINARY=/usr/local/bin/whisper-cli \ - VOCAPHONE_ENGINE=auto + VOCAGATEWAY_BIND_HOST=0.0.0.0 \ + VOCAGATEWAY_PORT=8765 \ + VOCAGATEWAY_DATA_DIR=/data \ + VOCAGATEWAY_MODELS_DIR=/data/models \ + VOCAGATEWAY_CONFIG_FILE=/data/config/config.json \ + VOCAGATEWAY_TOKEN_FILE=/run/secrets/vocagateway_token \ + VOCAGATEWAY_WHISPER_BINARY=/usr/local/bin/whisper-cli \ + VOCAGATEWAY_ENGINE=auto USER vocaphone EXPOSE 8765 VOLUME ["/data"] diff --git a/Dockerfile.vulkan b/Dockerfile.vulkan index 1ba6c7f..255c0f7 100644 --- a/Dockerfile.vulkan +++ b/Dockerfile.vulkan @@ -44,14 +44,14 @@ RUN uv sync --frozen --no-dev --extra engines \ && chown --recursive vocaphone:vocaphone /data /app ENV PATH="/app/.venv/bin:${PATH}" \ PYTHONUNBUFFERED=1 \ - VOCAPHONE_BIND_HOST=0.0.0.0 \ - VOCAPHONE_PORT=8765 \ - VOCAPHONE_DATA_DIR=/data \ - VOCAPHONE_MODELS_DIR=/data/models \ - VOCAPHONE_CONFIG_FILE=/data/config/config.json \ - VOCAPHONE_TOKEN_FILE=/run/secrets/vocaphone_token \ - VOCAPHONE_WHISPER_BINARY=/usr/local/bin/whisper-cli \ - VOCAPHONE_ENGINE=auto + VOCAGATEWAY_BIND_HOST=0.0.0.0 \ + VOCAGATEWAY_PORT=8765 \ + VOCAGATEWAY_DATA_DIR=/data \ + VOCAGATEWAY_MODELS_DIR=/data/models \ + VOCAGATEWAY_CONFIG_FILE=/data/config/config.json \ + VOCAGATEWAY_TOKEN_FILE=/run/secrets/vocagateway_token \ + VOCAGATEWAY_WHISPER_BINARY=/usr/local/bin/whisper-cli \ + VOCAGATEWAY_ENGINE=auto USER vocaphone EXPOSE 8765 VOLUME ["/data"] diff --git a/README.md b/README.md index d0e1cc9..9eac3bc 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,10 @@ audio with FFmpeg, invokes a local speech engine, and returns an idempotent transcript. An authenticated HTMX WebUI covers setup, model management, engine selection, microphone testing, and operational status. -CLI entry points, environment variables, and on-disk paths still use the -`vocaphone` prefix (`vocaphone-server`, `VOCAPHONE_*`, `~/.config/vocaphone/`) -so existing installs keep working. A later rename can introduce aliases without -breaking those paths. - -> CLI entry points, env vars, and config paths previously used the Local Flow -> working name (`localflow-server`, `LOCALFLOW_*`, `~/.config/localflow/`) and -> were renamed to vocaphone in v0.3.0. Native startups migrate a missing -> bootstrap token from the old path once; see the -> [migration guide](docs/deployment.md#migrating-from-the-local-flow-working-name-v030). +CLI entry points still use the historical `vocaphone-server` script names from +the Python package (`vocaphone-gateway`). Environment variables and on-disk +paths use the `vocagateway` prefix (`VOCAGATEWAY_*`, `~/.config/vocagateway/`, +`~/.local/share/vocagateway/`). ## Consumers @@ -73,7 +67,7 @@ uv sync --all-groups --extra engines --extra apple uv run vocaphone-server ``` -The first run creates `~/.config/vocaphone/token` with mode `600`. Open +The first run creates `~/.config/vocagateway/token` with mode `600`. Open `http://127.0.0.1:8765/`, enter the token, download a recommended model, select it, and confirm the Overview shows **Ready for dictation**. @@ -89,7 +83,7 @@ writes logs to `~/Library/Logs/Vocaphone/`. MLX Audio and WhisperKit are recommended on Apple silicon. The `apple` extra installs MLX only on an arm64 Mac; it is deliberately absent from Linux and Docker. The standalone `whisper.cpp` engine uses the `whisper-cli` binary -installed above (override its location with `VOCAPHONE_WHISPER_BINARY`); on a +installed above (override its location with `VOCAGATEWAY_WHISPER_BINARY`); on a native Linux host it is optional and can be built from source instead. ## Native Linux quick start @@ -106,7 +100,7 @@ uv run vocaphone-server ``` Do not pass `--extra apple` on Linux. The first run creates -`~/.config/vocaphone/token` with mode `600`. The banner prints the WebUI URL and +`~/.config/vocagateway/token` with mode `600`. The banner prints the WebUI URL and token path; show the secret (and a terminal pairing QR) with `just token` or `uv run vocaphone-token`. Open `http://127.0.0.1:8765/`, enter the token, download a recommended model (SenseVoice Small INT8 or Parakeet TDT INT8 on @@ -132,7 +126,7 @@ Phone clients on the same LAN can use `http://:8765` while the gateway binds `0.0.0.0` (the default). For Tailscale Serve only, bind loopback: ```sh -VOCAPHONE_BIND_HOST=127.0.0.1 uv run vocaphone-server +VOCAGATEWAY_BIND_HOST=127.0.0.1 uv run vocaphone-server ``` ### Phone pairing QR @@ -146,7 +140,7 @@ The code encodes a versioned JSON payload: ``` Discovery prefers private Wi‑Fi addresses (for example `192.168.x.x`). Override -with `VOCAPHONE_PUBLIC_URL` or `VOCAPHONE_PAIRING_URL` when automatic selection +with `VOCAGATEWAY_PUBLIC_URL` or `VOCAGATEWAY_PAIRING_URL` when automatic selection is wrong. The same payload is available without the WebUI: on a TTY, `just token` (or `uv run vocaphone-token`) prints an ASCII QR for headless setup; use `just token --plain` when you only want the secret (pipes always get @@ -168,9 +162,9 @@ CLI. The same Dockerfile builds on Linux `amd64` and `arm64`. ```sh umask 077 -printf 'VOCAPHONE_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env -printf 'VOCAPHONE_PUBLISH_HOST=127.0.0.1\n' >> .env -printf 'VOCAPHONE_PUBLISH_PORT=8765\n' >> .env +printf 'VOCAGATEWAY_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env +printf 'VOCAGATEWAY_PUBLISH_HOST=127.0.0.1\n' >> .env +printf 'VOCAGATEWAY_PUBLISH_PORT=8765\n' >> .env docker compose up --detach --build docker compose ps curl --fail http://127.0.0.1:8765/health/live @@ -178,7 +172,7 @@ curl --fail http://127.0.0.1:8765/health/live The token is provided as a Compose secret rather than a container environment variable. Models, configuration, and the SQLite database persist in the -`vocaphone_vocaphone-data` named volume mounted at `/data`. +`vocagateway_vocagateway-data` named volume mounted at `/data`. The container is live before a model is installed, so `/health/ready` initially returns `503`. Open the WebUI, enter the token from `.env`, download/select a @@ -190,17 +184,17 @@ curl --fail http://127.0.0.1:8765/health/ready The default Compose publication is host loopback only. This is appropriate for Tailscale Serve. To intentionally allow direct LAN access, set -`VOCAPHONE_PUBLISH_HOST=0.0.0.0` in `.env` and protect the port with the host +`VOCAGATEWAY_PUBLISH_HOST=0.0.0.0` in `.env` and protect the port with the host firewall. Never expose port 8765 to the public internet. The default bridge network also hides the host's real LAN address from the gateway's own address auto-discovery (used for the pairing QR): the container only ever sees its private bridge IP, not the host's Wi-Fi/Ethernet interface. On Linux Docker Engine (not Docker Desktop on macOS/Windows), set -`VOCAPHONE_NETWORK_MODE=host` in `.env` instead so the container shares the +`VOCAGATEWAY_NETWORK_MODE=host` in `.env` instead so the container shares the host's network namespace and discovery finds the real `192.168.x.x` address. -This ignores `VOCAPHONE_PUBLISH_HOST`/`PORT` — the container binds directly on -the host per `VOCAPHONE_BIND_HOST`/`VOCAPHONE_PORT`, so lock down port 8765 +This ignores `VOCAGATEWAY_PUBLISH_HOST`/`PORT` — the container binds directly on +the host per `VOCAGATEWAY_BIND_HOST`/`VOCAGATEWAY_PORT`, so lock down port 8765 with the host firewall first. ## WebUI @@ -222,7 +216,7 @@ The authenticated WebUI provides: or session identifiers - named, independently revocable per-device tokens (Settings tab), so losing one phone means revoking that device's token instead of rotating everyone - else's; the bootstrap `VOCAPHONE_TOKEN` always keeps working alongside them + else's; the bootstrap `VOCAGATEWAY_TOKEN` always keeps working alongside them Operational counters stay in process memory, contain no audio or transcript content, and reset when the gateway process restarts. @@ -352,12 +346,12 @@ complete model. VocaMac does not need to be running. To force VocaMac from the environment: ```sh -export VOCAPHONE_ENGINE=vocamac -export VOCAPHONE_VOCAMAC_MODEL='small' # optional; otherwise VocaMac's own choice +export VOCAGATEWAY_ENGINE=vocamac +export VOCAGATEWAY_VOCAMAC_MODEL='small' # optional; otherwise VocaMac's own choice uv run vocaphone-server ``` -`VOCAPHONE_VOCAMAC_MODEL` accepts either a VocaMac model size (`small`, +`VOCAGATEWAY_VOCAMAC_MODEL` accepts either a VocaMac model size (`small`, `large-v3-v20240930_turbo_632MB`) or a WhisperKit folder name (`openai_whisper-small`). A configured model is never substituted: if it is not downloaded, the engine reports unavailable rather than quietly using another. @@ -365,18 +359,18 @@ downloaded, the engine reports unavailable rather than quietly using another. To force Handy from the environment: ```sh -export VOCAPHONE_ENGINE=handy -export VOCAPHONE_HANDY_MODEL='owner/repository/model.gguf' -export VOCAPHONE_HANDY_FALLBACK_MODEL='owner/repository/fallback-model.gguf' +export VOCAGATEWAY_ENGINE=handy +export VOCAGATEWAY_HANDY_MODEL='owner/repository/model.gguf' +export VOCAGATEWAY_HANDY_FALLBACK_MODEL='owner/repository/fallback-model.gguf' uv run vocaphone-server ``` To force standalone `whisper.cpp`: ```sh -export VOCAPHONE_ENGINE=whisper.cpp -export VOCAPHONE_WHISPER_BINARY=/absolute/path/to/whisper-cli -export VOCAPHONE_WHISPER_MODEL=/absolute/path/to/ggml-model.bin +export VOCAGATEWAY_ENGINE=whisper.cpp +export VOCAGATEWAY_WHISPER_BINARY=/absolute/path/to/whisper-cli +export VOCAGATEWAY_WHISPER_MODEL=/absolute/path/to/ggml-model.bin uv run vocaphone-server ``` @@ -384,30 +378,30 @@ uv run vocaphone-server | Variable | Native default | Container default | Purpose | | --- | --- | --- | --- | -| `VOCAPHONE_BIND_HOST` | `0.0.0.0` | `0.0.0.0` inside container | Gateway listener | -| `VOCAPHONE_PORT` | `8765` | `8765` | Gateway listener port | -| `VOCAPHONE_TOKEN` | unset | unset | Direct token override; at least 32 characters | -| `VOCAPHONE_TOKEN_FILE` | `~/.config/vocaphone/token` | `/run/secrets/vocaphone_token` | Bearer-token file | -| `VOCAPHONE_DATA_DIR` | `~/.local/share/vocaphone` | `/data` | Sessions and application data | -| `VOCAPHONE_MODELS_DIR` | `/models` | `/data/models` | Downloaded models | -| `VOCAPHONE_CONFIG_FILE` | `~/.config/vocaphone/config.json` | `/data/config/config.json` | WebUI engine/model choice | -| `VOCAPHONE_ENGINE` | `auto` | `auto` | `auto`, `vocamac`, `handy`, `mlx-audio`, `whisperkit`, `sherpa-onnx`, `faster-whisper`, `moonshine`, or `whisper.cpp` | -| `VOCAPHONE_WHISPER_BINARY` | `/opt/homebrew/bin/whisper-cli` | `/usr/local/bin/whisper-cli` | `whisper.cpp` executable | -| `VOCAPHONE_WHISPER_MODEL` | base model path | base model path | Fallback `whisper.cpp` model | -| `VOCAPHONE_WHISPERKIT_BINARY` | `whisperkit-cli` | unavailable | WhisperKit executable | -| `VOCAPHONE_VOCAMAC_APP` | `/Applications/VocaMac.app` | unavailable | Optional VocaMac app bundle | -| `VOCAPHONE_VOCAMAC_MODEL` | unset | unset | Pin a VocaMac model instead of following the app's choice | -| `VOCAPHONE_RETENTION_HOURS` | `24` | `24` | Failed-session retry retention | -| `VOCAPHONE_DELETE_SUCCESSFUL_AUDIO` | `true` | `true` | Delete source/normalized audio after success | +| `VOCAGATEWAY_BIND_HOST` | `0.0.0.0` | `0.0.0.0` inside container | Gateway listener | +| `VOCAGATEWAY_PORT` | `8765` | `8765` | Gateway listener port | +| `VOCAGATEWAY_TOKEN` | unset | unset | Direct token override; at least 32 characters | +| `VOCAGATEWAY_TOKEN_FILE` | `~/.config/vocagateway/token` | `/run/secrets/vocagateway_token` | Bearer-token file | +| `VOCAGATEWAY_DATA_DIR` | `~/.local/share/vocagateway` | `/data` | Sessions and application data | +| `VOCAGATEWAY_MODELS_DIR` | `/models` | `/data/models` | Downloaded models | +| `VOCAGATEWAY_CONFIG_FILE` | `~/.config/vocagateway/config.json` | `/data/config/config.json` | WebUI engine/model choice | +| `VOCAGATEWAY_ENGINE` | `auto` | `auto` | `auto`, `vocamac`, `handy`, `mlx-audio`, `whisperkit`, `sherpa-onnx`, `faster-whisper`, `moonshine`, or `whisper.cpp` | +| `VOCAGATEWAY_WHISPER_BINARY` | `/opt/homebrew/bin/whisper-cli` | `/usr/local/bin/whisper-cli` | `whisper.cpp` executable | +| `VOCAGATEWAY_WHISPER_MODEL` | base model path | base model path | Fallback `whisper.cpp` model | +| `VOCAGATEWAY_WHISPERKIT_BINARY` | `whisperkit-cli` | unavailable | WhisperKit executable | +| `VOCAGATEWAY_VOCAMAC_APP` | `/Applications/VocaMac.app` | unavailable | Optional VocaMac app bundle | +| `VOCAGATEWAY_VOCAMAC_MODEL` | unset | unset | Pin a VocaMac model instead of following the app's choice | +| `VOCAGATEWAY_RETENTION_HOURS` | `24` | `24` | Failed-session retry retention | +| `VOCAGATEWAY_DELETE_SUCCESSFUL_AUDIO` | `true` | `true` | Delete source/normalized audio after success | Compose-specific variables live in `.env`: | Variable | Default | Purpose | | --- | --- | --- | -| `VOCAPHONE_PUBLISH_HOST` | `127.0.0.1` | Host interface published by Docker | -| `VOCAPHONE_PUBLISH_PORT` | `8765` | Host port published by Docker | -| `VOCAPHONE_NETWORK_MODE` | `bridge` | Set to `host` on Linux Docker Engine to share the host's network namespace (ignores `VOCAPHONE_PUBLISH_HOST`/`PORT`); not supported by Docker Desktop | -| `VOCAPHONE_IMAGE` | `vocaphone-gateway:local` | Local or registry image tag | +| `VOCAGATEWAY_PUBLISH_HOST` | `127.0.0.1` | Host interface published by Docker | +| `VOCAGATEWAY_PUBLISH_PORT` | `8765` | Host port published by Docker | +| `VOCAGATEWAY_NETWORK_MODE` | `bridge` | Set to `host` on Linux Docker Engine to share the host's network namespace (ignores `VOCAGATEWAY_PUBLISH_HOST`/`PORT`); not supported by Docker Desktop | +| `VOCAGATEWAY_IMAGE` | `vocaphone-gateway:local` | Local or registry image tag | Use [`.env.example`](.env.example) as a template and never commit the populated `.env` file. @@ -422,7 +416,7 @@ The iPhone and Android apps accept ordinary HTTP and HTTPS gateway URLs; a Tailscale hostname is not mandatory. Supported arrangements include: - a trusted LAN hostname such as `http://homelabone:8765/`; for Docker, set - `VOCAPHONE_PUBLISH_HOST=0.0.0.0` and protect the port with the host firewall + `VOCAGATEWAY_PUBLISH_HOST=0.0.0.0` and protect the port with the host firewall - a loopback listener exposed privately through Tailscale Serve - a VPS loopback listener behind an HTTPS reverse proxy and trusted certificate @@ -523,7 +517,7 @@ uv run ruff check . uv run ruff format --check . uv run mypy app uv run pytest -VOCAPHONE_TOKEN=test-token-with-at-least-thirty-two-characters docker compose config --quiet +VOCAGATEWAY_TOKEN=test-token-with-at-least-thirty-two-characters docker compose config --quiet docker build --tag vocaphone-gateway:test . ``` diff --git a/app/__init__.py b/app/__init__.py index fa5dbbf..fe546a5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1 +1 @@ -"""vocaphone private transcription gateway.""" +"""VocaGateway private transcription gateway.""" diff --git a/app/admin_queries.py b/app/admin_queries.py index 5fccb07..8de2a01 100644 --- a/app/admin_queries.py +++ b/app/admin_queries.py @@ -294,7 +294,7 @@ def token_entries(ctx: GatewayContext) -> list[DeviceTokenEntry]: entries = [ DeviceTokenEntry( id=BOOTSTRAP_TOKEN_ID, - label="Bootstrap token (VOCAPHONE_TOKEN / token file)", + label="Bootstrap token (VOCAGATEWAY_TOKEN / token file)", created_at=None, revocable=False, ) diff --git a/app/catalog.py b/app/catalog.py index 9048d3c..a130022 100644 --- a/app/catalog.py +++ b/app/catalog.py @@ -181,7 +181,7 @@ def _moonshine( ) ), source="Moonshine Voice", - marker_file=".vocaphone-model.json", + marker_file=".vocagateway-model.json", language_code=language, # Also as a tuple: the engine reads `language_code`, but the model cards and # the language filter read `language_codes`, and an empty tuple there means @@ -243,7 +243,7 @@ def _sherpa_onnx( family=family, description=description, source="sherpa-onnx", - marker_file=".vocaphone-model.json", + marker_file=".vocagateway-model.json", model_type=model_type, language_codes=language_codes, license_name=license_name, diff --git a/app/cli.py b/app/cli.py index 535bfdd..710a826 100644 --- a/app/cli.py +++ b/app/cli.py @@ -35,7 +35,7 @@ def serve() -> None: settings = Settings.from_env() host = settings.bind_host token_path = settings.token_file_display - token_source = "(from VOCAPHONE_TOKEN)" if _token_from_env() else token_path + token_source = "(from VOCAGATEWAY_TOKEN)" if _token_from_env() else token_path print(f"VocaGateway listening on {format_host_port(host, settings.port)}") print(f"WebUI (this host): {local_webui_url(host, settings.port)}") if host in WILDCARD_BIND_HOSTS: @@ -53,20 +53,20 @@ def serve() -> None: def _token_from_env() -> bool: - return bool(os.environ.get("VOCAPHONE_TOKEN", "").strip()) + return bool(os.environ.get("VOCAGATEWAY_TOKEN", "").strip()) def _load_existing_bootstrap_token() -> str: """Return the bootstrap token without minting a new secret. Path resolution matches :meth:`Settings.from_env` (blank - ``VOCAPHONE_TOKEN_FILE`` falls back to the default; ``~`` and + ``VOCAGATEWAY_TOKEN_FILE`` falls back to the default; ``~`` and ``XDG_CONFIG_HOME`` are expanded the same way). """ - token = os.environ.get("VOCAPHONE_TOKEN", "").strip() + token = os.environ.get("VOCAGATEWAY_TOKEN", "").strip() if token: return token - token_file = _env_path("VOCAPHONE_TOKEN_FILE", _default_token_file()) + token_file = _env_path("VOCAGATEWAY_TOKEN_FILE", _default_token_file()) if not token_file.is_file(): print( "No token yet — the gateway writes one on first start: just run", @@ -81,7 +81,7 @@ def _load_existing_bootstrap_token() -> str: def _saved_pairing_url() -> str | None: - config_path = _env_path("VOCAPHONE_CONFIG_FILE", _default_config_file()) + config_path = _env_path("VOCAGATEWAY_CONFIG_FILE", _default_config_file()) return RuntimeConfig.load(config_path).pairing_url @@ -108,13 +108,13 @@ def token() -> None: print(secret) return - port = int(os.environ.get("VOCAPHONE_PORT", "8765")) + port = int(os.environ.get("VOCAGATEWAY_PORT", "8765")) gateway_url = default_pairing_url(port, saved_pairing_url=_saved_pairing_url()) print(f"Token: {secret}") if gateway_url is None: print( - "No phone-reachable gateway address found. Set VOCAPHONE_PUBLIC_URL " - f"or VOCAPHONE_PAIRING_URL (for example http://192.168.1.20:{port}), " + "No phone-reachable gateway address found. Set VOCAGATEWAY_PUBLIC_URL " + f"or VOCAGATEWAY_PAIRING_URL (for example http://192.168.1.20:{port}), " "or pick an address in the WebUI pairing card.", file=sys.stderr, ) @@ -127,7 +127,7 @@ def token() -> None: print(qr_ascii_for_payload(payload), end="") print() print( - "Override the encoded address with VOCAPHONE_PUBLIC_URL, VOCAPHONE_PAIRING_URL, " + "Override the encoded address with VOCAGATEWAY_PUBLIC_URL, VOCAGATEWAY_PAIRING_URL, " "or the WebUI pairing card." ) diff --git a/app/config.py b/app/config.py index c6c674a..4b7394a 100644 --- a/app/config.py +++ b/app/config.py @@ -2,17 +2,12 @@ import os import secrets -import shutil -import sys from dataclasses import dataclass from pathlib import Path DEFAULT_HANDY_FALLBACK_MODEL = "handy-computer/whisper-base-gguf/whisper-base-Q8_0.gguf" WILDCARD_BIND_HOSTS = frozenset({"0.0.0.0", "::"}) -MIGRATION_DOCS = ( - "https://github.com/VocaHQ/vocaphone/blob/main/docs/deployment.md" - "#migrating-from-the-local-flow-working-name-v030" -) +APP_DIR_NAME = "vocagateway" def format_host_port(host: str, port: int) -> str: @@ -38,32 +33,27 @@ def _xdg_data_home() -> Path: return Path(os.environ.get("XDG_DATA_HOME", "~/.local/share")).expanduser() +def _env(name: str, default: str = "") -> str: + return os.environ.get(name, "").strip() or default + + def _env_path(name: str, default: Path) -> Path: - """Resolve a path env var, treating missing/blank values as the default.""" - value = os.environ.get(name, "").strip() + value = _env(name) if not value: return default return Path(value).expanduser() def _default_token_file() -> Path: - return _xdg_config_home() / "vocaphone" / "token" + return _xdg_config_home() / APP_DIR_NAME / "token" def _default_config_file() -> Path: - return _xdg_config_home() / "vocaphone" / "config.json" - - -def _legacy_token_file() -> Path: - return _xdg_config_home() / "localflow" / "token" - + return _xdg_config_home() / APP_DIR_NAME / "config.json" -def _legacy_config_file() -> Path: - return _xdg_config_home() / "localflow" / "config.json" - -def _legacy_data_dir() -> Path: - return _xdg_data_home() / "localflow" +def _default_data_dir() -> Path: + return _xdg_data_home() / APP_DIR_NAME def _display_path(path: Path | str) -> str: @@ -89,8 +79,8 @@ class Settings: vocamac_model: str | None = None whisperkit_binary: str = "whisperkit-cli" models_dir: Path | None = None - config_path: Path = Path("~/.config/vocaphone/config.json") - token_file: Path = Path("~/.config/vocaphone/token") + config_path: Path = Path("~/.config/vocagateway/config.json") + token_file: Path = Path("~/.config/vocagateway/token") bind_host: str = "0.0.0.0" port: int = 8765 maximum_upload_bytes: int = 25 * 1024 * 1024 @@ -109,107 +99,63 @@ def token_file_display(self) -> str: @classmethod def from_env(cls) -> Settings: - _warn_ignored_legacy_env() - token_file = _env_path("VOCAPHONE_TOKEN_FILE", _default_token_file()) - token = os.environ.get("VOCAPHONE_TOKEN", "").strip() + token_file = _env_path("VOCAGATEWAY_TOKEN_FILE", _default_token_file()) + token = _env("VOCAGATEWAY_TOKEN") if not token and token_file.is_file(): token = token_file.read_text(encoding="utf-8").strip() if not token: - token = _resolve_or_migrate_token(token_file) + token = _generate_token(token_file) if len(token) < 32: raise RuntimeError( - "Set VOCAPHONE_TOKEN to at least 32 characters or create " + "Set VOCAGATEWAY_TOKEN to at least 32 characters or create " f"{_display_path(token_file)} with mode 600." ) - data_dir = _env_path("VOCAPHONE_DATA_DIR", _xdg_data_home() / "vocaphone") - if ( - "VOCAPHONE_DATA_DIR" not in os.environ - or not os.environ.get("VOCAPHONE_DATA_DIR", "").strip() - ): - _warn_unmigrated_legacy_data(data_dir) - config_path = _env_path("VOCAPHONE_CONFIG_FILE", _default_config_file()) - if ( - "VOCAPHONE_CONFIG_FILE" not in os.environ - or not os.environ.get("VOCAPHONE_CONFIG_FILE", "").strip() - ): - _migrate_legacy_config_file(config_path) + data_dir = _env_path("VOCAGATEWAY_DATA_DIR", _default_data_dir()) + models_override = _env("VOCAGATEWAY_MODELS_DIR") return cls( token=token, data_dir=data_dir, whisper_binary=Path( - os.environ.get("VOCAPHONE_WHISPER_BINARY", "/opt/homebrew/bin/whisper-cli") + _env("VOCAGATEWAY_WHISPER_BINARY", "/opt/homebrew/bin/whisper-cli") ).expanduser(), whisper_model=Path( - os.environ.get( - "VOCAPHONE_WHISPER_MODEL", + _env( + "VOCAGATEWAY_WHISPER_MODEL", "~/.local/share/whisper.cpp/models/ggml-base.en.bin", ) ).expanduser(), - engine=os.environ.get("VOCAPHONE_ENGINE", "auto").lower(), + engine=_env("VOCAGATEWAY_ENGINE", "auto").lower(), handy_binary=Path( - os.environ.get( - "VOCAPHONE_HANDY_BINARY", + _env( + "VOCAGATEWAY_HANDY_BINARY", "/Applications/Handy.app/Contents/MacOS/handy", ) ).expanduser(), - handy_model=os.environ.get("VOCAPHONE_HANDY_MODEL") or None, - handy_fallback_model=os.environ.get( - "VOCAPHONE_HANDY_FALLBACK_MODEL", + handy_model=_env("VOCAGATEWAY_HANDY_MODEL") or None, + handy_fallback_model=_env( + "VOCAGATEWAY_HANDY_FALLBACK_MODEL", DEFAULT_HANDY_FALLBACK_MODEL, ) or None, vocamac_app=Path( - os.environ.get("VOCAPHONE_VOCAMAC_APP", "/Applications/VocaMac.app") - ).expanduser(), - vocamac_model=os.environ.get("VOCAPHONE_VOCAMAC_MODEL") or None, - whisperkit_binary=os.environ.get("VOCAPHONE_WHISPERKIT_BINARY", "whisperkit-cli"), - models_dir=Path( - os.environ.get("VOCAPHONE_MODELS_DIR", str(data_dir / "models")) + _env("VOCAGATEWAY_VOCAMAC_APP", "/Applications/VocaMac.app") ).expanduser(), - config_path=config_path, + vocamac_model=_env("VOCAGATEWAY_VOCAMAC_MODEL") or None, + whisperkit_binary=_env("VOCAGATEWAY_WHISPERKIT_BINARY", "whisperkit-cli"), + models_dir=Path(models_override).expanduser() + if models_override + else data_dir / "models", + config_path=_env_path("VOCAGATEWAY_CONFIG_FILE", _default_config_file()), token_file=token_file, - bind_host=os.environ.get("VOCAPHONE_BIND_HOST", "0.0.0.0"), - port=int(os.environ.get("VOCAPHONE_PORT", "8765")), - retention_hours=int(os.environ.get("VOCAPHONE_RETENTION_HOURS", "24")), - delete_successful_audio=os.environ.get( - "VOCAPHONE_DELETE_SUCCESSFUL_AUDIO", "true" - ).lower() + bind_host=_env("VOCAGATEWAY_BIND_HOST", "0.0.0.0"), + port=int(_env("VOCAGATEWAY_PORT", "8765")), + retention_hours=int(_env("VOCAGATEWAY_RETENTION_HOURS", "24")), + delete_successful_audio=_env("VOCAGATEWAY_DELETE_SUCCESSFUL_AUDIO", "true").lower() in {"1", "true", "yes"}, - debug=os.environ.get("VOCAPHONE_DEBUG", "false").lower() in {"1", "true", "yes"}, + debug=_env("VOCAGATEWAY_DEBUG", "false").lower() in {"1", "true", "yes"}, ) -def _legacy_env_names() -> list[str]: - return sorted(name for name in os.environ if name.startswith("LOCALFLOW_")) - - -def _warn_ignored_legacy_env() -> None: - leftover = _legacy_env_names() - if not leftover: - return - print( - "vocaphone: LOCALFLOW_* environment variables are obsolete " - f"({', '.join(leftover)}). Rename them to VOCAPHONE_*. " - "LOCALFLOW_TOKEN / LOCALFLOW_TOKEN_FILE are read only for a " - f"one-time bootstrap migration. See {MIGRATION_DOCS}", - file=sys.stderr, - ) - - -def _legacy_token_candidates() -> list[tuple[str, str]]: - """Return (source label, token) pairs from the pre-rename Local Flow layout.""" - candidates: list[tuple[str, str]] = [] - env_token = os.environ.get("LOCALFLOW_TOKEN", "").strip() - if env_token: - candidates.append(("LOCALFLOW_TOKEN", env_token)) - legacy_file = _env_path("LOCALFLOW_TOKEN_FILE", _legacy_token_file()) - if legacy_file.is_file(): - file_token = legacy_file.read_text(encoding="utf-8").strip() - if file_token: - candidates.append((_display_path(legacy_file), file_token)) - return candidates - - def _write_token_file(token_file: Path, token: str) -> None: token_file.parent.mkdir(parents=True, exist_ok=True) token_file.parent.chmod(0o700) @@ -218,119 +164,6 @@ def _write_token_file(token_file: Path, token: str) -> None: handle.write(token + "\n") -def _token_conflict_error(token_file: Path, source: str) -> RuntimeError: - return RuntimeError( - f"{_display_path(token_file)} already contains a different bootstrap token " - f"than the Local Flow source ({source}). Remove the conflicting file or unset " - f"LOCALFLOW_TOKEN / remove {_display_path(_legacy_token_file())}, then retry so " - f"paired phones keep the secret they already hold. See {MIGRATION_DOCS}" - ) - - -def _persist_migrated_token(token_file: Path, token: str, source: str) -> None: - """Write a migrated token, replacing an empty destination if needed.""" - token_file.parent.mkdir(parents=True, exist_ok=True) - token_file.parent.chmod(0o700) - if token_file.is_file(): - existing = token_file.read_text(encoding="utf-8").strip() - if existing == token: - return - if existing: - raise _token_conflict_error(token_file, source) - token_file.write_text(token + "\n", encoding="utf-8") - token_file.chmod(0o600) - return - try: - _write_token_file(token_file, token) - except FileExistsError: - # Another process created the destination between our checks. - existing = token_file.read_text(encoding="utf-8").strip() - if existing == token: - return - if not existing: - token_file.write_text(token + "\n", encoding="utf-8") - token_file.chmod(0o600) - return - raise _token_conflict_error(token_file, source) from None - - -def _resolve_or_migrate_token(token_file: Path) -> str: - """Prefer a one-time Local Flow token migration over minting a new secret.""" - legacy = _legacy_token_candidates() - if legacy: - source, token = legacy[0] - try: - _persist_migrated_token(token_file, token, source) - except OSError as exc: - raise RuntimeError( - "Found a Local Flow bootstrap token at " - f"{source} but could not write {_display_path(token_file)}: {exc}. " - f"Copy it manually, then restart. See {MIGRATION_DOCS}" - ) from exc - print( - "vocaphone: migrated Local Flow bootstrap token from " - f"{source} to {_display_path(token_file)}. Paired phones keep working. " - f"See {MIGRATION_DOCS}", - file=sys.stderr, - ) - return token - - legacy_data = _legacy_data_dir() - if legacy_data.is_dir() and any(legacy_data.iterdir()): - raise RuntimeError( - "Found Local Flow data at " - f"{_display_path(legacy_data)} but no vocaphone bootstrap token. " - f"Copy {_display_path(_legacy_token_file())} to {_display_path(token_file)} " - f"(or set VOCAPHONE_TOKEN) before starting the gateway so paired " - f"phones are not locked out. See {MIGRATION_DOCS}" - ) - return _generate_token(token_file) - - -def _migrate_legacy_config_file(config_path: Path) -> None: - if config_path.is_file() and config_path.read_text(encoding="utf-8").strip(): - return - legacy = _legacy_config_file() - if not legacy.is_file() or not legacy.read_text(encoding="utf-8").strip(): - return - try: - config_path.parent.mkdir(parents=True, exist_ok=True) - config_path.parent.chmod(0o700) - shutil.copy2(legacy, config_path) - config_path.chmod(0o600) - except OSError as exc: - print( - "vocaphone: could not migrate " - f"{_display_path(legacy)} to {_display_path(config_path)}: {exc}. " - f"Copy it manually if you need the previous WebUI settings. " - f"See {MIGRATION_DOCS}", - file=sys.stderr, - ) - return - print( - "vocaphone: migrated Local Flow config from " - f"{_display_path(legacy)} to {_display_path(config_path)}. " - f"See {MIGRATION_DOCS}", - file=sys.stderr, - ) - - -def _warn_unmigrated_legacy_data(data_dir: Path) -> None: - if data_dir.exists(): - return - legacy = _legacy_data_dir() - if not legacy.is_dir() or not any(legacy.iterdir()): - return - print( - "vocaphone: Local Flow data still exists at " - f"{_display_path(legacy)}. " - f"Move or copy it to {_display_path(data_dir)} (and rename any " - ".localflow-model.json markers to .vocaphone-model.json) to keep " - f"downloaded models and session history. See {MIGRATION_DOCS}", - file=sys.stderr, - ) - - def _generate_token(token_file: Path) -> str: """First-run friendly default: create a private token automatically.""" token = secrets.token_urlsafe(48) diff --git a/app/context.py b/app/context.py index 4c5e2cc..b6bc299 100644 --- a/app/context.py +++ b/app/context.py @@ -17,7 +17,7 @@ from app.tokens import TokenStore VERSION = "0.2.0" -TOKEN_FILE_HINT = "~/.config/vocaphone/token" +TOKEN_FILE_HINT = "~/.config/vocagateway/token" BOOTSTRAP_TOKEN_ID = "bootstrap" diff --git a/app/engines.py b/app/engines.py index 1f4a0d3..f4f26fc 100644 --- a/app/engines.py +++ b/app/engines.py @@ -196,7 +196,7 @@ def build_engine( runtime_config: RuntimeConfig, model_manager: ModelManager, ) -> TranscriptionEngine: - # A non-auto VOCAPHONE_ENGINE is an operator force-flag and must win over + # A non-auto VOCAGATEWAY_ENGINE is an operator force-flag and must win over # whatever the WebUI last persisted (including the default "auto" written # into config.json). When the env is auto, honour the runtime choice. engine = settings.engine if settings.engine != "auto" else runtime_config.engine or "auto" diff --git a/app/fragments/exposure.py b/app/fragments/exposure.py index 56c6bb4..d523a47 100644 --- a/app/fragments/exposure.py +++ b/app/fragments/exposure.py @@ -17,7 +17,7 @@ def exposure_banner_fragment(bind_host: str, *, is_mac: bool = False) -> str: firewall = "Keep macOS Firewall on" if is_mac else "Keep the host firewall enabled" return f"""
+ data-dismiss-key="vocagateway.exposure-dismiss-until">

@@ -57,6 +57,6 @@ def exposure_network_panel(*, is_mac: bool = False) -> str:

  • Do not publish this port on the public internet
  • Bind to a single address with - VOCAPHONE_BIND_HOST if you want to stop listening on every interface.

    + VOCAGATEWAY_BIND_HOST if you want to stop listening on every interface.

    """ diff --git a/app/fragments/pairing.py b/app/fragments/pairing.py index bc5653d..7323abd 100644 --- a/app/fragments/pairing.py +++ b/app/fragments/pairing.py @@ -45,7 +45,7 @@ def pairing_fragment(

    Pair phone

    No address the phone can reach was found. Set - VOCAPHONE_PUBLIC_URL to the URL the phone should use + VOCAGATEWAY_PUBLIC_URL to the URL the phone should use (for example http://192.168.1.20:8765), then reload.

    """ @@ -210,7 +210,7 @@ def pairing_fragment(

    Prefer LAN Wi-Fi when the phone is on the same network; use Tailscale when both are on the tailnet. - VOCAPHONE_PUBLIC_URL overrides discovery. + VOCAGATEWAY_PUBLIC_URL overrides discovery.

    """ diff --git a/app/main.py b/app/main.py index 50c63c3..7d8c9d6 100644 --- a/app/main.py +++ b/app/main.py @@ -197,7 +197,7 @@ def select_engine(settings: Settings) -> TranscriptionEngine: from app.runtime_config import VALID_ENGINES if settings.engine not in VALID_ENGINES: - raise RuntimeError("VOCAPHONE_ENGINE is not a supported engine.") + raise RuntimeError("VOCAGATEWAY_ENGINE is not a supported engine.") manager = ModelManager(settings.resolved_models_dir()) # Same resolution path as the long-running server (including sherpa-onnx # and mlx-audio). CLI tools like vocaphone-cleanup must accept every engine diff --git a/app/model_manager.py b/app/model_manager.py index de99732..0d692b4 100644 --- a/app/model_manager.py +++ b/app/model_manager.py @@ -22,7 +22,7 @@ ) CHUNK_SIZE = 1024 * 1024 -USER_AGENT = "vocaphone-gateway/0.2" +USER_AGENT = "vocagateway-gateway/0.2" HF_BASE_URL = "https://huggingface.co" @@ -276,7 +276,7 @@ async def _run_archive_download(self, model: CatalogModel, handle: _DownloadHand "language_codes": list(model.language_codes), "required_files": list(model.required_files), } - (partial_dir / ".vocaphone-model.json").write_text( + (partial_dir / ".vocagateway-model.json").write_text( json.dumps(metadata, indent=2) + "\n", encoding="utf-8" ) if handle.cancel.is_set(): @@ -299,7 +299,7 @@ async def _run_sherpa_huggingface_download( Unlike `_run_huggingface_download` (which mirrors an entire folder for engines like MLX Audio and writes no marker), this fetches only the named files a sherpa-onnx model actually needs and writes the same - `.vocaphone-model.json` marker `_run_archive_download` does, since + `.vocagateway-model.json` marker `_run_archive_download` does, since some model families (GigaAM, Canary) ship as bare Hugging Face repos with no pre-packaged `.tar.bz2` release archive. """ @@ -328,7 +328,7 @@ async def _run_sherpa_huggingface_download( "language_codes": list(model.language_codes), "required_files": list(model.required_files), } - (partial_dir / ".vocaphone-model.json").write_text( + (partial_dir / ".vocagateway-model.json").write_text( json.dumps(metadata, indent=2) + "\n", encoding="utf-8" ) if handle.cancel.is_set(): @@ -364,7 +364,7 @@ async def _run_moonshine_download(self, model: CatalogModel, handle: _DownloadHa "model_path": str(relative_path), "model_arch": int(model_arch), } - (partial_dir / ".vocaphone-model.json").write_text( + (partial_dir / ".vocagateway-model.json").write_text( json.dumps(metadata, indent=2) + "\n", encoding="utf-8" ) handle.state.downloaded_bytes = _directory_size(partial_dir) diff --git a/app/models/moonshine.py b/app/models/moonshine.py index 7eff5ac..08110da 100644 --- a/app/models/moonshine.py +++ b/app/models/moonshine.py @@ -16,7 +16,7 @@ ) from app.models.base import EngineHealth, EngineTranscription, TranscriptionOptions -MODEL_METADATA = ".vocaphone-model.json" +MODEL_METADATA = ".vocagateway-model.json" STREAMING_ARCHITECTURES = {2, 3, 4, 5} LANGUAGE_ALIASES = { "ar": {"ar", "ar-SA"}, diff --git a/app/models/sherpa_onnx.py b/app/models/sherpa_onnx.py index d132777..89526d7 100644 --- a/app/models/sherpa_onnx.py +++ b/app/models/sherpa_onnx.py @@ -18,7 +18,7 @@ ) from app.models.base import EngineHealth, EngineTranscription, TranscriptionOptions -MODEL_METADATA = ".vocaphone-model.json" +MODEL_METADATA = ".vocagateway-model.json" STREAMING_MODEL_TYPE = "streaming_zipformer" diff --git a/app/pairing.py b/app/pairing.py index e43fec6..265c947 100644 --- a/app/pairing.py +++ b/app/pairing.py @@ -135,11 +135,11 @@ def discover_gateway_base_urls(port: int) -> list[str]: """Return phone-reachable gateway bases, preferred first. Loopback is omitted: a phone cannot use 127.0.0.1 on the gateway host. - Override with VOCAPHONE_PUBLIC_URL or VOCAPHONE_PAIRING_URL when automatic + Override with VOCAGATEWAY_PUBLIC_URL or VOCAGATEWAY_PAIRING_URL when automatic discovery is wrong (multiple NICs, reverse proxy, etc.). """ overrides: list[str] = [] - for key in ("VOCAPHONE_PUBLIC_URL", "VOCAPHONE_PAIRING_URL"): + for key in ("VOCAGATEWAY_PUBLIC_URL", "VOCAGATEWAY_PAIRING_URL"): value = os.environ.get(key, "").strip() if value: try: diff --git a/app/pairing_view.py b/app/pairing_view.py index 9fea18f..792575a 100644 --- a/app/pairing_view.py +++ b/app/pairing_view.py @@ -171,6 +171,7 @@ def resolve_pairing_url(ctx: GatewayContext, url: str | None) -> str: raise APIProblem( 503, "pairing_unavailable", - "No phone-reachable gateway address was detected. Set VOCAPHONE_PUBLIC_URL and retry.", + "No phone-reachable gateway address was detected. " + "Set VOCAGATEWAY_PUBLIC_URL and retry.", ) return candidates[0] diff --git a/app/routes/admin_status.py b/app/routes/admin_status.py index 31fcfac..b8643e1 100644 --- a/app/routes/admin_status.py +++ b/app/routes/admin_status.py @@ -26,7 +26,7 @@ async def get_admin_diagnostics( response: Response, ctx: GatewayContext = Depends(get_context) ) -> DiagnosticsBundle: bundle = build_diagnostics_bundle(await status_payload(ctx), config_response(ctx)) - filename = f"vocaphone-diagnostics-{bundle.generated_at:%Y%m%dT%H%M%SZ}.json" + filename = f"vocagateway-diagnostics-{bundle.generated_at:%Y%m%dT%H%M%SZ}.json" response.headers["Content-Disposition"] = f'attachment; filename="{filename}"' return bundle diff --git a/app/routes/admin_tokens.py b/app/routes/admin_tokens.py index 1938a66..b9576d3 100644 --- a/app/routes/admin_tokens.py +++ b/app/routes/admin_tokens.py @@ -46,7 +46,7 @@ async def revoke_admin_token( raise APIProblem( 409, "bootstrap_token_not_revocable", - "Rotate VOCAPHONE_TOKEN or its token file instead of revoking it here.", + "Rotate VOCAGATEWAY_TOKEN or its token file instead of revoking it here.", ) revoked = ctx.token_store.revoke(token_id) if not revoked: @@ -62,7 +62,7 @@ async def rotate_admin_token( raise APIProblem( 409, "bootstrap_token_not_rotatable", - "Rotate VOCAPHONE_TOKEN or its token file instead of rotating it here.", + "Rotate VOCAGATEWAY_TOKEN or its token file instead of rotating it here.", ) rotated = ctx.token_store.rotate(token_id) if rotated is None: @@ -95,7 +95,7 @@ async def ui_revoke_token( raise APIProblem( 409, "bootstrap_token_not_revocable", - "Rotate VOCAPHONE_TOKEN or its token file instead of revoking it here.", + "Rotate VOCAGATEWAY_TOKEN or its token file instead of revoking it here.", ) ctx.token_store.revoke(token_id) return HTMLResponse(tokens_fragment_str(ctx)) @@ -109,7 +109,7 @@ async def ui_rotate_token( raise APIProblem( 409, "bootstrap_token_not_rotatable", - "Rotate VOCAPHONE_TOKEN or its token file instead of rotating it here.", + "Rotate VOCAGATEWAY_TOKEN or its token file instead of rotating it here.", ) rotated = ctx.token_store.rotate(token_id) if rotated is None: diff --git a/app/tokens.py b/app/tokens.py index 76f62aa..9e46591 100644 --- a/app/tokens.py +++ b/app/tokens.py @@ -1,6 +1,6 @@ """Named, individually revocable bearer tokens for additional paired devices. -`Settings.token` (from `VOCAPHONE_TOKEN` / the token file) remains a permanent +`Settings.token` (from `VOCAGATEWAY_TOKEN` / the token file) remains a permanent bootstrap credential managed outside this store — whoever controls that file or environment variable can always read/rotate it directly, so trying to revoke it through the API would be theatre. Everything issued through diff --git a/app/webui/app.js b/app/webui/app.js index 5744337..78bebd2 100644 --- a/app/webui/app.js +++ b/app/webui/app.js @@ -1,10 +1,10 @@ -/* vocaphone WebUI glue: token auth for HTMX, tab styling, mic recorder, toasts. */ +/* VocaGateway WebUI glue: token auth for HTMX, tab styling, mic recorder, toasts. */ (() => { "use strict"; - const TOKEN_KEY = "vocaphone.token"; - const THEME_KEY = "vocaphone.theme"; - const EXPOSURE_DISMISS_KEY = "vocaphone.exposure-dismiss-until"; + const TOKEN_KEY = "vocagateway.token"; + const THEME_KEY = "vocagateway.theme"; + const EXPOSURE_DISMISS_KEY = "vocagateway.exposure-dismiss-until"; const EXPOSURE_DISMISS_MS = 24 * 60 * 60 * 1000; const overlay = document.getElementById("token-overlay"); const tokenInput = document.getElementById("token-input"); @@ -316,7 +316,7 @@ return filters; } - const FILTER_RAIL_KEY = "vocaphone.models-filter-collapsed"; + const FILTER_RAIL_KEY = "vocagateway.models-filter-collapsed"; function isFilterRailCollapsed() { try { @@ -801,7 +801,7 @@ const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = `vocaphone-diagnostics-${payload.generated_at.replace(/[:.]/g, "-")}.json`; + link.download = `vocagateway-diagnostics-${payload.generated_at.replace(/[:.]/g, "-")}.json`; document.body.appendChild(link); link.click(); link.remove(); diff --git a/app/webui/index.html b/app/webui/index.html index 956d4e0..5bc4d52 100644 --- a/app/webui/index.html +++ b/app/webui/index.html @@ -34,7 +34,7 @@