Commit Graph

64 Commits

Author SHA1 Message Date
Paul Payne
1f3fcf50b4 Advanced pages. Model-driven controls. 2026-07-11 02:31:14 +00:00
Paul Payne
cd2f28df34 Remove dead CentralState types (replaced by resource-specific types) 2026-07-10 22:48:40 +00:00
Paul Payne
79f38d2750 Replace state blob with resource-oriented API endpoints
Split /api/v1/state into dedicated resource endpoints:
- /api/v1/operator, /api/v1/central-domain
- /api/v1/ddns/config, /api/v1/dns/settings
- /api/v1/dhcp/config (moved from /dnsmasq/dhcp/)
- /api/v1/nftables/config, /api/v1/haproxy/routes

Each page now talks to its own resource instead of deep-merging
a blob. Removes useConfig hook, CentralState type, and legacy
config methods.
2026-07-10 22:45:36 +00:00
Paul Payne
79c0c32b98 services -> domains 2026-07-10 20:46:22 +00:00
Paul Payne
3c99d26830 Auto-detect Central domain for CORS allowed origins
Read the configured Central domain from state.yaml and add it as an
HTTPS origin so the web UI at e.g. https://central.payne.io works
without manually setting WILD_CENTRAL_CORS_ORIGINS.
2026-07-10 19:12:27 +00:00
Paul Payne
6aa1e7d438 Resilient HAProxy config: isolate broken services on validation failure
When haproxy -c fails, parse ALERT line numbers, map to service domains
via # service: comments in generated config, exclude broken services,
and regenerate. One retry — prevents a single bad registration from
blocking all config updates.
2026-07-10 07:00:27 +00:00
Paul Payne
68d6fde80d Show routes, TLS certs, and port-forwarding on services page
- Add Routes model to services UI (paths, headers, IP whitelisting per route)
- Show TLS cert info per service with inline provision/renew actions
- Remove TLS Certificates section from dashboard (now on services page)
- Make gateway router port list dynamic from config + VPN state
- Add TODO for header validation in HAProxy config generation
2026-07-10 06:10:40 +00:00
Paul Payne
e78a1d548a feat: Add instructions for registering new CrowdSec agents 2026-07-10 05:22:01 +00:00
Paul Payne
43253ca120 Support routes. 2026-07-10 05:21:43 +00:00
Paul Payne
5c26c7530a It's state, not config. 2026-07-10 05:21:03 +00:00
Paul Payne
4feaa63da0 refactor: Replace 'reach' field with 'public' boolean in service registration and related components 2026-07-10 02:13:37 +00:00
Paul Payne
fffc84e14c refactor: Remove routing summary — controls already communicate it
The backend field + Public toggle already tell you where traffic
routes. Removed redundant "Routes to X on LAN, Y externally" line
and unused ddns/publicIp/backendHost vars.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-10 00:17:52 +00:00
Paul Payne
e1582edfec refactor: Remove redundant status table from service cards
The toggles (Public, Subdomains, Central TLS) already communicate
everything the table was restating. Replaced with a single routing
summary line: "Routes to X on LAN, Y externally."

Cleaner, no redundancy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:58:01 +00:00
Paul Payne
7c9e8e51d7 feat: All service fields editable inline
Service cards now have full inline editing:
- Backend address: editable input, updates on blur
- Public/Private: toggle switch, updates immediately
- Subdomains: toggle switch, updates immediately
- TLS (Central TLS / Passthrough): toggle switch, updates
  both tls mode and backend type together
- Deregister button

Add form also uses toggle for TLS instead of dropdown.
Removed unused Badge and Select imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:53:20 +00:00
Paul Payne
98385449b4 feat: Service cards with inline toggles + updated docs
Service cards now have:
- Public/Private toggle (switches reach)
- Subdomains toggle (include *.domain)
- TLS badge (passthrough vs terminate, read-only for now)
- Inline status details (DNS, Proxy, TLS status)
- Deregister button
- Add Service form with toggles instead of dropdowns

The card speaks the user's language (public/private, subdomains on/off)
not implementation details (tcp-passthrough vs http, reach: internal).

