From e1655edd83893946cace9a44efb979a7357736b6 Mon Sep 17 00:00:00 2001
From: Jade Ellis <jade@ellis.link>
Date: Fri, 25 Apr 2025 02:47:48 +0100
Subject: [PATCH 1/6] feat: HTML default page

---
 .dockerignore                        |   2 +-
 .forgejo/workflows/release-image.yml |   3 +
 Cargo.lock                           |  64 ++++++++++++++++
 Cargo.toml                           |   5 ++
 docker/Dockerfile                    | 106 +++++++++++++++++++++++++--
 nix/pkgs/main/default.nix            |   2 +
 src/router/Cargo.toml                |   1 +
 src/router/router.rs                 |   6 +-
 src/web/Cargo.toml                   |  32 ++++++++
 src/web/css/index.css                |  68 +++++++++++++++++
 src/web/mod.rs                       |  61 +++++++++++++++
 src/web/templates/_layout.html.j2    |  32 ++++++++
 src/web/templates/error.html.j2      |  20 +++++
 src/web/templates/index.html.j2      |  16 ++++
 14 files changed, 408 insertions(+), 10 deletions(-)
 create mode 100644 src/web/Cargo.toml
 create mode 100644 src/web/css/index.css
 create mode 100644 src/web/mod.rs
 create mode 100644 src/web/templates/_layout.html.j2
 create mode 100644 src/web/templates/error.html.j2
 create mode 100644 src/web/templates/index.html.j2

diff --git a/.dockerignore b/.dockerignore
index 453634df..8ca2e3f8 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -11,7 +11,7 @@ docker/
 *.iml
 
 # Git folder
-.git
+# .git
 .gitea
 .gitlab
 .github
diff --git a/.forgejo/workflows/release-image.yml b/.forgejo/workflows/release-image.yml
index 141bfef9..3eb84223 100644
--- a/.forgejo/workflows/release-image.yml
+++ b/.forgejo/workflows/release-image.yml
@@ -144,6 +144,9 @@ jobs:
           file: "docker/Dockerfile"
           build-args: |
             CONDUWUIT_VERSION_EXTRA=${{ env.COMMIT_SHORT_SHA }}
+            COMMIT_SHA=${{ github.sha }})
+            REMOTE_URL=${{github.event.repository.html_url }}
+            REMOTE_COMMIT_URL=${{github.event.head_commit.url }}
           platforms: ${{ matrix.platform }}
           labels: ${{ steps.meta.outputs.labels }}
           annotations: ${{ steps.meta.outputs.annotations }}
diff --git a/Cargo.lock b/Cargo.lock
index 2d8a2d0f..1b6c5d26 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -109,6 +109,48 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a"
 
