#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
if [ ! -d web ]; then
flutter create . --platforms web
fi
VERSION="$(awk '/^version:/ {print $2; exit}' pubspec.yaml)"
RELEASE_ID="${VERSION//+/-}"
RELEASE_PATH="releases/${RELEASE_ID}/"
printf '{\n "version": "%s",\n "release_id": "%s",\n "release_path": "%s",\n "cache_policy": "always-current-launcher-versioned-runtime"\n}\n' \
"$VERSION" "$RELEASE_ID" "$RELEASE_PATH" > web/version.json
echo "Building MeinWeimar web $VERSION …"
echo "Runtime release path: $RELEASE_PATH"
# No Flutter PWA worker. Runtime files are isolated by release path.
flutter build web --release --pwa-strategy=none
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
cp -a build/web/. "$TMP_DIR/"
rm -rf build/web
mkdir -p "build/web/$RELEASE_PATH"
cp -a "$TMP_DIR/." "build/web/$RELEASE_PATH"
# ---------------------------------------------------------------------------
# ALWAYS-CURRENT ROOT LAUNCHER
# ---------------------------------------------------------------------------
# index.html is intentionally a tiny stable trampoline. Even if a browser keeps
# an old copy of it, every invocation jumps to index.php with a unique query
# parameter. PHP then emits explicit no-store headers before serving the real
# launcher. This avoids depending on nginx/.htaccess cache behavior.
cat > build/web/index.html <<'HTML'
<!doctype html>
<html><head>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta name="robots" content="noindex">
<title>MeinWeimar TOURGUIDE</title>
<script>
(function () {
var target = new URL('index.php', window.location.href);
var current = new URL(window.location.href);
current.searchParams.forEach(function (value, key) {
if (key !== 'mwentry') target.searchParams.set(key, value);
});
target.searchParams.set('mwentry', Date.now().toString());
target.hash = current.hash;
window.location.replace(target.toString());
})();
</script>
</head><body></body></html>
HTML
# Real production launcher, served by PHP with headers that nginx cannot turn
# into a stale static HTML response. The HTML itself is Flutter's generated
# launcher, including MeinWeimar startup hardening.
{
cat <<'PHP'
<?php
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
header('X-MeinWeimar-Launcher: always-current');
?>
PHP
cat "$TMP_DIR/index.html"
} > build/web/index.php
# Authoritative release metadata via PHP = explicit no-store response headers.
cat > build/web/version.php <<PHP
<?php
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
header('X-MeinWeimar-Version: ${VERSION}');
echo json_encode([
'version' => '${VERSION}',
'release_id' => '${RELEASE_ID}',
'release_path' => '${RELEASE_PATH}',
'cache_policy' => 'always-current-launcher-versioned-runtime',
], JSON_UNESCAPED_SLASHES);
PHP
# Static JSON remains for compatibility with older MeinWeimar launchers.
cp web/version.json build/web/version.json
cp web/update.html build/web/update.html
for item in manifest.json favicon.png icons; do
if [ -e "$TMP_DIR/$item" ]; then
cp -a "$TMP_DIR/$item" build/web/
fi
done
# Legacy Flutter service-worker killer. Old Flutter Web builds registered this
# exact URL. Browsers periodically check it for updates; once this script wins,
# it discards old Cache Storage and unregisters itself.
cat > build/web/flutter_service_worker.js <<'JS'
self.addEventListener('install', function () {
self.skipWaiting();
});
self.addEventListener('activate', function (event) {
event.waitUntil((async function () {
try {
var keys = await caches.keys();
await Promise.all(keys.map(function (key) { return caches.delete(key); }));
} catch (_) {}
try { await self.clients.claim(); } catch (_) {}
try { await self.registration.unregister(); } catch (_) {}
try {
var clients = await self.clients.matchAll({type: 'window', includeUncontrolled: true});
clients.forEach(function (client) { client.postMessage({type: 'MW_LEGACY_SW_REMOVED'}); });
} catch (_) {}
})());
});
self.addEventListener('fetch', function () {
// Deliberately no fetch interception. Network/browser cache handles assets.
});
JS
# Also cover a generic historical service-worker filename if one was ever used.
cp build/web/flutter_service_worker.js build/web/service-worker.js
# Optional Apache hardening for hosts that honor it. Correctness no longer
# depends on these rules because index.php/version.php emit their own headers.
cat > build/web/.htaccess <<'HT'
<IfModule mod_headers.c>
<FilesMatch "^(index\.html|index\.php|version\.json|version\.php|flutter_service_worker\.js|service-worker\.js)$">
Header always set Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
Header always set Pragma "no-cache"
Header always set Expires "0"
</FilesMatch>
<FilesMatch "\.(?:js|mjs|json|wasm|woff2?|ttf|otf|png|jpe?g|webp|svg|ico)$">
Header set Cache-Control "public, max-age=31536000, immutable" "expr=%{REQUEST_URI} =~ m#^/releases/#"
</FilesMatch>
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
HT
# Sanity checks.
test -f "build/web/$RELEASE_PATH/flutter_bootstrap.js"
test -f "build/web/$RELEASE_PATH/main.dart.js"
test -d "build/web/$RELEASE_PATH/assets"
test -f build/web/index.html
test -f build/web/index.php
test -f build/web/version.php
test -f build/web/flutter_service_worker.js
test ! -f build/web/main.dart.js
echo
echo "✓ build/web ready for COMPLETE upload"
echo " Version: $VERSION"
echo " Release path: $RELEASE_PATH"
echo " Root: stable index.html -> no-store index.php"
echo " Metadata: no-store version.php (+ version.json legacy fallback)"
echo " Legacy SW: active killer/unregister script"
echo
echo "Deploy the COMPLETE build/web directory."
echo "Normal reload path: / -> index.php?mwentry=<unique> -> version.php?cacheBust=<unique> -> $RELEASE_PATH"
echo "No user cache clearing is required for current MeinWeimar launchers."

