:80 {
	encode zstd gzip

	root * /srv

	# SPA shell and runtime config are mutable — always revalidate.
	# (env-config.js is regenerated by docker-entrypoint.sh at container start.)
	@mutable path /index.html /env-config.js
	handle @mutable {
		header Cache-Control "no-cache"
		file_server
	}

	# Vite content-hashes everything under /assets, so cache forever.
	# The @found file gate keeps cache headers off 404s — a transient miss
	# during a deploy must not become a year-long cached 404 in CF/browsers.
	@hashed path /assets/*
	handle @hashed {
		@found file
		header @found Cache-Control "public, max-age=31536000, immutable"
		file_server
	}

	# Un-hashed static content: hardware/icon images change ~never (new files
	# get new names); /generated/itm/itm.js is the ITM wasm worker at a fixed
	# URL, replaced only on image rebuilds — cap staleness at a week.
	@images path /images/*
	handle @images {
		@found file
		header @found Cache-Control "public, max-age=2592000"
		file_server
	}
	@generated path /generated/*
	handle @generated {
		@found file
		header @found Cache-Control "public, max-age=604800"
		file_server
	}

	# Real assets 404 cleanly instead of falling through to index.html.
	# Without this the SPA fallback returns HTML with 200, which CDNs (CF)
	# then cache as a "successful" text/html response for the asset URL.
	@assets path *.js *.css *.wasm *.map *.json *.png *.jpg *.jpeg *.svg *.webp *.ico *.woff *.woff2 *.ttf *.otf
	handle @assets {
		file_server
	}

	# SPA routes — fall back to index.html for client-side routing.
	# The rewritten index.html must not be heuristically cached either.
	handle {
		header Cache-Control "no-cache"
		try_files {path} /index.html
		file_server
	}
}