+[[package]]
+name = "askama"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
+dependencies = [
+ "askama_derive",
+ "itoa",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
+dependencies = [
+ "askama_parser",
+ "basic-toml",
+ "memchr",
+ "proc-macro2",
+ "quote",
+ "rustc-hash 2.1.1",
+ "serde",
+ "serde_derive",
+ "syn",
+]
+
+[[package]]
+name = "askama_parser"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
+dependencies = [
+ "memchr",
+ "serde",
+ "serde_derive",
+ "winnow",
+]
+
 [[package]]
 name = "assign"
 version = "1.1.1"
@@ -415,6 +457,15 @@ version = "1.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
 
+[[package]]
+name = "basic-toml"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "bindgen"
 version = "0.69.5"
@@ -904,6 +955,7 @@ dependencies = [
  "conduwuit_api",
  "conduwuit_core",
  "conduwuit_service",
+ "conduwuit_web",
  "const-str",
  "futures",
  "http",
@@ -961,6 +1013,18 @@ dependencies = [
  "webpage",
 ]
 
+[[package]]
+name = "conduwuit_web"
+version = "0.5.0-rc.5"
+dependencies = [
+ "askama",
+ "axum",
+ "futures",
+ "rand 0.8.5",
+ "thiserror 2.0.12",
+ "tracing",
+]
+
 [[package]]
 name = "console-api"
 version = "0.8.1"
diff --git a/Cargo.toml b/Cargo.toml
index 1ce5c1db..44db4c4b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -626,6 +626,11 @@ package = "conduwuit_macros"
 path = "src/macros"
 default-features = false
 
+[workspace.dependencies.conduwuit-web]
+package = "conduwuit_web"
+path = "src/web"
+default-features = false
+
 ###############################################################################
 #
 # Release profiles
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 536af632..56f2d5fa 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -111,14 +111,112 @@ RUN mkdir /out
 
 FROM toolchain AS builder
 
+
+# Get source
+COPY . .
+
 # Conduwuit version info
 ARG COMMIT_SHA=
+ARG SHORT_COMMIT_SHA=
+ARG REMOTE_URL=
 ARG CONDUWUIT_VERSION_EXTRA=
+ENV COMMIT_SHA=$COMMIT_SHA
+ENV SHORT_COMMIT_SHA=$SHORT_COMMIT_SHA
+ENV REMOTE_URL=$REMOTE_URL
 ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA
-RUN <<EOF
-if [ -z "${CONDUWUIT_VERSION_EXTRA}" ]; then
-    echo "CONDUWUIT_VERSION_EXTRA='$(set -e; git rev-parse --short ${COMMIT_SHA:-HEAD} || echo unknown revision)'" >> /etc/environment
+
+# Calculate version info from git if not provided via ARGs
+# and write all relevant vars to /etc/environment
+RUN <<'EOF'
+set -e # Exit on error
+
+# Use temp variables to store calculated values
+calculated_commit_sha=""
+calculated_remote_url=""
+calculated_version_extra=""
+
+# --- COMMIT_SHA ---
+# Calculate COMMIT_SHA if ENV var (from ARG) is empty
+if [ -z "${COMMIT_SHA}" ]; then
+    # Try to get short commit hash from git
+    calculated_commit_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
+    if [ -n "${calculated_commit_sha}" ]; then
+        echo "COMMIT_SHA='${calculated_commit_sha}'" >> /etc/environment
+    fi
+else
+    # Ensure ARG-provided value is in /etc/environment
+    echo "COMMIT_SHA='${COMMIT_SHA}'" >> /etc/environment
 fi
+
+
+if [ -z "${SHORT_COMMIT_SHA}" ]; then
+    # Try to get short commit hash from git
+    calculated_short_commit_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "")
+    if [ -n "${calculated_short_commit_sha}" ]; then
+        echo "SHORT_COMMIT_SHA='${calculated_short_commit_sha}'" >> /etc/environment
+    fi
+else
+    # Ensure ARG-provided value is in /etc/environment
+    echo "SHORT_COMMIT_SHA='${SHORT_COMMIT_SHA}'" >> /etc/environment
+fi
+
+# --- REMOTE_URL ---
+# Calculate REMOTE_URL if ENV var (from ARG) is empty
+if [ -z "${REMOTE_URL}" ]; then
+    # Try to get remote origin URL from git
+    remote_url_raw=$(git config --get remote.origin.url 2>/dev/null || echo "")
+    if [ -n "${remote_url_raw}" ]; then
+        # Transform git URL (SSH or HTTPS) to web URL
+        if [[ $remote_url_raw == "https://"* ]]; then
+            # Already HTTPS, just remove .git suffix
+            calculated_remote_url=$(echo "$remote_url_raw" | sed 's/\.git$//')
+        else
+            # Convert SSH URL to HTTPS URL
+            calculated_remote_url=$(echo "$remote_url_raw" | sed 's/\.git$//' | sed 's/:/\//' | sed 's/^git@/https:\/\//')
+        fi
+
+        # Write calculated web URL if transformation was successful
+        if [ -n "${calculated_remote_url}" ]; then
+             echo "REMOTE_URL='${calculated_remote_url}'" >> /etc/environment
+        fi
+    fi
+else
+     # Ensure ARG-provided value is in /etc/environment (assume it's a valid web URL)
+     echo "REMOTE_URL='${REMOTE_URL}'" >> /etc/environment
+     # Use provided value for REMOTE_COMMIT_URL calculation below
+     calculated_remote_url="${REMOTE_URL}"
+fi
+
+# --- Determine effective values for subsequent calculations ---
+# Use ENV var value if set (from ARG), otherwise use calculated value
+effective_commit_sha="${COMMIT_SHA:-$calculated_commit_sha}"
+effective_short_commit_sha="${SHORT_COMMIT_SHA:-$calculated_short_commit_sha}"
+effective_remote_url="${REMOTE_URL:-$calculated_remote_url}"
+
+# --- REMOTE_COMMIT_URL ---
+# Calculate and write REMOTE_COMMIT_URL if both components are available
+if [ -z "${REMOTE_COMMIT_URL}" ] && [ -n "${effective_remote_url}" ] && [ -n "${effective_commit_sha}" ]; then
+    echo "REMOTE_COMMIT_URL='${effective_remote_url}/commit/${effective_commit_sha}'" >> /etc/environment
+else
+    # Ensure ARG-provided value is in /etc/environment
+    echo "REMOTE_COMMIT_URL='${REMOTE_COMMIT_URL}'" >> /etc/environment
+fi
+
+# --- CONDUWUIT_VERSION_EXTRA ---
+# Calculate CONDUWUIT_VERSION_EXTRA if ENV var (from ARG) is empty
+if [ -z "${CONDUWUIT_VERSION_EXTRA}" ]; then
+    # Use the effective short commit sha, fallback to "unknown revision"
+    calculated_version_extra="${effective_short_commit_sha:-unknown revision}"
+    # Handle case where commit sha calculation failed and ARG wasn't set
+    if [ -z "${calculated_version_extra}" ]; then
+         calculated_version_extra="unknown revision"
+    fi
+    echo "CONDUWUIT_VERSION_EXTRA='${calculated_version_extra}'" >> /etc/environment
+else
+    # Ensure ARG-provided value is in /etc/environment
+    echo "CONDUWUIT_VERSION_EXTRA='${CONDUWUIT_VERSION_EXTRA}'" >> /etc/environment
+fi
+
 EOF
 
 ARG TARGETPLATFORM
@@ -127,8 +225,6 @@ ARG TARGETPLATFORM
 RUN cat /etc/environment
 RUN xx-cargo --print-target-triple
 
-# Get source
-COPY . .
 
 # Build the binary
 RUN --mount=type=cache,target=/usr/local/cargo/registry \
diff --git a/nix/pkgs/main/default.nix b/nix/pkgs/main/default.nix
index 9c8038a7..3a43af5a 100644
--- a/nix/pkgs/main/default.nix
+++ b/nix/pkgs/main/default.nix
@@ -130,6 +130,8 @@ buildDepsOnlyEnv =
   });
 
 buildPackageEnv = {
+  COMMIT_SHA = inputs.self.rev or inputs.self.dirtyRev or "";
+  SHORT_COMMIT_SHA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
   CONDUWUIT_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
 } // buildDepsOnlyEnv // {
   # Only needed in static stdenv because these are transitive dependencies of rocksdb
diff --git a/src/router/Cargo.toml b/src/router/Cargo.toml
index e4ddcb9b..9fcb8d6a 100644
--- a/src/router/Cargo.toml
+++ b/src/router/Cargo.toml
@@ -103,6 +103,7 @@ conduwuit-admin.workspace = true
 conduwuit-api.workspace = true
 conduwuit-core.workspace = true
 conduwuit-service.workspace = true
+conduwuit-web.workspace = true
 const-str.workspace = true
 futures.workspace = true
 http.workspace = true
diff --git a/src/router/router.rs b/src/router/router.rs
index 0f95b924..2b9ce2ec 100644
--- a/src/router/router.rs
+++ b/src/router/router.rs
@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use axum::{Router, response::IntoResponse, routing::get};
+use axum::{Router, response::IntoResponse};
 use conduwuit::Error;
 use conduwuit_api::router::{state, state::Guard};
 use conduwuit_service::Services;
@@ -11,7 +11,7 @@ pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
 	let router = Router::<state::State>::new();
 	let (state, guard) = state::create(services.clone());
 	let router = conduwuit_api::router::build(router, &services.server)
-		.route("/", get(it_works))
+		.merge(conduwuit_web::build::<state::State>().with_state(()))
 		.fallback(not_found)
 		.with_state(state);
 
@@ -21,5 +21,3 @@ pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
 async fn not_found(_uri: Uri) -> impl IntoResponse {
 	Error::Request(ErrorKind::Unrecognized, "Not Found".into(), StatusCode::NOT_FOUND)
 }