Also updated docs/registrations.md:
- Added "User-facing concepts" section mapping API fields to toggles
- Added batch deregister endpoint
- Added backend.health field
- Cleaner examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:50:05 +00:00
Paul Payne
c9732ffa7f style: Move VPN above Firewall in sidebar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:42:34 +00:00
Paul Payne
b0a3e2f1f1 fix: Allow multiple service cards expanded simultaneously
Changed from single expandedDomain string to a Set<string> so
multiple cards can be open at once.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:38:02 +00:00
Paul Payne
c96af69189 fix: TLS status dot colors — grey/red/green
- na (passthrough, Central doesn't handle TLS): grey checkmark
- error (terminate but cert missing): red alert icon
- ok (terminate and cert exists): green checkmark

Previously passthrough showed green (wrong — Central isn't doing
anything) and missing certs showed amber (should be red — broken).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:36:47 +00:00
Paul Payne
a4fbecc61e feat: Services page — individual cards, full CRUD, no custom TCP
Rewrote ServicesComponent:
- Each service is its own Card (not rows in a table)
- "Add Service" button with form (domain, backend, type, reach, subdomains)
- Deregister button per service
- Wildcard services show as *.domain
- Expandable detail view with DNS/Proxy/TLS status
- Removed separate "Custom TCP Routes" section — TCP routes are just
  services with type tcp-passthrough
- Added register/deregister methods to servicesApi

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 23:34:24 +00:00
Paul Payne
7d703832ab fix: Add token editing, DDNS record management, wildcard display
Dashboard:
- Cloudflare token can be changed when already configured (was only
  editable when missing/invalid)
- DDNS records can be added and removed inline (was read-only)

Services:
- Wildcard services display as *.domain instead of domain with a
  separate subdomains badge — clearer at a glance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 22:45:17 +00:00
Paul Payne
9ce9999d5c feat: UI refactor — Dashboard + Services, 9 items → 6
Major restructure of Wild Central's web UI:

New pages:
- Dashboard: infrastructure health (Cloudflare token, DDNS sync,
  daemon status, gateway router guidance, cert warnings)
- Services: every registered service with expandable rows showing
  DNS, proxy, TLS, DDNS status. Custom TCP routes section.
  Advanced HAProxy management collapsed at bottom.

Sidebar restructured into two groups:
- Services: Dashboard, Services
- Central: Firewall, VPN, CrowdSec, DHCP

Deleted 7 pages + 7 components (2501 lines removed):
- CentralPage, CloudflarePage, DdnsPage, DnsPage, LanDnsPage,
  CertificatesPage, IngressProxyPage
- CentralComponent, CloudflareComponent, DdnsComponent,
  DnsComponent, LanDnsComponent, IngressProxyComponent,
  DnsmasqSection

All hooks and API services kept unchanged — reused in new components.
Type-check and production build pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 22:36:54 +00:00
Paul Payne
c18217944f feat: Add EnsureCentralService + improved tests
Linter-contributed improvements:
- EnsureCentralService() method: registers Central's own domain as a
  service from config, with cleanup on domain change
- Tests for EnsureCentralService: registration, idempotency, domain
  change cleanup, no-domain-is-noop
- Improved reconciliation tests with Central as a registered service
- Certbot handler cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 22:26:56 +00:00
Paul Payne
2e6fce54e3 docs: Final UI refactor — Dashboard + Services, 6 items
Dashboard: infrastructure health (router, cloudflare, DDNS, daemons).
Services: every registered service with full status (DNS, proxy, TLS,
DDNS) in expandable rows + custom TCP routes section.

9 sidebar items → 6. Seven old pages eliminated — their content lives
in the two new pages where it belongs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 22:16:31 +00:00
Paul Payne
0646abb9af docs: Revised UI refactor — services as the entrypoint
Complete rewrite of the UI refactor plan based on the insight that
services are the entrypoint for everything. DNS, certs, proxy routes
are effects of service registrations, not independent config surfaces.

Two sidebar sections: Services (outward-facing, service-driven) and
Central (inward-facing, device config). Each service page answers
"what did Central do because of registered services?"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 22:00:58 +00:00
Paul Payne
a9249e32d8 docs: UI refactor plan — information architecture review
Audit of current Wild Central UI navigation and page structure.
Identifies 5 problems (DNS triple redundancy, scattered Cloudflare,
jargon naming, arbitrary ordering, empty overview) and proposes a
restructured 9-item navigation with better grouping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 21:46:35 +00:00
Paul Payne
234a8f8462 test: Add 22 unit tests across dnsmasq, haproxy, services
dnsmasq (6 new, 30.8% → 32%):
- Empty instances, multiple instances, nil config, configured IP
- No leaked empty entries (address=//, local=//)
- Empty fields don't produce broken entries

Also fixed a bug: empty Domain/InternalDomain fields in commented-out
entries (no LB IP case) now guarded against producing # local=// and
# address=// directives.

haproxy (7 new):
- No HTTPS frontend with no routes
- HTTP-only and L4-only route scenarios
- CertsDir customization and default
- Custom TCP route ports
- Health check directive in L7 backends

services (8 new, 72.7% → 75.8%):
- Not-found errors for Get, Deregister, Update
- DeregisterBySource partial match (source AND backend)
- Overwrite preserves file count
- Empty dir and non-YAML file handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 21:07:12 +00:00
Paul Payne
112f7c7f55 test: Verify Central backend uses configured port, not default
The reconciliation test for Central domain now verifies the HAProxy
backend uses port 15055 (the configured port) and NOT port 5055
(the default). This catches the bug where Reconcile() was called
before SetPort().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:59:42 +00:00
Paul Payne
995d2cd7da fix: Run Reconcile after SetPort so Central routes to correct port
Reconcile() was called before SetPort(), so getRunningPort() returned
the default 5055 (Wild Cloud's port) instead of 15055. Moved
Reconcile() after SetPort(port) so Central's config-driven HAProxy
route points to the actual running port.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:57:59 +00:00
Paul Payne
e5cd6583f2 feat: Add comprehensive tests + fix startup reconciliation
Tests added (13 new):
- HAProxy ACL ordering: L7 before L4 wildcard, L4 exact before L4 wildcard
- HAProxy subdomains: false=exact only, true=wildcard+exact
- Reconciliation integration: HAProxy config from services, DNS config
  from services, Central config domain injection, correct DNS target IPs
- dnsmasq: reach:internal has local=/, reach:public does not
- Services: idempotent registration, no duplicates in list

Bug fix:
- Run initial reconciliation on startup so Central's own domain (from
  config) gets HAProxy + DNS routes even with zero registered services.
  Previously, Central only got routes after the first service was
  registered, causing 503 on fresh restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:55:27 +00:00
Paul Payne
1fca2b8032 fix: Pass Subdomains field through to HAProxy InstanceRoute
The reconciliation was building InstanceRoutes without setting the
Subdomains field, so all services were exact-match only — breaking
wildcard subdomain routing for *.cloud.payne.io and *.cloud2.payne.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:45:25 +00:00
Paul Payne
56b7dd800d feat: Update web UI for domain-keyed service model
- RegisteredService: removed name, added subdomains bool
- CertEntry: removed service field (domain is the key)
- Ingress page: domain as primary label, subdomains badge
- Certificates page: source shown under domain
- All type-check passes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:39:38 +00:00
Paul Payne
9de2311357 refactor: Remove RegisterSelf — Central domain from config
Central no longer self-registers as a service. Its own domain comes
from config (cloud.central.domain) and is injected directly into
reconciliation as an HTTPRoute. This is cleaner — Central's own
services are config-driven, not registration-driven.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:30:34 +00:00
Paul Payne
548be903e7 feat: Fix HAProxy ACL ordering + Subdomains field
Rewrite HAProxy config generation with correct ACL ordering:
1. L7 HTTP exact matches first (central, wild-cloud app)
2. L4 exact matches (payne.io, civilsociety.dev)
3. L4 wildcard matches (*.cloud.payne.io) — only when subdomains:true
4. Default → L7 termination

Key changes:
- InstanceRoute: removed ExtraDomains, added Subdomains bool
- GenerateOpts: removed CentralDomain (Central is now just another
  HTTPRoute injected by reconciliation), renamed WildcardCert→CertsDir
- Central's domain comes from config, injected as HTTPRoute with
  the actual running port (no more hardcoded 5055)
- Added API.SetPort() so reconciliation knows the running port
- subdomains:false → exact match only (no *.domain shadowing)

New tests: TestGenerate_WithSubdomains, TestGenerate_ACLOrdering
(verifies L7 exact matches come before L4 wildcards).

Fixes: BUG 1 (central routing to wrong port), BUG 2 (wild-cloud
getting traefik cert), BUG 3 (*.payne.io too broad).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:29:30 +00:00
Paul Payne
8874bc6281 feat: Domain-keyed service registration model
Rewrite the service registration model per the architecture plan:

Model changes:
- Domain is the unique key (removed Name field)
- Removed ExtraDomains — each domain is its own registration
- Removed SourceNode, BackendStatic, ReachOff
- Added Subdomains bool for wildcard matching control
- Files stored as <domain_with_underscores>.yaml
- API routes: /services/{domain:.+} (regex for dots in path)
- Added DeregisterBySource for cleanup before re-registration

Reconciliation changes:
- No ExtraDomains iteration — each service IS one domain
- reach:internal → sets InternalDomain (generates local=/)
- reach:public → sets Domain (no local=/)
- tcp-passthrough → DNS points to backend IP
- http → DNS points to Central IP

All 22 tests pass (9 manager + 8 handler + 5 config/cert).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:23:11 +00:00
Paul Payne
d009e095c0 docs: Add README and service registration reference
- README.md: project overview, two sources of truth (config vs
  registrations), quick start, development, architecture
- docs/registrations.md: complete API reference for service
  registrations — fields, types, defaults, what Central does
  per combination, examples for each use case

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 20:11:23 +00:00
Paul Payne
170500e173 fix: Use configured dnsmasq IP instead of auto-detect
The auto-detect picks the default route interface (wlan0 at
192.168.8.152) but dnsmasq is bound to eth0 (192.168.8.151).
Now uses cloud.dnsmasq.ip from config as the Central IP for DNS
entries, falling back to auto-detect only if not configured.

Fixes DNS resolution failures when dnsmasq config points services
to the wrong interface IP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 19:24:13 +00:00
Paul Payne
900d25bab2 fix: Reload dnsmasq instead of restart to avoid DNS downtime
Use systemctl reload-or-restart instead of restart. dnsmasq supports
SIGHUP to reload config without dropping DNS service, preventing
transient DNS resolution failures during reconciliation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 15:54:34 +00:00
Paul Payne
ed0691d56f feat: Split DNS with correct IPs per service type
Fix dnsmasq reconciliation to use the correct DNS target IP:
- tcp-passthrough (k8s instances): DNS → k8s LB IP (LAN direct)
- http/static (Central apps): DNS → Central IP (HAProxy terminates)

Also handle ExtraDomains properly:
- Internal domains (internal.cloud.payne.io) get local=/ entries
  to prevent upstream DNS forwarding
- Extra domains get their own dnsmasq address= entries
- HAProxy already propagated ExtraDomains for SNI ACLs

Verified: cloud.payne.io → 192.168.8.240 (direct to k8s),
cloud2.payne.io → 192.168.8.80, central.payne.io → 192.168.8.151.
SNI routing through HAProxy works for both instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 15:46:35 +00:00
Paul Payne
7e8f660d11 feat: Per-service certs with opt-in wildcard provisioning
Remove automatic wildcard cert assumption. Each service gets its own
cert by default. Wildcards are user-initiated via the Certificates page.

Backend:
- HAProxy L7 frontend uses cert directory (/etc/haproxy/certs/)
  instead of single wildcard file — loads all PEMs, serves by SNI
- Cert status API shows every registered service individually with
  coveredBy field when a wildcard cert covers the domain
- Reconciliation filters L7 routes to services that have a cert
- No auto-provisioning in reconciliation (just warnings)

Frontend:
- Certificates page shows per-service cert status
- "Provision" button for individual certs
- "Add Wildcard" form for opt-in wildcard provisioning
- Fixed CloudflareComponent type errors from cert API changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:47:00 +00:00
Paul Payne
447c7e51a3 feat: Add Certificates page + stop auto-provisioning certs
New dedicated Certificates page in Wild Central UI showing:
- All tracked certs (central, wildcard, per-service)
- Status (valid with days remaining, or missing)
- Per-domain Provision button
- Renew All button

Cert provisioning is now user-initiated only — reconciliation logs
warnings about missing certs but does NOT auto-provision. This
prevents unexpected wildcard cert requests on startup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:26:48 +00:00
Paul Payne
9eea061e9b feat: Enhanced cert status API + TLS certificates UI
API changes:
- /api/v1/cert/status now returns all relevant certs (central,
  wildcard, per-service) with existence status and expiry
- /api/v1/cert/provision accepts ?domain= param for per-domain
  provisioning
- Removed stale syncHAProxy references, uses reconcileNetworking

UI changes:
- Added TLS Certificates card to Cloudflare page showing cert status
  for each domain with provision/renew actions
- Updated useCert hook and cert API types for new response format

Next: extract into dedicated Certificates page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:23:08 +00:00
Paul Payne
7d5e4f4e29 fix: Skip L7 routes and central TLS in HAProxy when certs don't exist
HAProxy validation fails if the config references cert files that
don't exist (wildcard.pem for L7 routes, domain.pem for central TLS).

Now reconciliation checks for cert existence before including:
- L7 HTTP routes: only generated when wildcard cert exists
- Central domain TLS frontend: only generated when domain cert exists

This ensures HAProxy never enters a failure state due to missing
certs. Once certs are provisioned (via certbot), the next
reconciliation picks them up automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:15:32 +00:00
Paul Payne
53a640c34f fix: Auto-recover NATS JetStream from stale store on startup
If the NATS server fails to start (stale lock files, corrupt store
from a crash), automatically clean the data directory and retry once.
This prevents the frustrating "NATS server failed to start" error
after unclean shutdowns.

The NATS KV data (registered services) is also stored as YAML files
in the services directory, so cleaning the NATS store loses nothing
— services are re-registered on the next startup or API call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:12:29 +00:00
Paul Payne
e4e1a15f92 fix: Prevent broken dnsmasq entries for services without internal domain
Services registered via the service API have a domain but no internal
domain (that's a Wild Cloud instance concept). The dnsmasq generator
was producing broken entries like `local=//` and `address=//`.

Fix: skip internal domain entries when InternalDomain is empty, in
both config.go and config_modular.go. Also fix DNS entries to point
to Central's IP (where HAProxy listens) rather than the backend
address — all traffic flows through Central.

Added 3 tests covering: service-only, mixed services+instances.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 14:11:24 +00:00
Paul Payne
c7a0227e03 feat: Wire up TLS cert provisioning in reconciliation
When HTTP services are registered (where Central terminates TLS),
reconciliation now checks for and provisions TLS certificates:

- Derives gateway domain from central domain (e.g., payne.io)
- Checks for existing wildcard cert (*.payne.io)
- If no wildcard exists and Cloudflare token + operator email are
  configured, provisions one via certbot DNS-01
- For services outside the gateway domain, provisions individual certs
- Services with TLS passthrough (k8s) are skipped (backend handles TLS)
- Graceful: skips silently if no Cloudflare token or email configured

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 13:36:49 +00:00
Paul Payne
75a82becb9 feat: Show Registered Services on Ingress Proxy page
Replace "Instance Routes" with "Registered Services" on the Central
Ingress page. Services are fetched from /api/v1/services and display
name, domain, backend, source, reach level, and backend type.

Central no longer manages instances directly — it shows services
registered by Wild Cloud, Wild Works, and itself.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 05:43:32 +00:00
Paul Payne
57a597fe5a feat: Self-register Wild Central on startup
On startup, Wild Central registers its own domain (from config
cloud.central.domain) as an HTTP service with itself. This creates
the DNS entry, proxy route, and TLS cert needed to access the
Central UI via its domain name.

Registration is idempotent — safe on every restart. Skips silently
if no domain is configured. Logs a warning if registration fails.

Verified: after startup, /api/v1/services shows wild-central
registered with domain central.payne.io.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 05:40:27 +00:00
Paul Payne
909527f060 feat: Add tests for config, secrets, services, and cloudflare endpoints
Fix config endpoint to return wrapped { configured, config } response
that the web UI expects. Fix services handler to return stored version
with defaults applied.

New tests:
- GetGlobalConfig: wrapped response, empty/not-configured case
- GetGlobalSecrets: leaf redaction preserves structure, raw mode
- UpdateGlobalConfig: write and verify
- CloudflareVerify: no-token case
- Services: register, list, get, update, deregister, TCP passthrough
  defaults, validation errors

15 new test cases, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 04:45:55 +00:00
Paul Payne
d9c152f044 fix: Preserve nested structure when redacting secrets
The secrets endpoint was flattening nested maps (e.g., cloudflare.apiToken)
to a single "********" string. The UI checks for nested keys to determine
if credentials are configured, so the structure must be preserved.

Now redactSecrets() recursively walks the map and only redacts leaf values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 04:40:47 +00:00
Paul Payne
4cafe68064 fix: Use relative API URLs for browser compatibility
VITE_API_BASE_URL must be empty (relative URLs) so API calls go to
the same origin the browser loaded the page from. Setting it to
http://localhost:15055 breaks when accessing via LAN hostname since
the browser resolves localhost to its own machine, not the server.

The Go backend handles /api/v1/* routes before the Vite proxy
catch-all, so relative URLs work correctly in dev mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-09 04:39:08 +00:00