# ipx-web (#203): the frontend as static files, and everything else passed to ipx, on one origin,
# so the session cookie, live updates and Cloudflare Access work as they did when ipx served it.
#   IPX_UPSTREAM  where ipx is, host:port            (ipx:8099)
#   WEB_BIND      where to listen                    (:8080)
#   WEB_TRACING   on: a span for each request to OpenTelemetry (OTEL_EXPORTER_OTLP_ENDPOINT,
#                 OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME), continuing the page's traceparent
#                 and passing it to ipx, so one trace runs browser, Caddy, ipx (#213). Off: none,
#                 and no export failing every few seconds where there is nothing to send to.
#   TUNNEL_PEER   the one address whose Cf-Access-* headers are passed on: where cloudflared's
#                 requests come from. Nobody else's are, so nobody else can claim to be signed in
#                 by Cloudflare. Unset passes none.
{
	admin off
	auto_https off
	# Everything else Caddy logs, its errors among them, which carry the request too: the same
	# filter as the access log's below.
	log default {
		output stderr
		format filter {
			wrap json
			fields {
				request>uri query {
					delete token
				}
				request>headers delete
			}
		}
	}
}

(tracing-on) {
	tracing {
		span ipx-web
	}
}
(tracing-off) {
}

http://{$WEB_BIND::8080} {
	import tracing-{$WEB_TRACING:off}

	root * {$WEB_ROOT:/srv}

	# One JSON line a request on stdout, for Loki and the dashboard's Page row (#210). Not the
	# shared token a ?token= link carries, nor the request's headers, which hold the session
	# cookie and Cloudflare Access's token.
	log {
		output stdout
		format filter {
			wrap json
			fields {
				request>uri query {
					delete token
				}
				request>headers delete
			}
		}
	}

	# Compressed, zstd or gzip as the browser takes them: the feed list and a page of items went
	# out at over 100 KB each (#239). Not the live updates, which a compressor would hold back
	# until it had enough to send, nor /media, which is audio and video already compressed.
	@compress not path /api/events /media/*
	encode @compress zstd gzip

	# The copies web/build.mjs compressed are served in place of their file, never asked for by
	# name: /admin.js.br would be the admin page's script without the check below that keeps it to
	# admins (#253).
	# A handle of its own: a bare respond runs after the handles, and the last one serves any file.
	@packed path *.br *.gz
	handle @packed {
		respond 404
	}

	# Before anything else, so no page or API call sees them: only ipx sets these.
	@forged not remote_ip {$TUNNEL_PEER:255.255.255.255}
	request_header @forged -Cf-Access-Authenticated-User-Email
	request_header @forged -Cf-Access-Jwt-Assertion

	@api path /api/* /media/*
	handle @api {
		# Live updates are a stream: flushed as they come, never buffered.
		reverse_proxy {$IPX_UPSTREAM:ipx:8099} {
			flush_interval -1
		}
	}

	# Signing in with the shared token in the address (?token=): ipx sets the session cookie and
	# sends the browser on to the page without it.
	@tokened {
		path / /admin
		query token=*
	}
	handle @tokened {
		rewrite * /api/signin?{query}
		reverse_proxy {$IPX_UPSTREAM:ipx:8099}
	}

	# The app and the admin page go to whoever ipx says may have them: a browser not signed in is
	# sent to /login, anyone but an admin asking for /admin to the app.
	@page path / /index.html /admin /admin.html /admin.js
	handle @page {
		forward_auth {$IPX_UPSTREAM:ipx:8099} {
			uri /api/page-check
		}
		rewrite / /index.html
		rewrite /admin /admin.html
		header Cache-Control no-cache
		file_server {
			precompressed br gzip
		}
	}

	# The sign-in form, except for someone Cloudflare Access vouches for, who is sent to the app.
	@login path /login /login.html
	handle @login {
		forward_auth {$IPX_UPSTREAM:ipx:8099} {
			uri /api/login-check
		}
		rewrite * /login.html
		header Cache-Control no-cache
		file_server {
			precompressed br gzip
		}
	}

	# Scripts and the stylesheet are named by a hash of their contents (web/build.mjs), so a browser
	# keeps them a year; the icons a day, under names that do not change.
	@hashed path /app.js /admin.js /login.js /app.css /hls.js /boot.js /icons/index.json
	header @hashed Cache-Control "public, max-age=31536000, immutable"
	# The same PNG as /favicon.png, for a browser that asks for /favicon.ico on its own.
	header /favicon.ico Content-Type image/png
	@icons path *.png *.svg *.ico *.woff2
	header @icons Cache-Control "public, max-age=86400"
	handle {
		file_server {
			precompressed br gzip
		}
	}
}