-
-async fn it_works() -> &'static str { "hewwo from conduwuit woof!" }
diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml
new file mode 100644
index 00000000..07dde0f7
--- /dev/null
+++ b/src/web/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "conduwuit_web"
+categories.workspace = true
+description.workspace = true
+edition.workspace = true
+keywords.workspace = true
+license.workspace = true
+readme.workspace = true
+repository.workspace = true
+version.workspace = true
+
+[lib]
+path = "mod.rs"
+crate-type = [
+	"rlib",
+#	"dylib",
+]
+
+[features]
+
+
+[dependencies]
+askama = "0.14.0"
+
+axum.workspace = true
+futures.workspace = true
+tracing.workspace = true
+rand.workspace = true
+thiserror.workspace = true
+
+[lints]
+workspace = true
diff --git a/src/web/css/index.css b/src/web/css/index.css
new file mode 100644
index 00000000..86cb6d8d
--- /dev/null
+++ b/src/web/css/index.css
@@ -0,0 +1,68 @@
+:root {
+	color-scheme: light;
+	--font-stack: sans-serif;
+
+	--background-color: #fff;
+	--text-color: #000;
+
+	--bg: oklch(0.76 0.0854 317.27);
+	--panel-bg: oklch(0.91 0.042 317.27);
+
+	--name-lightness: 0.45;
+
+	@media (prefers-color-scheme: dark) {
+		color-scheme: dark;
+		--text-color: #fff;
+		--bg: oklch(0.15 0.042 317.27);
+		--panel-bg: oklch(0.24 0.03 317.27);
+
+		--name-lightness: 0.8;
+	}
+
+	--c1: oklch(0.44 0.177 353.06);
+	--c2: oklch(0.59 0.158 150.88);
+
+	--normal-font-size: 1rem;
+	--small-font-size: 0.8rem;
+}
+
+body {
+	color: var(--text-color);
+	font-family: var(--font-stack);
+	margin: 0;
+	padding: 0;
+	display: grid;
+	place-items: center;
+	min-height: 100vh;
+}
+
+html {
+	background-color: var(--bg);
+	background-image: linear-gradient(
+		70deg,
+		oklch(from var(--bg) l + 0.2 c h),
+		oklch(from var(--bg) l - 0.2 c h)
+	);
+	font-size: 16px;
+}
+
+.panel {
+	width: min(clamp(24rem, 12rem + 40vw, 48rem), 100vw);
+	border-radius: 15px;
+	background-color: var(--panel-bg);
+	padding-inline: 1.5rem;
+	padding-block: 1rem;
+	box-shadow: 0 0.25em 0.375em hsla(0, 0%, 0%, 0.1);
+}
+
+.project-name {
+	text-decoration: none;
+	background: linear-gradient(
+		130deg,
+		oklch(from var(--c1) var(--name-lightness) c h),
+		oklch(from var(--c2) var(--name-lightness) c h)
+	);
+	background-clip: text;
+	color: transparent;
+	filter: brightness(1.2);
+}
diff --git a/src/web/mod.rs b/src/web/mod.rs
new file mode 100644
index 00000000..ddf13be4
--- /dev/null
+++ b/src/web/mod.rs
@@ -0,0 +1,61 @@
+use askama::Template;
+use axum::{
+	Router,
+	http::{StatusCode, header},
+	response::{Html, IntoResponse, Response},
+	routing::get,
+};
+
+pub fn build<S>() -> Router<()> { Router::new().route("/", get(index_handler)) }
+
+async fn index_handler() -> Result<impl IntoResponse, WebError> {
+	#[derive(Debug, Template)]
+	#[template(path = "index.html.j2")]
+	struct Tmpl<'a> {
+		nonce: &'a str,
+	}
+	let nonce = rand::random::<u64>().to_string();
+
+	let template = Tmpl { nonce: &nonce };
+	Ok((
+		[(header::CONTENT_SECURITY_POLICY, format!("default-src 'none' 'nonce-{nonce}';"))],
+		Html(template.render()?),
+	))
+}
+
+#[derive(Debug, thiserror::Error)]
+enum WebError {
+	#[error("Failed to render template: {0}")]
+	Render(#[from] askama::Error),
+}
+
+impl IntoResponse for WebError {
+	fn into_response(self) -> Response {
+		#[derive(Debug, Template)]
+		#[template(path = "error.html.j2")]
+		struct Tmpl<'a> {
+			nonce: &'a str,
+			err: WebError,
+		}
+
+		let nonce = rand::random::<u64>().to_string();
+
+		let status = match &self {
+			| Self::Render(_) => StatusCode::INTERNAL_SERVER_ERROR,
+		};
+		let tmpl = Tmpl { nonce: &nonce, err: self };
+		if let Ok(body) = tmpl.render() {
+			(
+				status,
+				[(
+					header::CONTENT_SECURITY_POLICY,
+					format!("default-src 'none' 'nonce-{nonce}';"),
+				)],
+				Html(body),
+			)
+				.into_response()
+		} else {
+			(status, "Something went wrong").into_response()
+		}
+	}
+}
diff --git a/src/web/templates/_layout.html.j2 b/src/web/templates/_layout.html.j2
new file mode 100644
index 00000000..a38745e1
--- /dev/null
+++ b/src/web/templates/_layout.html.j2
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8" />
+    <title>{% block title %}Continuwuity{% endblock %}</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+    <style type="text/css" nonce="{{ nonce }}">
+        /*<![CDATA[*/
+            {{ include_str !("css/index.css") | safe }}
+        /*]]>*/
+    </style>
+</head>
+
+<body>
+    <main>{%~ block content %}{% endblock ~%}</main>
+    {%~ block footer ~%}
+        <footer>
+            <p>Powered by <a href="https://continuwuity.org">Continuwuity</a>
+                {%~ if let Some(version_info) = option_env!("CONDUWUIT_VERSION_EXTRA").or(option_env!("SHORT_COMMIT_SHA")) ~%}
+                    {%~ if let Some(url) = option_env!("REMOTE_COMMIT_URL").or(option_env!("REMOTE_URL")) ~%}
+                        (<a href="{{ url }}">{{ version_info }}</a>)
+                    {%~ else ~%}
+                        ({{ version_info }})
+                    {%~ endif ~%}
+                {%~ endif ~%}</p>
+        </footer>
+    {%~ endblock ~%}
+</body>
+
+</html>
diff --git a/src/web/templates/error.html.j2 b/src/web/templates/error.html.j2
new file mode 100644
index 00000000..e320d0ed
--- /dev/null
+++ b/src/web/templates/error.html.j2
@@ -0,0 +1,20 @@
+{% extends "_layout.html.j2" %}
+
+{%- block title -%}
+Server Error
+{%- endblock -%}
+
+{%- block content -%}
+<h1>
+    {%- match err -%}
+    {% else -%} 500: Internal Server Error
+    {%- endmatch -%}
+</h1>
+
+{%- match err -%}
+    {% when WebError::Render(err) -%}
+        <pre>{{ err }}</pre>
+    {% else -%} <p>An error occurred</p>
+{%- endmatch -%}
+
+{%- endblock -%}
diff --git a/src/web/templates/index.html.j2 b/src/web/templates/index.html.j2
new file mode 100644
index 00000000..51393822
--- /dev/null
+++ b/src/web/templates/index.html.j2
@@ -0,0 +1,16 @@
+{% extends "_layout.html.j2" %}
+{%- block content -%}
+<div class="orb"></div>
+<div class="panel">
+    <h1>Welcome to <a class="project-name" href="https://continuwuity.org">Continuwuity</a>!</h1>
+    <p>Continuwuity is successfully installed and working. </p>
+    <p>To get started, you can:</p>
+    <ul>
+        <li>Read the <a href="https://continuwuity.org/introduction">documentation</a></li>
+        <li>Join the <a href="https://continuwuity.org/community">community</a></li>
+        <li>Log in with a <a href="https://matrix.org/ecosystem/clients/">client</a></li>
+        <li>Ensure <a href="https://federationtester.matrix.org/">federation</a> works</li>
+    </ul>
+</div>
+
+{%- endblock content -%}

From a98da7d9422500de592824eee0f441bc94803065 Mon Sep 17 00:00:00 2001
From: Jade Ellis <jade@ellis.link>
Date: Thu, 1 May 2025 00:38:35 +0100
Subject: [PATCH 2/6] refactor: Move git version info gather in into a build
 script

---
 .forgejo/workflows/release-image.yml |   8 +-
 Cargo.lock                           |  44 ++++++++++
 Cargo.toml                           |   6 ++
 docker/Dockerfile                    | 118 ++++-----------------------
 nix/pkgs/main/default.nix            |   5 +-
 src/api/client/message.rs            |   5 +-
 src/build_metadata/Cargo.toml        |  34 ++++++++
 src/build_metadata/build.rs          |  92 +++++++++++++++++++++
 src/build_metadata/mod.rs            |  23 ++++++
 src/core/Cargo.toml                  |   1 +
 src/core/info/version.rs             |  11 +--
 src/web/Cargo.toml                   |   2 +
 src/web/mod.rs                       |   1 +
 src/web/templates/_layout.html.j2    |   4 +-
 14 files changed, 231 insertions(+), 123 deletions(-)
 create mode 100644 src/build_metadata/Cargo.toml
 create mode 100644 src/build_metadata/build.rs
 create mode 100644 src/build_metadata/mod.rs

diff --git a/.forgejo/workflows/release-image.yml b/.forgejo/workflows/release-image.yml
index 3eb84223..de009ad5 100644
--- a/.forgejo/workflows/release-image.yml
+++ b/.forgejo/workflows/release-image.yml
@@ -143,10 +143,10 @@ jobs:
           context: .
           file: "docker/Dockerfile"
           build-args: |
-            CONDUWUIT_VERSION_EXTRA=${{ env.COMMIT_SHORT_SHA }}
-            COMMIT_SHA=${{ github.sha }})
-            REMOTE_URL=${{github.event.repository.html_url }}
-            REMOTE_COMMIT_URL=${{github.event.head_commit.url }}
+            GIT_COMMIT_HASH=${{ github.sha }})
+            GIT_COMMIT_HASH_SHORT=${{ env.COMMIT_SHORT_SHA }})
+            GIT_REMOTE_URL=${{github.event.repository.html_url }}
+            GIT_REMOTE_COMMIT_URL=${{github.event.head_commit.url }}
           platforms: ${{ matrix.platform }}
           labels: ${{ steps.meta.outputs.labels }}
           annotations: ${{ steps.meta.outputs.annotations }}
diff --git a/Cargo.lock b/Cargo.lock
index 1b6c5d26..5f82384e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -584,6 +584,9 @@ name = "built"
 version = "0.7.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
+dependencies = [
+ "cargo-lock",
+]
 
 [[package]]
 name = "bumpalo"
@@ -631,6 +634,19 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "cargo-lock"
+version = "10.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06acb4f71407ba205a07cb453211e0e6a67b21904e47f6ba1f9589e38f2e454"
+dependencies = [
+ "petgraph",
+ "semver",
+ "serde",
+ "toml",
+ "url",
+]
+
 [[package]]
 name = "cargo_toml"
 version = "0.21.0"
@@ -856,6 +872,13 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "conduwuit_build_metadata"
+version = "0.5.0-rc.5"
+dependencies = [
+ "built",
+]
+
 [[package]]
 name = "conduwuit_core"
 version = "0.5.0-rc.5"
@@ -870,6 +893,7 @@ dependencies = [
  "checked_ops",
  "chrono",
  "clap",
+ "conduwuit_build_metadata",
  "conduwuit_macros",
  "const-str",
  "core_affinity",
@@ -1019,6 +1043,7 @@ version = "0.5.0-rc.5"
 dependencies = [
  "askama",
  "axum",
+ "conduwuit_build_metadata",
  "futures",
  "rand 0.8.5",
  "thiserror 2.0.12",
@@ -1515,6 +1540,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
 [[package]]
 name = "flate2"
 version = "1.1.1"
@@ -3132,6 +3163,16 @@ version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
+[[package]]
+name = "petgraph"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
+dependencies = [
+ "fixedbitset",
+ "indexmap 2.8.0",
+]
+
 [[package]]
 name = "phf"
 version = "0.11.3"
@@ -4127,6 +4168,9 @@ name = "semver"
 version = "1.0.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "sentry"
diff --git a/Cargo.toml b/Cargo.toml
index 44db4c4b..43cd3f4f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -631,6 +631,12 @@ package = "conduwuit_web"
 path = "src/web"
 default-features = false
 
+
+[workspace.dependencies.conduwuit-build-metadata]
+package = "conduwuit_build_metadata"
+path = "src/build_metadata"
+default-features = false
+
 ###############################################################################
 #
 # Release profiles
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 56f2d5fa..3029282f 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -115,116 +115,26 @@ FROM toolchain AS builder
 # Get source
 COPY . .
 
-# Conduwuit version info
-ARG COMMIT_SHA=
-ARG SHORT_COMMIT_SHA=
-ARG REMOTE_URL=
-ARG CONDUWUIT_VERSION_EXTRA=
-ENV COMMIT_SHA=$COMMIT_SHA
-ENV SHORT_COMMIT_SHA=$SHORT_COMMIT_SHA
-ENV REMOTE_URL=$REMOTE_URL
-ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA
-
-# Calculate version info from git if not provided via ARGs
-# and write all relevant vars to /etc/environment
-RUN <<'EOF'
-set -e # Exit on error
-
-# Use temp variables to store calculated values
-calculated_commit_sha=""
-calculated_remote_url=""
-calculated_version_extra=""
-
-# --- COMMIT_SHA ---
-# Calculate COMMIT_SHA if ENV var (from ARG) is empty
-if [ -z "${COMMIT_SHA}" ]; then
-    # Try to get short commit hash from git
-    calculated_commit_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
-    if [ -n "${calculated_commit_sha}" ]; then
-        echo "COMMIT_SHA='${calculated_commit_sha}'" >> /etc/environment
-    fi
-else
-    # Ensure ARG-provided value is in /etc/environment
-    echo "COMMIT_SHA='${COMMIT_SHA}'" >> /etc/environment
-fi
-
-
-if [ -z "${SHORT_COMMIT_SHA}" ]; then
-    # Try to get short commit hash from git
-    calculated_short_commit_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "")
-    if [ -n "${calculated_short_commit_sha}" ]; then
-        echo "SHORT_COMMIT_SHA='${calculated_short_commit_sha}'" >> /etc/environment
-    fi
-else
-    # Ensure ARG-provided value is in /etc/environment
-    echo "SHORT_COMMIT_SHA='${SHORT_COMMIT_SHA}'" >> /etc/environment
-fi
-
-# --- REMOTE_URL ---
-# Calculate REMOTE_URL if ENV var (from ARG) is empty
-if [ -z "${REMOTE_URL}" ]; then
-    # Try to get remote origin URL from git
-    remote_url_raw=$(git config --get remote.origin.url 2>/dev/null || echo "")
-    if [ -n "${remote_url_raw}" ]; then
-        # Transform git URL (SSH or HTTPS) to web URL
-        if [[ $remote_url_raw == "https://"* ]]; then
-            # Already HTTPS, just remove .git suffix
-            calculated_remote_url=$(echo "$remote_url_raw" | sed 's/\.git$//')
-        else
-            # Convert SSH URL to HTTPS URL
-            calculated_remote_url=$(echo "$remote_url_raw" | sed 's/\.git$//' | sed 's/:/\//' | sed 's/^git@/https:\/\//')
-        fi
-
-        # Write calculated web URL if transformation was successful
-        if [ -n "${calculated_remote_url}" ]; then
-             echo "REMOTE_URL='${calculated_remote_url}'" >> /etc/environment
-        fi
-    fi
-else
-     # Ensure ARG-provided value is in /etc/environment (assume it's a valid web URL)
-     echo "REMOTE_URL='${REMOTE_URL}'" >> /etc/environment
-     # Use provided value for REMOTE_COMMIT_URL calculation below
-     calculated_remote_url="${REMOTE_URL}"
-fi
-
-# --- Determine effective values for subsequent calculations ---
-# Use ENV var value if set (from ARG), otherwise use calculated value
-effective_commit_sha="${COMMIT_SHA:-$calculated_commit_sha}"
-effective_short_commit_sha="${SHORT_COMMIT_SHA:-$calculated_short_commit_sha}"
-effective_remote_url="${REMOTE_URL:-$calculated_remote_url}"
-
-# --- REMOTE_COMMIT_URL ---
-# Calculate and write REMOTE_COMMIT_URL if both components are available
-if [ -z "${REMOTE_COMMIT_URL}" ] && [ -n "${effective_remote_url}" ] && [ -n "${effective_commit_sha}" ]; then
-    echo "REMOTE_COMMIT_URL='${effective_remote_url}/commit/${effective_commit_sha}'" >> /etc/environment
-else
-    # Ensure ARG-provided value is in /etc/environment
-    echo "REMOTE_COMMIT_URL='${REMOTE_COMMIT_URL}'" >> /etc/environment
-fi
-
-# --- CONDUWUIT_VERSION_EXTRA ---
-# Calculate CONDUWUIT_VERSION_EXTRA if ENV var (from ARG) is empty
-if [ -z "${CONDUWUIT_VERSION_EXTRA}" ]; then
-    # Use the effective short commit sha, fallback to "unknown revision"
-    calculated_version_extra="${effective_short_commit_sha:-unknown revision}"
-    # Handle case where commit sha calculation failed and ARG wasn't set
-    if [ -z "${calculated_version_extra}" ]; then
-         calculated_version_extra="unknown revision"
-    fi
-    echo "CONDUWUIT_VERSION_EXTRA='${calculated_version_extra}'" >> /etc/environment
-else
-    # Ensure ARG-provided value is in /etc/environment
-    echo "CONDUWUIT_VERSION_EXTRA='${CONDUWUIT_VERSION_EXTRA}'" >> /etc/environment
-fi
-
-EOF
-
 ARG TARGETPLATFORM
 
 # Verify environment configuration
 RUN cat /etc/environment
 RUN xx-cargo --print-target-triple
 
+# Conduwuit version info
+ARG GIT_COMMIT_HASH=
+ARG GIT_COMMIT_HASH_SHORT=
+ARG GIT_REMOTE_URL=
+ARG GIT_REMOTE_COMMIT_URL=
+ARG CONDUWUIT_VERSION_EXTRA=
+ARG CONTINUWUITY_VERSION_EXTRA=
+ENV GIT_COMMIT_HASH=$GIT_COMMIT_HASH
+ENV GIT_COMMIT_HASH_SHORT=$GIT_COMMIT_HASH_SHORT
+ENV GIT_REMOTE_URL=$GIT_REMOTE_URL
+ENV GIT_REMOTE_COMMIT_URL=$GIT_REMOTE_COMMIT_URL
+ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA
+ENV CONTINUWUITY_VERSION_EXTRA=$CONTINUWUITY_VERSION_EXTRA
+
 
 # Build the binary
 RUN --mount=type=cache,target=/usr/local/cargo/registry \
diff --git a/nix/pkgs/main/default.nix b/nix/pkgs/main/default.nix
index 3a43af5a..f2fffec0 100644
--- a/nix/pkgs/main/default.nix
+++ b/nix/pkgs/main/default.nix
@@ -130,9 +130,8 @@ buildDepsOnlyEnv =
   });
 
 buildPackageEnv = {
-  COMMIT_SHA = inputs.self.rev or inputs.self.dirtyRev or "";
-  SHORT_COMMIT_SHA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
-  CONDUWUIT_VERSION_EXTRA = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
+  GIT_COMMIT_HASH = inputs.self.rev or inputs.self.dirtyRev or "";
+  GIT_COMMIT_HASH_SHORT = inputs.self.shortRev or inputs.self.dirtyShortRev or "";
 } // buildDepsOnlyEnv // {
   # Only needed in static stdenv because these are transitive dependencies of rocksdb
   CARGO_BUILD_RUSTFLAGS = buildDepsOnlyEnv.CARGO_BUILD_RUSTFLAGS
diff --git a/src/api/client/message.rs b/src/api/client/message.rs
index 16b1796a..e442850b 100644
--- a/src/api/client/message.rs
+++ b/src/api/client/message.rs
@@ -143,7 +143,10 @@ pub(crate) async fn get_message_events_route(
 				if let Some(registration) = body.appservice_info.as_ref() {
 					<&DeviceId>::from(registration.registration.id.as_str())
 				} else {
-					panic!("No device_id provided and no appservice registration found, this should be unreachable");
+					panic!(
+						"No device_id provided and no appservice registration found, this \
+						 should be unreachable"
+					);
 				},
 		},
 		room_id,
diff --git a/src/build_metadata/Cargo.toml b/src/build_metadata/Cargo.toml
new file mode 100644
index 00000000..3a98c6bf
--- /dev/null
+++ b/src/build_metadata/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "conduwuit_build_metadata"
+categories.workspace = true
+description.workspace = true
+edition.workspace = true
+keywords.workspace = true
+license.workspace = true
+readme.workspace = true
+repository.workspace = true
+version.workspace = true
+
+
+build = "build.rs"
+# [[bin]]
+# path = "main.rs"
+# name = "conduwuit_build_metadata" 
+
+[lib]
+path = "mod.rs"
+crate-type = [
+	"rlib",
+#	"dylib",
+]
+
+[features]
+
+
+[dependencies]
+
+[build-dependencies]
+built = {version = "0.7", features = ["cargo-lock", "dependency-tree"]}
+
+[lints]
+workspace = true
diff --git a/src/build_metadata/build.rs b/src/build_metadata/build.rs
new file mode 100644
index 00000000..2fec16a7
--- /dev/null
+++ b/src/build_metadata/build.rs
@@ -0,0 +1,92 @@
+use std::process::Command;
+
+fn run_git_command(args: &[&str]) -> Option<String> {
+	Command::new("git")
+		.args(args)
+		.output()
+		.ok()
+		.filter(|output| output.status.success())
+		.and_then(|output| String::from_utf8(output.stdout).ok())
+		.map(|s| s.trim().to_owned())
+		.filter(|s| !s.is_empty())
+}
+fn get_env(env_var: &str) -> Option<String> {
+	match std::env::var(env_var) {
+		| Ok(val) if !val.is_empty() => Some(val),
+		| _ => None,
+	}
+}
+fn main() {
+	// built gets the default crate from the workspace. Not sure if this is intended
+	// behavior, but it's what we want.
+	built::write_built_file().expect("Failed to acquire build-time information");
+
+	// --- Git Information ---
+	let mut commit_hash = None;
+	let mut commit_hash_short = None;
+	let mut remote_url_web = None;
+
+	// Get full commit hash
+	if let Some(hash) =
+		get_env("GIT_COMMIT_HASH").or_else(|| run_git_command(&["rev-parse", "HEAD"]))
+	{
+		println!("cargo:rustc-env=GIT_COMMIT_HASH={hash}");
+		commit_hash = Some(hash);
+	}
+
+	// Get short commit hash
+	if let Some(short_hash) = get_env("GIT_COMMIT_HASH_SHORT")
+		.or_else(|| run_git_command(&["rev-parse", "--short", "HEAD"]))
+	{
+		println!("cargo:rustc-env=GIT_COMMIT_HASH_SHORT={short_hash}");
+		commit_hash_short = Some(short_hash);
+	}
+
+	// Get remote URL and convert to web URL
+	if let Some(remote_url_raw) = get_env("GIT_REMOTE_URL")
+		.or_else(|| run_git_command(&["config", "--get", "remote.origin.url"]))
+	{
+		println!("cargo:rustc-env=GIT_REMOTE_URL={remote_url_raw}");
+		let web_url = if remote_url_raw.starts_with("https://") {
+			remote_url_raw.trim_end_matches(".git").to_owned()
+		} else if remote_url_raw.starts_with("git@") {
+			remote_url_raw
+				.trim_end_matches(".git")
+				.replacen(':', "/", 1)
+				.replacen("git@", "https://", 1)
+		} else if remote_url_raw.starts_with("ssh://") {
+			remote_url_raw
+				.trim_end_matches(".git")
+				.replacen("git@", "", 1)
+				.replacen("ssh:", "https:", 1)
+		} else {
+			// Assume it's already a web URL or unknown format
+			remote_url_raw
+		};
+		println!("cargo:rustc-env=GIT_REMOTE_WEB_URL={web_url}");
+		remote_url_web = Some(web_url);
+	}
+
+	// Construct remote commit URL
+	if let Some(remote_commit_url) = get_env("GIT_REMOTE_COMMIT_URL") {
+		println!("cargo:rustc-env=GIT_REMOTE_COMMIT_URL={remote_commit_url}");
+	} else if let (Some(base_url), Some(hash)) =
+		(&remote_url_web, commit_hash.as_ref().or(commit_hash_short.as_ref()))
+	{
+		let commit_page = format!("{base_url}/commit/{hash}");
+		println!("cargo:rustc-env=GIT_REMOTE_COMMIT_URL={commit_page}");
+	}
+
+	// --- Rerun Triggers ---
+	// Rerun if the git HEAD changes
+	println!("cargo:rerun-if-changed=.git/HEAD");
+	// Rerun if the ref pointed to by HEAD changes (e.g., new commit on branch)
+	if let Some(ref_path) = run_git_command(&["symbolic-ref", "--quiet", "HEAD"]) {
+		println!("cargo:rerun-if-changed=.git/{ref_path}");
+	}
+
+	println!("cargo:rerun-if-env-changed=GIT_COMMIT_HASH");
+	println!("cargo:rerun-if-env-changed=GIT_COMMIT_HASH_SHORT");
+	println!("cargo:rerun-if-env-changed=GIT_REMOTE_URL");
+	println!("cargo:rerun-if-env-changed=GIT_REMOTE_COMMIT_URL");
+}
diff --git a/src/build_metadata/mod.rs b/src/build_metadata/mod.rs
new file mode 100644
index 00000000..cf3364c1
--- /dev/null
+++ b/src/build_metadata/mod.rs
@@ -0,0 +1,23 @@
+pub mod built {
+	include!(concat!(env!("OUT_DIR"), "/built.rs"));
+}
+
+pub static GIT_COMMIT_HASH: Option<&str> = option_env!("GIT_COMMIT_HASH");
+
+pub static GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("GIT_COMMIT_HASH_SHORT");
+
+// this would be a lot better if Option::or was const.
+pub static VERSION_EXTRA: Option<&str> =
+	if let v @ Some(_) = option_env!("CONTINUWUITY_VERSION_EXTRA") {
+		v
+	} else if let v @ Some(_) = option_env!("CONDUWUIT_VERSION_EXTRA") {
+		v
+	} else if let v @ Some(_) = option_env!("CONDUIT_VERSION_EXTRA") {
+		v
+	} else {
+		GIT_COMMIT_HASH_SHORT
+	};
+pub static GIT_REMOTE_WEB_URL: Option<&str> = option_env!("GIT_REMOTE_WEB_URL");
+pub static GIT_REMOTE_COMMIT_URL: Option<&str> = option_env!("GIT_REMOTE_COMMIT_URL");
+
+// TODO: Mark dirty builds within the version string
diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml
index f42b049b..0c33c590 100644
--- a/src/core/Cargo.toml
+++ b/src/core/Cargo.toml
@@ -67,6 +67,7 @@ checked_ops.workspace = true
 chrono.workspace = true
 clap.workspace = true
 conduwuit-macros.workspace = true
+conduwuit-build-metadata.workspace = true
 const-str.workspace = true
 core_affinity.workspace = true
 ctor.workspace = true
diff --git a/src/core/info/version.rs b/src/core/info/version.rs
index 6abb6e13..523c40a2 100644
--- a/src/core/info/version.rs
+++ b/src/core/info/version.rs
@@ -26,13 +26,6 @@ pub fn user_agent() -> &'static str { USER_AGENT.get_or_init(init_user_agent) }
 fn init_user_agent() -> String { format!("{}/{}", name(), version()) }
 
 fn init_version() -> String {
-	option_env!("CONDUWUIT_VERSION_EXTRA")
-		.or(option_env!("CONDUIT_VERSION_EXTRA"))
-		.map_or(SEMANTIC.to_owned(), |extra| {
-			if extra.is_empty() {
-				SEMANTIC.to_owned()
-			} else {
-				format!("{SEMANTIC} ({extra})")
-			}
-		})
+	conduwuit_build_metadata::VERSION_EXTRA
+		.map_or(SEMANTIC.to_owned(), |extra| format!("{SEMANTIC} ({extra})"))
 }
diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml
index 07dde0f7..8e03a338 100644
--- a/src/web/Cargo.toml
+++ b/src/web/Cargo.toml
@@ -20,6 +20,8 @@ crate-type = [
 
 
 [dependencies]
+conduwuit-build-metadata.workspace = true
+
 askama = "0.14.0"
 
 axum.workspace = true
diff --git a/src/web/mod.rs b/src/web/mod.rs
index ddf13be4..520a9a34 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -5,6 +5,7 @@ use axum::{
 	response::{Html, IntoResponse, Response},
 	routing::get,
 };
+use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, VERSION_EXTRA};
 
 pub fn build<S>() -> Router<()> { Router::new().route("/", get(index_handler)) }
 
diff --git a/src/web/templates/_layout.html.j2 b/src/web/templates/_layout.html.j2
index a38745e1..fd0a5b29 100644
--- a/src/web/templates/_layout.html.j2
+++ b/src/web/templates/_layout.html.j2
@@ -18,8 +18,8 @@
     {%~ block footer ~%}
         <footer>
             <p>Powered by <a href="https://continuwuity.org">Continuwuity</a>
-                {%~ if let Some(version_info) = option_env!("CONDUWUIT_VERSION_EXTRA").or(option_env!("SHORT_COMMIT_SHA")) ~%}
-                    {%~ if let Some(url) = option_env!("REMOTE_COMMIT_URL").or(option_env!("REMOTE_URL")) ~%}
+                {%~ if let Some(version_info) = VERSION_EXTRA ~%}
+                    {%~ if let Some(url) = GIT_REMOTE_COMMIT_URL.or(GIT_REMOTE_WEB_URL) ~%}
                         (<a href="{{ url }}">{{ version_info }}</a>)
                     {%~ else ~%}
                         ({{ version_info }})

From cbcf4300dffb55ad9b1ed5529ae6bf81ad446408 Mon Sep 17 00:00:00 2001
From: Jade Ellis <jade@ellis.link>
Date: Thu, 1 May 2025 00:47:03 +0100
Subject: [PATCH 3/6] ci: Cache timelord-cli to avoid unnecesary compilation

---
 .forgejo/workflows/release-image.yml | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/.forgejo/workflows/release-image.yml b/.forgejo/workflows/release-image.yml
index de009ad5..69d7d2fd 100644
--- a/.forgejo/workflows/release-image.yml
+++ b/.forgejo/workflows/release-image.yml
@@ -80,17 +80,29 @@ jobs:
         run: echo '${{ toJSON(fromJSON(needs.define-variables.outputs.build_matrix)) }}'
       - name: Echo matrix
         run: echo '${{ toJSON(matrix) }}'
-      - name: Checkout repository
-        uses: actions/checkout@v4
-        with:
-          persist-credentials: false
       - run: |
           if ! command -v rustup &> /dev/null ; then
             curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused -fsSL "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
             echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
           fi
-      - uses: https://github.com/cargo-bins/cargo-binstall@main
+      
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          persist-credentials: false
+
+      - name: Cache timelord-cli installation
+        id: cache-timelord-bin
+        uses: actions/cache@v3
+        with:
+          path: ~/.cargo/bin/timelord
+          key: timelord-cli-v3.0.1
+      - name: Install timelord-cli
+        uses: https://github.com/cargo-bins/cargo-binstall@main
+        if: steps.cache-timelord-bin.outputs.cache-hit != 'true'
       - run: cargo binstall timelord-cli@3.0.1
+        if: steps.cache-timelord-bin.outputs.cache-hit != 'true'
+
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
       - name: Set up QEMU

From fb9d4c30f4e89227244fcb275cdbed6261607610 Mon Sep 17 00:00:00 2001
From: Jade Ellis <jade@ellis.link>
Date: Thu, 1 May 2025 18:46:30 +0100
Subject: [PATCH 4/6] feat: Prefill server name in federation test

---
 Cargo.lock                           |  1 +
 src/api/router.rs                    |  4 ++--
 src/router/layers.rs                 |  3 +--
 src/router/router.rs                 |  5 ++---
 src/service/mod.rs                   |  1 +
 src/{api/router => service}/state.rs |  2 +-
 src/web/Cargo.toml                   |  1 +
 src/web/mod.rs                       | 17 ++++++++++++++---
 src/web/templates/index.html.j2      |  2 +-
 9 files changed, 24 insertions(+), 12 deletions(-)
 rename src/{api/router => service}/state.rs (98%)

diff --git a/Cargo.lock b/Cargo.lock
index 5f82384e..18375234 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1044,6 +1044,7 @@ dependencies = [
  "askama",
  "axum",
  "conduwuit_build_metadata",
+ "conduwuit_service",
  "futures",
  "rand 0.8.5",
  "thiserror 2.0.12",
diff --git a/src/api/router.rs b/src/api/router.rs
index 3fbef275..5416e9e9 100644
--- a/src/api/router.rs
+++ b/src/api/router.rs
@@ -3,7 +3,6 @@ mod auth;
 mod handler;
 mod request;
 mod response;
-pub mod state;
 
 use std::str::FromStr;
 
@@ -13,10 +12,11 @@ use axum::{
 	routing::{any, get, post},
 };
 use conduwuit::{Server, err};
+pub(super) use conduwuit_service::state::State;
 use http::{Uri, uri};
 
 use self::handler::RouterExt;
-pub(super) use self::{args::Args as Ruma, response::RumaResponse, state::State};
+pub(super) use self::{args::Args as Ruma, response::RumaResponse};
 use crate::{client, server};
 
 pub fn build(router: Router<State>, server: &Server) -> Router<State> {
diff --git a/src/router/layers.rs b/src/router/layers.rs
index 6920555d..70f3a660 100644
--- a/src/router/layers.rs
+++ b/src/router/layers.rs
@@ -6,8 +6,7 @@ use axum::{
 };
 use axum_client_ip::SecureClientIpSource;
 use conduwuit::{Result, Server, debug, error};
-use conduwuit_api::router::state::Guard;
-use conduwuit_service::Services;
+use conduwuit_service::{Services, state::Guard};
 use http::{
 	HeaderValue, Method, StatusCode,
 	header::{self, HeaderName},
diff --git a/src/router/router.rs b/src/router/router.rs
index 2b9ce2ec..fdaf9126 100644
--- a/src/router/router.rs
+++ b/src/router/router.rs
@@ -2,8 +2,7 @@ use std::sync::Arc;
 
 use axum::{Router, response::IntoResponse};
 use conduwuit::Error;
-use conduwuit_api::router::{state, state::Guard};
-use conduwuit_service::Services;
+use conduwuit_service::{Services, state, state::Guard};
 use http::{StatusCode, Uri};
 use ruma::api::client::error::ErrorKind;
 
@@ -11,7 +10,7 @@ pub(crate) fn build(services: &Arc<Services>) -> (Router, Guard) {
 	let router = Router::<state::State>::new();
 	let (state, guard) = state::create(services.clone());
 	let router = conduwuit_api::router::build(router, &services.server)
-		.merge(conduwuit_web::build::<state::State>().with_state(()))
+		.merge(conduwuit_web::build())
 		.fallback(not_found)
 		.with_state(state);
 
diff --git a/src/service/mod.rs b/src/service/mod.rs
index eb15e5ec..3d7a3aa9 100644
--- a/src/service/mod.rs
+++ b/src/service/mod.rs
@@ -5,6 +5,7 @@ mod manager;
 mod migrations;
 mod service;
 pub mod services;
+pub mod state;
 
 pub mod account_data;
 pub mod admin;
diff --git a/src/api/router/state.rs b/src/service/state.rs
similarity index 98%
rename from src/api/router/state.rs
rename to src/service/state.rs
index 57eb94ca..c0884a5c 100644
--- a/src/api/router/state.rs
+++ b/src/service/state.rs
@@ -1,6 +1,6 @@
 use std::{ops::Deref, sync::Arc};
 
-use conduwuit_service::Services;
+use crate::Services;
 
 #[derive(Clone, Copy)]
 pub struct State {
diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml
index 8e03a338..5c2dbebb 100644
--- a/src/web/Cargo.toml
+++ b/src/web/Cargo.toml
@@ -21,6 +21,7 @@ crate-type = [
 
 [dependencies]
 conduwuit-build-metadata.workspace = true
+conduwuit-service.workspace = true
 
 askama = "0.14.0"
 
diff --git a/src/web/mod.rs b/src/web/mod.rs
index 520a9a34..25ec868c 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -1,23 +1,34 @@
 use askama::Template;
 use axum::{
 	Router,
+	extract::State,
 	http::{StatusCode, header},
 	response::{Html, IntoResponse, Response},
 	routing::get,
 };
 use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, VERSION_EXTRA};
+use conduwuit_service::state;
 
-pub fn build<S>() -> Router<()> { Router::new().route("/", get(index_handler)) }
+pub fn build() -> Router<state::State> {
+	let router = Router::<state::State>::new();
+	router.route("/", get(index_handler))
+}
 
-async fn index_handler() -> Result<impl IntoResponse, WebError> {
+async fn index_handler(
+	State(services): State<state::State>,
+) -> Result<impl IntoResponse, WebError> {
 	#[derive(Debug, Template)]
 	#[template(path = "index.html.j2")]
 	struct Tmpl<'a> {
 		nonce: &'a str,
+		server_name: &'a str,
 	}
 	let nonce = rand::random::<u64>().to_string();
 
-	let template = Tmpl { nonce: &nonce };
+	let template = Tmpl {
+		nonce: &nonce,
+		server_name: services.config.server_name.as_str(),
+	};
 	Ok((
 		[(header::CONTENT_SECURITY_POLICY, format!("default-src 'none' 'nonce-{nonce}';"))],
 		Html(template.render()?),
diff --git a/src/web/templates/index.html.j2 b/src/web/templates/index.html.j2
index 51393822..648f5ddd 100644
--- a/src/web/templates/index.html.j2
+++ b/src/web/templates/index.html.j2
@@ -9,7 +9,7 @@
         <li>Read the <a href="https://continuwuity.org/introduction">documentation</a></li>
         <li>Join the <a href="https://continuwuity.org/community">community</a></li>
         <li>Log in with a <a href="https://matrix.org/ecosystem/clients/">client</a></li>
-        <li>Ensure <a href="https://federationtester.matrix.org/">federation</a> works</li>
+        <li>Ensure <a href="https://federationtester.matrix.org/#{{ server_name }}">federation</a> works</li>
     </ul>
 </div>
 

From e3ae024ed37ce869e66a45f1d567b54600a90cf7 Mon Sep 17 00:00:00 2001
From: Jade Ellis <jade@ellis.link>
Date: Thu, 1 May 2025 21:21:28 +0100
Subject: [PATCH 5/6] chore: Link to Matrix rooms directly

---
 src/web/templates/index.html.j2 | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/web/templates/index.html.j2 b/src/web/templates/index.html.j2
index 648f5ddd..7f11cb1c 100644
--- a/src/web/templates/index.html.j2
+++ b/src/web/templates/index.html.j2
@@ -7,7 +7,7 @@
     <p>To get started, you can:</p>
     <ul>
         <li>Read the <a href="https://continuwuity.org/introduction">documentation</a></li>
-        <li>Join the <a href="https://continuwuity.org/community">community</a></li>
+        <li>Join the <a href="https://matrix.to/#/#continuwuity:continuwuity.org">Continuwuity Matrix room</a> or <a href="https://matrix.to/#/#space:continuwuity.org">space</a></li>
         <li>Log in with a <a href="https://matrix.org/ecosystem/clients/">client</a></li>
         <li>Ensure <a href="https://federationtester.matrix.org/#{{ server_name }}">federation</a> works</li>
     </ul>

From d78fc53577b38d0cd9f93341804d1ca930d0d719 Mon Sep 17 00:00:00 2001
From: Jade Ellis <jade@ellis.link>
Date: Thu, 1 May 2025 21:27:12 +0100
Subject: [PATCH 6/6] ci: Fix bad comparison

---
 .forgejo/workflows/release-image.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.forgejo/workflows/release-image.yml b/.forgejo/workflows/release-image.yml
index 69d7d2fd..704a3bbf 100644
--- a/.forgejo/workflows/release-image.yml
+++ b/.forgejo/workflows/release-image.yml
@@ -214,7 +214,7 @@ jobs:
             type=semver,pattern=v{{version}}
             type=semver,pattern=v{{major}}.{{minor}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.0.') }}
             type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
-            type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) 1= github.ref && 'branch-' || '' }}
+            type=ref,event=branch,prefix=${{ format('refs/heads/{0}', github.event.repository.default_branch) != github.ref && 'branch-' || '' }}
             type=ref,event=pr
             type=sha,format=long
           images: ${{needs.define-variables.outputs.images}}