kitgit

open #1 temp

feat/better-search → main

migrations/010_repo_mirrors.sql +0 −14
diff --git a/migrations/010_repo_mirrors.sql b/migrations/010_repo_mirrors.sql
deleted file mode 100644
index 6d5bb5c..0000000
--- a/migrations/010_repo_mirrors.sql
+++ /dev/null
@@ -1,15 +0,0 @@
--- Pull mirrors: fetch from a remote into the local bare repository.
-CREATE TABLE IF NOT EXISTS repo_mirrors (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL UNIQUE REFERENCES repositories(id) ON DELETE CASCADE,
-    remote_url TEXT NOT NULL,
-    enabled BOOLEAN NOT NULL DEFAULT TRUE,
-    last_synced_at TIMESTAMPTZ,
-    last_error TEXT,
-    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    CONSTRAINT repo_mirrors_remote_url_nonempty CHECK (char_length(trim(remote_url)) > 0)
-);
-CREATE INDEX IF NOT EXISTS repo_mirrors_enabled_idx
-    ON repo_mirrors(enabled) WHERE enabled = TRUE;
migrations/011_user_audit_log.sql +0 −17
diff --git a/migrations/011_user_audit_log.sql b/migrations/011_user_audit_log.sql
deleted file mode 100644
index 13a5f1b..0000000
--- a/migrations/011_user_audit_log.sql
+++ /dev/null
@@ -1,18 +0,0 @@
--- Per-user security / account audit trail (separate from activity_events social feed).
-CREATE TABLE IF NOT EXISTS audit_log (
-    id BIGSERIAL PRIMARY KEY,
-    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    actor_id UUID REFERENCES users(id) ON DELETE SET NULL,
-    action TEXT NOT NULL,
-    ip TEXT,
-    user_agent TEXT,
-    metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE INDEX IF NOT EXISTS audit_log_user_id_created_at_idx
-    ON audit_log(user_id, created_at DESC);
-CREATE INDEX IF NOT EXISTS audit_log_created_at_idx
-    ON audit_log(created_at DESC);
-CREATE INDEX IF NOT EXISTS audit_log_action_idx
-    ON audit_log(action);
migrations/012_user_theme.sql +0 −9
diff --git a/migrations/012_user_theme.sql b/migrations/012_user_theme.sql
deleted file mode 100644
index 7b57126..0000000
--- a/migrations/012_user_theme.sql
+++ /dev/null
@@ -1,10 +0,0 @@
--- Per-user appearance preference: light | dark | system
-
-ALTER TABLE users
-    ADD COLUMN IF NOT EXISTS theme TEXT NOT NULL DEFAULT 'system';
-
-ALTER TABLE users
-    DROP CONSTRAINT IF EXISTS users_theme_check;
-
-ALTER TABLE users
-    ADD CONSTRAINT users_theme_check CHECK (theme IN ('light', 'dark', 'system'));
migrations/013_deploy_keys.sql +0 −13
diff --git a/migrations/013_deploy_keys.sql b/migrations/013_deploy_keys.sql
deleted file mode 100644
index dd351b3..0000000
--- a/migrations/013_deploy_keys.sql
+++ /dev/null
@@ -1,14 +0,0 @@
--- Repo-scoped SSH deploy keys (read-only or read-write).
-CREATE TABLE IF NOT EXISTS deploy_keys (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    name TEXT NOT NULL DEFAULT 'deploy',
-    public_key TEXT NOT NULL,
-    fingerprint TEXT NOT NULL UNIQUE,
-    read_only BOOLEAN NOT NULL DEFAULT TRUE,
-    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE INDEX IF NOT EXISTS deploy_keys_repo_id_idx ON deploy_keys(repo_id);
-CREATE INDEX IF NOT EXISTS deploy_keys_fingerprint_idx ON deploy_keys(fingerprint);
\ No newline at end of file
migrations/014_webhooks.sql +0 −26
diff --git a/migrations/014_webhooks.sql b/migrations/014_webhooks.sql
deleted file mode 100644
index 880aed9..0000000
--- a/migrations/014_webhooks.sql
+++ /dev/null
@@ -1,27 +0,0 @@
--- Repository webhooks + delivery log
-
-CREATE TABLE webhooks (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    url TEXT NOT NULL,
-    secret TEXT NOT NULL DEFAULT '',
-    events TEXT[] NOT NULL DEFAULT '{}',
-    active BOOLEAN NOT NULL DEFAULT TRUE,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    CONSTRAINT webhooks_url_http CHECK (url ~* '^https?://')
-);
-CREATE INDEX webhooks_repo_id_idx ON webhooks(repo_id);
-
-CREATE TABLE webhook_deliveries (
-    id BIGSERIAL PRIMARY KEY,
-    webhook_id UUID NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
-    event TEXT NOT NULL,
-    action TEXT NOT NULL DEFAULT '',
-    success BOOLEAN NOT NULL DEFAULT FALSE,
-    status_code INT,
-    error TEXT,
-    duration_ms INT,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-CREATE INDEX webhook_deliveries_webhook_id_idx ON webhook_deliveries(webhook_id);
-CREATE INDEX webhook_deliveries_created_at_idx ON webhook_deliveries(created_at DESC);
\ No newline at end of file
migrations/016_notifications.sql +0 −19
diff --git a/migrations/016_notifications.sql b/migrations/016_notifications.sql
deleted file mode 100644
index 0439544..0000000
--- a/migrations/016_notifications.sql
+++ /dev/null
@@ -1,20 +0,0 @@
--- In-app notifications for watched repos and thread participants
-
-CREATE TABLE IF NOT EXISTS notifications (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-    kind TEXT NOT NULL,
-    title TEXT NOT NULL,
-    body TEXT NOT NULL DEFAULT '',
-    href TEXT NOT NULL DEFAULT '',
-    repo_id UUID REFERENCES repositories(id) ON DELETE SET NULL,
-    read_at TIMESTAMPTZ,
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-
-CREATE INDEX IF NOT EXISTS notifications_user_id_created_at_idx
-    ON notifications (user_id, created_at DESC);
-
-CREATE INDEX IF NOT EXISTS notifications_user_id_unread_idx
-    ON notifications (user_id)
-    WHERE read_at IS NULL;
migrations/017_labels_milestones.sql +0 −53
diff --git a/migrations/017_labels_milestones.sql b/migrations/017_labels_milestones.sql
deleted file mode 100644
index f4ce6e1..0000000
--- a/migrations/017_labels_milestones.sql
+++ /dev/null
@@ -1,55 +0,0 @@
--- Labels and milestones for issues and pull requests.
-
--- Replace the legacy text-only issue_labels table with proper label entities.
-DROP TABLE IF EXISTS issue_labels;
-
-CREATE TABLE labels (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    name TEXT NOT NULL,
-    color TEXT NOT NULL DEFAULT '0969da',
-    description TEXT NOT NULL DEFAULT '',
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    UNIQUE (repo_id, name),
-    CONSTRAINT labels_name_len CHECK (char_length(name) BETWEEN 1 AND 50),
-    CONSTRAINT labels_color_format CHECK (color ~ '^[0-9a-fA-F]{6}$')
-);
-CREATE INDEX labels_repo_id_idx ON labels(repo_id);
-
-CREATE TABLE milestones (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    repo_id UUID NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
-    title TEXT NOT NULL,
-    description TEXT NOT NULL DEFAULT '',
-    due_on DATE,
-    state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'closed')),
-    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-    closed_at TIMESTAMPTZ,
-    UNIQUE (repo_id, title),
-    CONSTRAINT milestones_title_len CHECK (char_length(title) BETWEEN 1 AND 120)
-);
-CREATE INDEX milestones_repo_id_idx ON milestones(repo_id);
-CREATE INDEX milestones_repo_state_idx ON milestones(repo_id, state);
-
-ALTER TABLE issues
-    ADD COLUMN IF NOT EXISTS milestone_id UUID REFERENCES milestones(id) ON DELETE SET NULL;
-CREATE INDEX IF NOT EXISTS issues_milestone_id_idx ON issues(milestone_id);
-
-ALTER TABLE pull_requests
-    ADD COLUMN IF NOT EXISTS milestone_id UUID REFERENCES milestones(id) ON DELETE SET NULL;
-CREATE INDEX IF NOT EXISTS pull_requests_milestone_id_idx ON pull_requests(milestone_id);
-
-CREATE TABLE issue_labels (
-    issue_id UUID NOT NULL REFERENCES issues(id) ON DELETE CASCADE,
-    label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
-    PRIMARY KEY (issue_id, label_id)
-);
-CREATE INDEX issue_labels_label_id_idx ON issue_labels(label_id);
-
-CREATE TABLE pull_labels (
-    pull_id UUID NOT NULL REFERENCES pull_requests(id) ON DELETE CASCADE,
-    label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
-    PRIMARY KEY (pull_id, label_id)
-);
-CREATE INDEX pull_labels_label_id_idx ON pull_labels(label_id);
migrations/018_pull_reviews.sql +0 −9
diff --git a/migrations/018_pull_reviews.sql b/migrations/018_pull_reviews.sql
deleted file mode 100644
index bbc690f..0000000
--- a/migrations/018_pull_reviews.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-CREATE TABLE IF NOT EXISTS pull_reviews (
-  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-  pull_id UUID NOT NULL REFERENCES pull_requests(id) ON DELETE CASCADE,
-  reviewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-  state TEXT NOT NULL CHECK (state IN ('approved','changes_requested','commented')),
-  body TEXT NOT NULL DEFAULT '',
-  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
-);
-CREATE INDEX IF NOT EXISTS pull_reviews_pull_id_idx ON pull_reviews(pull_id);
src/db/deploy_keys.rs +0 −98
diff --git a/src/db/deploy_keys.rs b/src/db/deploy_keys.rs
deleted file mode 100644
index d538af3..0000000
--- a/src/db/deploy_keys.rs
+++ /dev/null
@@ -1,98 +0,0 @@
-//! Repo-scoped SSH deploy keys (read-only or read-write).
-
-use anyhow::Result;
-use chrono::{DateTime, Utc};
-use serde::Serialize;
-use sqlx::{FromRow, PgPool};
-use uuid::Uuid;
-
-#[derive(Debug, Clone, FromRow, Serialize)]
-pub struct DeployKey {
-    pub id: Uuid,
-    pub repo_id: Uuid,
-    pub name: String,
-    pub public_key: String,
-    pub fingerprint: String,
-    pub read_only: bool,
-    pub created_by: Option<Uuid>,
-    pub created_at: DateTime<Utc>,
-}
-
-impl DeployKey {
-    pub fn permission_label(&self) -> &'static str {
-        if self.read_only {
-            "Read-only"
-        } else {
-            "Read-write"
-        }
-    }
-}
-
-pub async fn add_deploy_key(
-    pool: &PgPool,
-    repo_id: Uuid,
-    name: &str,
-    public_key: &str,
-    fingerprint: &str,
-    read_only: bool,
-    created_by: Option<Uuid>,
-) -> Result<DeployKey> {
-    let clash: Option<(Uuid,)> = sqlx::query_as(
-        "SELECT id FROM ssh_keys WHERE rtrim(fingerprint, '=') = rtrim($1, '=') LIMIT 1",
-    )
-    .bind(fingerprint)
-    .fetch_optional(pool)
-    .await?;
-    if clash.is_some() {
-        anyhow::bail!("fingerprint already registered as a user SSH key");
-    }
-    Ok(sqlx::query_as::<_, DeployKey>(
-        r#"
-        INSERT INTO deploy_keys (repo_id, name, public_key, fingerprint, read_only, created_by)
-        VALUES ($1, $2, $3, $4, $5, $6)
-        RETURNING *
-        "#,
-    )
-    .bind(repo_id)
-    .bind(name)
-    .bind(public_key)
-    .bind(fingerprint)
-    .bind(read_only)
-    .bind(created_by)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn list_deploy_keys(pool: &PgPool, repo_id: Uuid) -> Result<Vec<DeployKey>> {
-    Ok(sqlx::query_as::<_, DeployKey>(
-        "SELECT * FROM deploy_keys WHERE repo_id = $1 ORDER BY created_at",
-    )
-    .bind(repo_id)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn delete_deploy_key(pool: &PgPool, repo_id: Uuid, id: Uuid) -> Result<()> {
-    sqlx::query("DELETE FROM deploy_keys WHERE id = $1 AND repo_id = $2")
-        .bind(id)
-        .bind(repo_id)
-        .execute(pool)
-        .await?;
-    Ok(())
-}
-
-pub async fn deploy_key_by_fingerprint(
-    pool: &PgPool,
-    fingerprint: &str,
-) -> Result<Option<DeployKey>> {
-    let normalized = fingerprint.trim_end_matches('=');
-    Ok(sqlx::query_as::<_, DeployKey>(
-        r#"
-        SELECT * FROM deploy_keys
-        WHERE rtrim(fingerprint, '=') = $1
-        "#,
-    )
-    .bind(normalized)
-    .fetch_optional(pool)
-    .await?)
-}
\ No newline at end of file
src/db/mod.rs +0 −3
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 2b7d249..8d84a74 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -1,9 +1,6 @@
-pub mod deploy_keys;
 pub mod models;
 pub mod queries;
 
-pub use deploy_keys::DeployKey;
-
 use anyhow::{Context, Result};
 use sqlx::postgres::PgPoolOptions;
 use sqlx::PgPool;
src/db/models.rs +0 −131
diff --git a/src/db/models.rs b/src/db/models.rs
index 42d2653..874e00c 100644
--- a/src/db/models.rs
+++ b/src/db/models.rs
@@ -17,21 +17,10 @@ pub struct User {
     pub is_suspended: bool,
     pub show_email: bool,
     pub vigilant_mode: bool,
-    /// `light`, `dark`, or `system` (follows prefers-color-scheme).
-    pub theme: String,
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
 }
 
-impl User {
-    pub fn theme_pref(&self) -> &str {
-        match self.theme.as_str() {
-            "light" | "dark" => self.theme.as_str(),
-            _ => "system",
-        }
-    }
-}
-
 #[derive(Debug, Clone, FromRow)]
 pub struct UserMfa {
     pub user_id: Uuid,
@@ -112,19 +101,6 @@ pub struct BranchRule {
     pub created_at: DateTime<Utc>,
 }
 
-#[derive(Debug, Clone, FromRow, Serialize)]
-pub struct RepoMirror {
-    pub id: Uuid,
-    pub repo_id: Uuid,
-    pub remote_url: String,
-    pub enabled: bool,
-    pub last_synced_at: Option<DateTime<Utc>>,
-    pub last_error: Option<String>,
-    pub created_by: Option<Uuid>,
-    pub created_at: DateTime<Utc>,
-    pub updated_at: DateTime<Utc>,
-}
-
 #[derive(Debug, Clone, FromRow)]
 pub struct CommentReaction {
     pub comment_id: Uuid,
@@ -182,19 +158,6 @@ pub struct ActivityEvent {
     pub created_at: DateTime<Utc>,
 }
 
-/// Per-user security / account audit entry (not the social activity feed).
-#[derive(Debug, Clone, FromRow)]
-pub struct AuditLog {
-    pub id: i64,
-    pub user_id: Uuid,
-    pub actor_id: Option<Uuid>,
-    pub action: String,
-    pub ip: Option<String>,
-    pub user_agent: Option<String>,
-    pub metadata: serde_json::Value,
-    pub created_at: DateTime<Utc>,
-}
-
 #[derive(Debug, Clone, FromRow)]
 pub struct Issue {
     pub id: Uuid,
@@ -204,7 +167,6 @@ pub struct Issue {
     pub title: String,
     pub body: String,
     pub state: String,
-    pub milestone_id: Option<Uuid>,
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
     pub closed_at: Option<DateTime<Utc>>,
@@ -223,7 +185,6 @@ pub struct PullRequest {
     pub target_branch: String,
     pub merge_commit: Option<String>,
     pub source_repo_id: Option<Uuid>,
-    pub milestone_id: Option<Uuid>,
     pub created_at: DateTime<Utc>,
     pub updated_at: DateTime<Utc>,
     pub merged_at: Option<DateTime<Utc>>,
@@ -231,51 +192,6 @@ pub struct PullRequest {
 }
 
 #[derive(Debug, Clone, FromRow)]
-pub struct Label {
-    pub id: Uuid,
-    pub repo_id: Uuid,
-    pub name: String,
-    pub color: String,
-    pub description: String,
-    pub created_at: DateTime<Utc>,
-}
-
-impl Label {
-    pub fn bg_color(&self) -> String {
-        format!("#{}", self.color.trim_start_matches('#'))
-    }
-
-    pub fn text_color(&self) -> &'static str {
-        let hex = self.color.trim_start_matches('#');
-        if hex.len() != 6 {
-            return "#ffffff";
-        }
-        let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32;
-        let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32;
-        let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32;
-        let lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255.0;
-        if lum > 0.55 {
-            "#111111"
-        } else {
-            "#ffffff"
-        }
-    }
-}
-
-#[derive(Debug, Clone, FromRow)]
-pub struct Milestone {
-    pub id: Uuid,
-    pub repo_id: Uuid,
-    pub title: String,
-    pub description: String,
-    pub due_on: Option<NaiveDate>,
-    pub state: String,
-    pub created_at: DateTime<Utc>,
-    pub updated_at: DateTime<Utc>,
-    pub closed_at: Option<DateTime<Utc>>,
-}
-
-#[derive(Debug, Clone, FromRow)]
 pub struct Comment {
     pub id: Uuid,
     pub repo_id: Uuid,
@@ -288,16 +204,6 @@ pub struct Comment {
 }
 
 #[derive(Debug, Clone, FromRow)]
-pub struct PullReview {
-    pub id: Uuid,
-    pub pull_id: Uuid,
-    pub reviewer_id: Uuid,
-    pub state: String,
-    pub body: String,
-    pub created_at: DateTime<Utc>,
-}
-
-#[derive(Debug, Clone, FromRow)]
 pub struct Release {
     pub id: Uuid,
     pub repo_id: Uuid,
@@ -328,43 +234,6 @@ pub struct CommitDay {
     pub count: i32,
 }
 
-#[derive(Debug, Clone, FromRow, Serialize)]
-pub struct Webhook {
-    pub id: Uuid,
-    pub repo_id: Uuid,
-    pub url: String,
-    pub secret: String,
-    pub events: Vec<String>,
-    pub active: bool,
-    pub created_at: DateTime<Utc>,
-}
-
-#[derive(Debug, Clone, FromRow)]
-pub struct WebhookDelivery {
-    pub id: i64,
-    pub webhook_id: Uuid,
-    pub event: String,
-    pub action: String,
-    pub success: bool,
-    pub status_code: Option<i32>,
-    pub error: Option<String>,
-    pub duration_ms: Option<i32>,
-    pub created_at: DateTime<Utc>,
-}
-
-#[derive(Debug, Clone, FromRow, Serialize)]
-pub struct Notification {
-    pub id: Uuid,
-    pub user_id: Uuid,
-    pub kind: String,
-    pub title: String,
-    pub body: String,
-    pub href: String,
-    pub repo_id: Option<Uuid>,
-    pub read_at: Option<DateTime<Utc>>,
-    pub created_at: DateTime<Utc>,
-}
-
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase")]
 pub enum Access {
src/db/queries.rs +33 −1027
diff --git a/src/db/queries.rs b/src/db/queries.rs
index 8df7cab..340cf0a 100644
--- a/src/db/queries.rs
+++ b/src/db/queries.rs
@@ -1,7 +1,7 @@
 use super::models::*;
 use anyhow::Result;
 use chrono::{DateTime, NaiveDate, Utc};
-use sqlx::{FromRow, PgPool};
+use sqlx::PgPool;
 use uuid::Uuid;
 
 pub async fn upsert_user_from_oidc(
@@ -1326,29 +1326,6 @@ pub async fn user_owns_email(pool: &PgPool, user_id: Uuid, email: &str) -> Resul
     Ok(row.is_some())
 }
 
-/// Resolve a kitgit username from a commit author email (primary or secondary).
-pub async fn username_by_email(pool: &PgPool, email: &str) -> Result<Option<String>> {
-    let email = email.trim().to_ascii_lowercase();
-    if email.is_empty() {
-        return Ok(None);
-    }
-    let row: Option<(String,)> = sqlx::query_as(
-        r#"
-        SELECT u.username FROM users u
-        WHERE lower(u.email) = $1
-        UNION
-        SELECT u.username FROM users u
-        JOIN user_emails e ON e.user_id = u.id
-        WHERE lower(e.email) = $1
-        LIMIT 1
-        "#,
-    )
-    .bind(&email)
-    .fetch_optional(pool)
-    .await?;
-    Ok(row.map(|(u,)| u))
-}
-
 pub async fn list_all_gpg_keys(pool: &PgPool) -> Result<Vec<GpgKey>> {
     Ok(sqlx::query_as::<_, GpgKey>("SELECT * FROM gpg_keys ORDER BY created_at DESC")
         .fetch_all(pool)
@@ -1522,50 +1499,22 @@ pub async fn create_issue(
 }
 
 pub async fn list_issues(pool: &PgPool, repo_id: Uuid, state: Option<&str>) -> Result<Vec<Issue>> {
-    list_issues_filtered(pool, repo_id, state, None, None).await
-}
-
-pub async fn list_issues_filtered(
-    pool: &PgPool,
-    repo_id: Uuid,
-    state: Option<&str>,
-    label_id: Option<Uuid>,
-    milestone_id: Option<Uuid>,
-) -> Result<Vec<Issue>> {
-    Ok(sqlx::query_as::<_, Issue>(
-        r#"
-        SELECT i.* FROM issues i
-        WHERE i.repo_id = $1
-          AND ($2::text IS NULL OR i.state = $2)
-          AND ($3::uuid IS NULL OR i.milestone_id = $3)
-          AND (
-            $4::uuid IS NULL OR EXISTS (
-              SELECT 1 FROM issue_labels il
-              WHERE il.issue_id = i.id AND il.label_id = $4
-            )
-          )
-        ORDER BY i.number DESC
-        "#,
-    )
-    .bind(repo_id)
-    .bind(state)
-    .bind(milestone_id)
-    .bind(label_id)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn set_issue_milestone(
-    pool: &PgPool,
-    issue_id: Uuid,
-    milestone_id: Option<Uuid>,
-) -> Result<()> {
-    sqlx::query("UPDATE issues SET milestone_id = $2, updated_at = now() WHERE id = $1")
-        .bind(issue_id)
-        .bind(milestone_id)
-        .execute(pool)
-        .await?;
-    Ok(())
+    if let Some(s) = state {
+        Ok(sqlx::query_as::<_, Issue>(
+            "SELECT * FROM issues WHERE repo_id = $1 AND state = $2 ORDER BY number DESC",
+        )
+        .bind(repo_id)
+        .bind(s)
+        .fetch_all(pool)
+        .await?)
+    } else {
+        Ok(sqlx::query_as::<_, Issue>(
+            "SELECT * FROM issues WHERE repo_id = $1 ORDER BY number DESC",
+        )
+        .bind(repo_id)
+        .fetch_all(pool)
+        .await?)
+    }
 }
 
 pub async fn get_issue(pool: &PgPool, repo_id: Uuid, number: i32) -> Result<Option<Issue>> {
@@ -1638,50 +1587,22 @@ pub async fn create_pull_ex(
 }
 
 pub async fn list_pulls(pool: &PgPool, repo_id: Uuid, state: Option<&str>) -> Result<Vec<PullRequest>> {
-    list_pulls_filtered(pool, repo_id, state, None, None).await
-}
-
-pub async fn list_pulls_filtered(
-    pool: &PgPool,
-    repo_id: Uuid,
-    state: Option<&str>,
-    label_id: Option<Uuid>,
-    milestone_id: Option<Uuid>,
-) -> Result<Vec<PullRequest>> {
-    Ok(sqlx::query_as::<_, PullRequest>(
-        r#"
-        SELECT p.* FROM pull_requests p
-        WHERE p.repo_id = $1
-          AND ($2::text IS NULL OR p.state = $2)
-          AND ($3::uuid IS NULL OR p.milestone_id = $3)
-          AND (
-            $4::uuid IS NULL OR EXISTS (
-              SELECT 1 FROM pull_labels pl
-              WHERE pl.pull_id = p.id AND pl.label_id = $4
-            )
-          )
-        ORDER BY p.number DESC
-        "#,
-    )
-    .bind(repo_id)
-    .bind(state)
-    .bind(milestone_id)
-    .bind(label_id)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn set_pull_milestone(
-    pool: &PgPool,
-    pull_id: Uuid,
-    milestone_id: Option<Uuid>,
-) -> Result<()> {
-    sqlx::query("UPDATE pull_requests SET milestone_id = $2, updated_at = now() WHERE id = $1")
-        .bind(pull_id)
-        .bind(milestone_id)
-        .execute(pool)
-        .await?;
-    Ok(())
+    if let Some(s) = state {
+        Ok(sqlx::query_as::<_, PullRequest>(
+            "SELECT * FROM pull_requests WHERE repo_id = $1 AND state = $2 ORDER BY number DESC",
+        )
+        .bind(repo_id)
+        .bind(s)
+        .fetch_all(pool)
+        .await?)
+    } else {
+        Ok(sqlx::query_as::<_, PullRequest>(
+            "SELECT * FROM pull_requests WHERE repo_id = $1 ORDER BY number DESC",
+        )
+        .bind(repo_id)
+        .fetch_all(pool)
+        .await?)
+    }
 }
 
 pub async fn get_pull(pool: &PgPool, repo_id: Uuid, number: i32) -> Result<Option<PullRequest>> {
@@ -1762,36 +1683,6 @@ pub async fn list_pull_comments(pool: &PgPool, pull_id: Uuid) -> Result<Vec<Comm
     .await?)
 }
 
-pub async fn create_review(
-    pool: &PgPool,
-    pull_id: Uuid,
-    reviewer_id: Uuid,
-    state: &str,
-    body: &str,
-) -> Result<PullReview> {
-    Ok(sqlx::query_as::<_, PullReview>(
-        r#"
-        INSERT INTO pull_reviews (pull_id, reviewer_id, state, body)
-        VALUES ($1, $2, $3, $4) RETURNING *
-        "#,
-    )
-    .bind(pull_id)
-    .bind(reviewer_id)
-    .bind(state)
-    .bind(body)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn list_reviews_for_pull(pool: &PgPool, pull_id: Uuid) -> Result<Vec<PullReview>> {
-    Ok(sqlx::query_as::<_, PullReview>(
-        "SELECT * FROM pull_reviews WHERE pull_id = $1 ORDER BY created_at",
-    )
-    .bind(pull_id)
-    .fetch_all(pool)
-    .await?)
-}
-
 pub async fn create_release(
     pool: &PgPool,
     repo_id: Uuid,
@@ -2359,19 +2250,6 @@ pub async fn update_privacy(
     .await?)
 }
 
-pub async fn update_user_theme(pool: &PgPool, id: Uuid, theme: &str) -> Result<User> {
-    Ok(sqlx::query_as::<_, User>(
-        r#"
-        UPDATE users SET theme = $2, updated_at = now()
-        WHERE id = $1 RETURNING *
-        "#,
-    )
-    .bind(id)
-    .bind(theme)
-    .fetch_one(pool)
-    .await?)
-}
-
 pub async fn list_user_emails(pool: &PgPool, user_id: Uuid) -> Result<Vec<UserEmail>> {
     Ok(sqlx::query_as::<_, UserEmail>(
         "SELECT * FROM user_emails WHERE user_id = $1 ORDER BY is_primary DESC, email",
@@ -2404,36 +2282,6 @@ pub async fn delete_user_email(pool: &PgPool, id: Uuid, user_id: Uuid) -> Result
     Ok(())
 }
 
-pub async fn set_primary_email(pool: &PgPool, user_id: Uuid, email_id: Uuid) -> Result<()> {
-    let mut tx = pool.begin().await?;
-    let row: Option<(String,)> = sqlx::query_as(
-        "SELECT email FROM user_emails WHERE id = $1 AND user_id = $2",
-    )
-    .bind(email_id)
-    .bind(user_id)
-    .fetch_optional(&mut *tx)
-    .await?;
-    let Some((email,)) = row else {
-        anyhow::bail!("email not found");
-    };
-    sqlx::query("UPDATE user_emails SET is_primary = false WHERE user_id = $1")
-        .bind(user_id)
-        .execute(&mut *tx)
-        .await?;
-    sqlx::query("UPDATE user_emails SET is_primary = true WHERE id = $1 AND user_id = $2")
-        .bind(email_id)
-        .bind(user_id)
-        .execute(&mut *tx)
-        .await?;
-    sqlx::query("UPDATE users SET email = $2, updated_at = now() WHERE id = $1")
-        .bind(user_id)
-        .bind(&email)
-        .execute(&mut *tx)
-        .await?;
-    tx.commit().await?;
-    Ok(())
-}
-
 pub async fn ensure_primary_email(pool: &PgPool, user_id: Uuid, email: &str) -> Result<()> {
     if email.trim().is_empty() {
         return Ok(());
@@ -2680,7 +2528,6 @@ pub async fn export_user_data(pool: &PgPool, user_id: Uuid) -> Result<serde_json
             "bio": user.bio,
             "show_email": user.show_email,
             "vigilant_mode": user.vigilant_mode,
-            "theme": user.theme,
             "created_at": user.created_at,
         },
         "emails": emails,
@@ -2749,844 +2596,3 @@ pub async fn lfs_object_size(pool: &PgPool, oid: &str) -> Result<Option<i64>> {
         .await?;
     Ok(row.map(|r| r.0))
 }
-
-// ── repo mirrors ─────────────────────────────────────────────────────────────
-
-pub async fn get_repo_mirror(pool: &PgPool, repo_id: Uuid) -> Result<Option<RepoMirror>> {
-    Ok(sqlx::query_as::<_, RepoMirror>(
-        "SELECT * FROM repo_mirrors WHERE repo_id = $1",
-    )
-    .bind(repo_id)
-    .fetch_optional(pool)
-    .await?)
-}
-
-pub async fn upsert_repo_mirror(
-    pool: &PgPool,
-    repo_id: Uuid,
-    remote_url: &str,
-    enabled: bool,
-    created_by: Uuid,
-) -> Result<RepoMirror> {
-    Ok(sqlx::query_as::<_, RepoMirror>(
-        r#"
-        INSERT INTO repo_mirrors (repo_id, remote_url, enabled, created_by)
-        VALUES ($1, $2, $3, $4)
-        ON CONFLICT (repo_id) DO UPDATE SET
-            remote_url = EXCLUDED.remote_url,
-            enabled = EXCLUDED.enabled,
-            updated_at = now()
-        RETURNING *
-        "#,
-    )
-    .bind(repo_id)
-    .bind(remote_url)
-    .bind(enabled)
-    .bind(created_by)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn set_mirror_sync_result(
-    pool: &PgPool,
-    repo_id: Uuid,
-    ok: bool,
-    error: Option<&str>,
-) -> Result<RepoMirror> {
-    if ok {
-        Ok(sqlx::query_as::<_, RepoMirror>(
-            r#"
-            UPDATE repo_mirrors
-            SET last_synced_at = now(), last_error = NULL, updated_at = now()
-            WHERE repo_id = $1
-            RETURNING *
-            "#,
-        )
-        .bind(repo_id)
-        .fetch_one(pool)
-        .await?)
-    } else {
-        Ok(sqlx::query_as::<_, RepoMirror>(
-            r#"
-            UPDATE repo_mirrors
-            SET last_error = $2, updated_at = now()
-            WHERE repo_id = $1
-            RETURNING *
-            "#,
-        )
-        .bind(repo_id)
-        .bind(error)
-        .fetch_one(pool)
-        .await?)
-    }
-}
-
-pub async fn delete_repo_mirror(pool: &PgPool, repo_id: Uuid) -> Result<()> {
-    sqlx::query("DELETE FROM repo_mirrors WHERE repo_id = $1")
-        .bind(repo_id)
-        .execute(pool)
-        .await?;
-    Ok(())
-}
-
-// ── per-user audit log ───────────────────────────────────────────────────────
-
-pub async fn record_audit_log(
-    pool: &PgPool,
-    user_id: Uuid,
-    actor_id: Option<Uuid>,
-    action: &str,
-    ip: Option<&str>,
-    user_agent: Option<&str>,
-    metadata: serde_json::Value,
-) -> Result<()> {
-    sqlx::query(
-        r#"
-        INSERT INTO audit_log (user_id, actor_id, action, ip, user_agent, metadata)
-        VALUES ($1, $2, $3, $4, $5, $6)
-        "#,
-    )
-    .bind(user_id)
-    .bind(actor_id)
-    .bind(action)
-    .bind(ip)
-    .bind(user_agent)
-    .bind(metadata)
-    .execute(pool)
-    .await?;
-    Ok(())
-}
-
-pub async fn list_audit_log_for_user(
-    pool: &PgPool,
-    user_id: Uuid,
-    limit: i64,
-) -> Result<Vec<AuditLog>> {
-    Ok(sqlx::query_as::<_, AuditLog>(
-        "SELECT * FROM audit_log WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2",
-    )
-    .bind(user_id)
-    .bind(limit)
-    .fetch_all(pool)
-    .await?)
-}
-
-// ── webhooks ─────────────────────────────────────────────────────────────────
-
-pub async fn list_webhooks(pool: &PgPool, repo_id: Uuid) -> Result<Vec<Webhook>> {
-    Ok(sqlx::query_as::<_, Webhook>(
-        "SELECT * FROM webhooks WHERE repo_id = $1 ORDER BY created_at DESC",
-    )
-    .bind(repo_id)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn list_active_webhooks_for_event(
-    pool: &PgPool,
-    repo_id: Uuid,
-    event: &str,
-) -> Result<Vec<Webhook>> {
-    Ok(sqlx::query_as::<_, Webhook>(
-        r#"
-        SELECT * FROM webhooks
-        WHERE repo_id = $1 AND active = TRUE AND $2 = ANY(events)
-        ORDER BY created_at
-        "#,
-    )
-    .bind(repo_id)
-    .bind(event)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn get_webhook(pool: &PgPool, id: Uuid, repo_id: Uuid) -> Result<Option<Webhook>> {
-    Ok(sqlx::query_as::<_, Webhook>(
-        "SELECT * FROM webhooks WHERE id = $1 AND repo_id = $2",
-    )
-    .bind(id)
-    .bind(repo_id)
-    .fetch_optional(pool)
-    .await?)
-}
-
-pub async fn create_webhook(
-    pool: &PgPool,
-    repo_id: Uuid,
-    url: &str,
-    secret: &str,
-    events: &[String],
-) -> Result<Webhook> {
-    Ok(sqlx::query_as::<_, Webhook>(
-        r#"
-        INSERT INTO webhooks (repo_id, url, secret, events)
-        VALUES ($1, $2, $3, $4)
-        RETURNING *
-        "#,
-    )
-    .bind(repo_id)
-    .bind(url)
-    .bind(secret)
-    .bind(events)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn set_webhook_active(
-    pool: &PgPool,
-    id: Uuid,
-    repo_id: Uuid,
-    active: bool,
-) -> Result<()> {
-    sqlx::query("UPDATE webhooks SET active = $3 WHERE id = $1 AND repo_id = $2")
-        .bind(id)
-        .bind(repo_id)
-        .bind(active)
-        .execute(pool)
-        .await?;
-    Ok(())
-}
-
-pub async fn delete_webhook(pool: &PgPool, id: Uuid, repo_id: Uuid) -> Result<()> {
-    sqlx::query("DELETE FROM webhooks WHERE id = $1 AND repo_id = $2")
-        .bind(id)
-        .bind(repo_id)
-        .execute(pool)
-        .await?;
-    Ok(())
-}
-
-pub async fn record_webhook_delivery(
-    pool: &PgPool,
-    webhook_id: Uuid,
-    event: &str,
-    action: &str,
-    success: bool,
-    status_code: Option<i32>,
-    error: Option<&str>,
-    duration_ms: i32,
-) -> Result<()> {
-    sqlx::query(
-        r#"
-        INSERT INTO webhook_deliveries
-            (webhook_id, event, action, success, status_code, error, duration_ms)
-        VALUES ($1, $2, $3, $4, $5, $6, $7)
-        "#,
-    )
-    .bind(webhook_id)
-    .bind(event)
-    .bind(action)
-    .bind(success)
-    .bind(status_code)
-    .bind(error)
-    .bind(duration_ms)
-    .execute(pool)
-    .await?;
-    Ok(())
-}
-
-pub async fn list_recent_webhook_deliveries(
-    pool: &PgPool,
-    repo_id: Uuid,
-    limit: i64,
-) -> Result<Vec<(WebhookDelivery, String)>> {
-    #[derive(sqlx::FromRow)]
-    struct Row {
-        id: i64,
-        webhook_id: Uuid,
-        event: String,
-        action: String,
-        success: bool,
-        status_code: Option<i32>,
-        error: Option<String>,
-        duration_ms: Option<i32>,
-        created_at: DateTime<Utc>,
-        webhook_url: String,
-    }
-    let rows = sqlx::query_as::<_, Row>(
-        r#"
-        SELECT d.id, d.webhook_id, d.event, d.action, d.success, d.status_code,
-               d.error, d.duration_ms, d.created_at, w.url AS webhook_url
-        FROM webhook_deliveries d
-        JOIN webhooks w ON w.id = d.webhook_id
-        WHERE w.repo_id = $1
-        ORDER BY d.created_at DESC
-        LIMIT $2
-        "#,
-    )
-    .bind(repo_id)
-    .bind(limit)
-    .fetch_all(pool)
-    .await?;
-    Ok(rows
-        .into_iter()
-        .map(|r| {
-            (
-                WebhookDelivery {
-                    id: r.id,
-                    webhook_id: r.webhook_id,
-                    event: r.event,
-                    action: r.action,
-                    success: r.success,
-                    status_code: r.status_code,
-                    error: r.error,
-                    duration_ms: r.duration_ms,
-                    created_at: r.created_at,
-                },
-                r.webhook_url,
-            )
-        })
-        .collect())
-}
-
-// ── notifications ────────────────────────────────────────────────────────────
-
-pub async fn create_notification(
-    pool: &PgPool,
-    user_id: Uuid,
-    kind: &str,
-    title: &str,
-    body: &str,
-    href: &str,
-    repo_id: Option<Uuid>,
-) -> Result<Notification> {
-    Ok(sqlx::query_as::<_, Notification>(
-        r#"
-        INSERT INTO notifications (user_id, kind, title, body, href, repo_id)
-        VALUES ($1, $2, $3, $4, $5, $6)
-        RETURNING *
-        "#,
-    )
-    .bind(user_id)
-    .bind(kind)
-    .bind(title)
-    .bind(body)
-    .bind(href)
-    .bind(repo_id)
-    .fetch_one(pool)
-    .await?)
-}
-
-/// Fan out the same notification to many users, skipping `exclude_user_id`.
-pub async fn notify_users(
-    pool: &PgPool,
-    user_ids: &[Uuid],
-    exclude_user_id: Uuid,
-    kind: &str,
-    title: &str,
-    body: &str,
-    href: &str,
-    repo_id: Option<Uuid>,
-) -> Result<u64> {
-    let mut created = 0u64;
-    for &uid in user_ids {
-        if uid == exclude_user_id {
-            continue;
-        }
-        create_notification(pool, uid, kind, title, body, href, repo_id).await?;
-        created += 1;
-    }
-    Ok(created)
-}
-
-pub async fn list_repo_watcher_ids(pool: &PgPool, repo_id: Uuid) -> Result<Vec<Uuid>> {
-    let rows: Vec<(Uuid,)> =
-        sqlx::query_as("SELECT user_id FROM repo_watches WHERE repo_id = $1")
-            .bind(repo_id)
-            .fetch_all(pool)
-            .await?;
-    Ok(rows.into_iter().map(|r| r.0).collect())
-}
-
-pub async fn notify_repo_watchers(
-    pool: &PgPool,
-    repo_id: Uuid,
-    exclude_user_id: Uuid,
-    kind: &str,
-    title: &str,
-    body: &str,
-    href: &str,
-) -> Result<u64> {
-    let watchers = list_repo_watcher_ids(pool, repo_id).await?;
-    notify_users(
-        pool,
-        &watchers,
-        exclude_user_id,
-        kind,
-        title,
-        body,
-        href,
-        Some(repo_id),
-    )
-    .await
-}
-
-/// Issue author plus prior comment authors.
-pub async fn list_issue_participant_ids(pool: &PgPool, issue_id: Uuid) -> Result<Vec<Uuid>> {
-    let rows: Vec<(Uuid,)> = sqlx::query_as(
-        r#"
-        SELECT author_id FROM issues WHERE id = $1
-        UNION
-        SELECT DISTINCT author_id FROM comments WHERE issue_id = $1
-        "#,
-    )
-    .bind(issue_id)
-    .fetch_all(pool)
-    .await?;
-    Ok(rows.into_iter().map(|r| r.0).collect())
-}
-
-/// Pull author plus prior comment authors.
-pub async fn list_pull_participant_ids(pool: &PgPool, pull_id: Uuid) -> Result<Vec<Uuid>> {
-    let rows: Vec<(Uuid,)> = sqlx::query_as(
-        r#"
-        SELECT author_id FROM pull_requests WHERE id = $1
-        UNION
-        SELECT DISTINCT author_id FROM comments WHERE pull_id = $1
-        "#,
-    )
-    .bind(pull_id)
-    .fetch_all(pool)
-    .await?;
-    Ok(rows.into_iter().map(|r| r.0).collect())
-}
-
-pub async fn list_notifications(
-    pool: &PgPool,
-    user_id: Uuid,
-    limit: i64,
-) -> Result<Vec<Notification>> {
-    Ok(sqlx::query_as::<_, Notification>(
-        r#"
-        SELECT * FROM notifications
-        WHERE user_id = $1
-        ORDER BY created_at DESC
-        LIMIT $2
-        "#,
-    )
-    .bind(user_id)
-    .bind(limit)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn unread_notification_count(pool: &PgPool, user_id: Uuid) -> Result<i64> {
-    let (n,): (i64,) = sqlx::query_as(
-        "SELECT COUNT(*)::bigint FROM notifications WHERE user_id = $1 AND read_at IS NULL",
-    )
-    .bind(user_id)
-    .fetch_one(pool)
-    .await?;
-    Ok(n)
-}
-
-pub async fn mark_notification_read(
-    pool: &PgPool,
-    id: Uuid,
-    user_id: Uuid,
-) -> Result<Option<Notification>> {
-    Ok(sqlx::query_as::<_, Notification>(
-        r#"
-        UPDATE notifications
-        SET read_at = COALESCE(read_at, now())
-        WHERE id = $1 AND user_id = $2
-        RETURNING *
-        "#,
-    )
-    .bind(id)
-    .bind(user_id)
-    .fetch_optional(pool)
-    .await?)
-}
-
-pub async fn mark_all_notifications_read(pool: &PgPool, user_id: Uuid) -> Result<u64> {
-    let res = sqlx::query(
-        "UPDATE notifications SET read_at = now() WHERE user_id = $1 AND read_at IS NULL",
-    )
-    .bind(user_id)
-    .execute(pool)
-    .await?;
-    Ok(res.rows_affected())
-}
-
-// ── labels & milestones ──────────────────────────────────────────────────────
-
-#[derive(Debug, Clone, FromRow)]
-struct IssueLabelRow {
-    issue_id: Uuid,
-    id: Uuid,
-    repo_id: Uuid,
-    name: String,
-    color: String,
-    description: String,
-    created_at: DateTime<Utc>,
-}
-
-#[derive(Debug, Clone, FromRow)]
-struct PullLabelRow {
-    pull_id: Uuid,
-    id: Uuid,
-    repo_id: Uuid,
-    name: String,
-    color: String,
-    description: String,
-    created_at: DateTime<Utc>,
-}
-
-impl From<IssueLabelRow> for (Uuid, Label) {
-    fn from(r: IssueLabelRow) -> Self {
-        (
-            r.issue_id,
-            Label {
-                id: r.id,
-                repo_id: r.repo_id,
-                name: r.name,
-                color: r.color,
-                description: r.description,
-                created_at: r.created_at,
-            },
-        )
-    }
-}
-
-impl From<PullLabelRow> for (Uuid, Label) {
-    fn from(r: PullLabelRow) -> Self {
-        (
-            r.pull_id,
-            Label {
-                id: r.id,
-                repo_id: r.repo_id,
-                name: r.name,
-                color: r.color,
-                description: r.description,
-                created_at: r.created_at,
-            },
-        )
-    }
-}
-
-pub async fn list_labels(pool: &PgPool, repo_id: Uuid) -> Result<Vec<Label>> {
-    Ok(sqlx::query_as::<_, Label>(
-        "SELECT * FROM labels WHERE repo_id = $1 ORDER BY lower(name)",
-    )
-    .bind(repo_id)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn get_label(pool: &PgPool, repo_id: Uuid, id: Uuid) -> Result<Option<Label>> {
-    Ok(
-        sqlx::query_as::<_, Label>("SELECT * FROM labels WHERE repo_id = $1 AND id = $2")
-            .bind(repo_id)
-            .bind(id)
-            .fetch_optional(pool)
-            .await?,
-    )
-}
-
-pub async fn create_label(
-    pool: &PgPool,
-    repo_id: Uuid,
-    name: &str,
-    color: &str,
-    description: &str,
-) -> Result<Label> {
-    Ok(sqlx::query_as::<_, Label>(
-        r#"
-        INSERT INTO labels (repo_id, name, color, description)
-        VALUES ($1, $2, $3, $4) RETURNING *
-        "#,
-    )
-    .bind(repo_id)
-    .bind(name)
-    .bind(color)
-    .bind(description)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn update_label(
-    pool: &PgPool,
-    id: Uuid,
-    name: &str,
-    color: &str,
-    description: &str,
-) -> Result<Label> {
-    Ok(sqlx::query_as::<_, Label>(
-        r#"
-        UPDATE labels SET name = $2, color = $3, description = $4
-        WHERE id = $1 RETURNING *
-        "#,
-    )
-    .bind(id)
-    .bind(name)
-    .bind(color)
-    .bind(description)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn delete_label(pool: &PgPool, id: Uuid) -> Result<()> {
-    sqlx::query("DELETE FROM labels WHERE id = $1")
-        .bind(id)
-        .execute(pool)
-        .await?;
-    Ok(())
-}
-
-pub async fn list_milestones(
-    pool: &PgPool,
-    repo_id: Uuid,
-    state: Option<&str>,
-) -> Result<Vec<Milestone>> {
-    if let Some(s) = state {
-        Ok(sqlx::query_as::<_, Milestone>(
-            "SELECT * FROM milestones WHERE repo_id = $1 AND state = $2 ORDER BY due_on NULLS LAST, lower(title)",
-        )
-        .bind(repo_id)
-        .bind(s)
-        .fetch_all(pool)
-        .await?)
-    } else {
-        Ok(sqlx::query_as::<_, Milestone>(
-            "SELECT * FROM milestones WHERE repo_id = $1 ORDER BY state ASC, due_on NULLS LAST, lower(title)",
-        )
-        .bind(repo_id)
-        .fetch_all(pool)
-        .await?)
-    }
-}
-
-pub async fn list_open_milestones(pool: &PgPool, repo_id: Uuid) -> Result<Vec<Milestone>> {
-    list_milestones(pool, repo_id, Some("open")).await
-}
-
-pub async fn get_milestone(pool: &PgPool, repo_id: Uuid, id: Uuid) -> Result<Option<Milestone>> {
-    Ok(
-        sqlx::query_as::<_, Milestone>("SELECT * FROM milestones WHERE repo_id = $1 AND id = $2")
-            .bind(repo_id)
-            .bind(id)
-            .fetch_optional(pool)
-            .await?,
-    )
-}
-
-pub async fn create_milestone(
-    pool: &PgPool,
-    repo_id: Uuid,
-    title: &str,
-    description: &str,
-    due_on: Option<NaiveDate>,
-) -> Result<Milestone> {
-    Ok(sqlx::query_as::<_, Milestone>(
-        r#"
-        INSERT INTO milestones (repo_id, title, description, due_on)
-        VALUES ($1, $2, $3, $4) RETURNING *
-        "#,
-    )
-    .bind(repo_id)
-    .bind(title)
-    .bind(description)
-    .bind(due_on)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn update_milestone(
-    pool: &PgPool,
-    id: Uuid,
-    title: &str,
-    description: &str,
-    due_on: Option<NaiveDate>,
-) -> Result<Milestone> {
-    Ok(sqlx::query_as::<_, Milestone>(
-        r#"
-        UPDATE milestones SET
-            title = $2,
-            description = $3,
-            due_on = $4,
-            updated_at = now()
-        WHERE id = $1
-        RETURNING *
-        "#,
-    )
-    .bind(id)
-    .bind(title)
-    .bind(description)
-    .bind(due_on)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn set_milestone_state(pool: &PgPool, id: Uuid, state: &str) -> Result<Milestone> {
-    let closed = if state == "closed" {
-        Some(Utc::now())
-    } else {
-        None
-    };
-    Ok(sqlx::query_as::<_, Milestone>(
-        r#"
-        UPDATE milestones SET
-            state = $2,
-            closed_at = $3,
-            updated_at = now()
-        WHERE id = $1
-        RETURNING *
-        "#,
-    )
-    .bind(id)
-    .bind(state)
-    .bind(closed)
-    .fetch_one(pool)
-    .await?)
-}
-
-pub async fn delete_milestone(pool: &PgPool, id: Uuid) -> Result<()> {
-    sqlx::query("DELETE FROM milestones WHERE id = $1")
-        .bind(id)
-        .execute(pool)
-        .await?;
-    Ok(())
-}
-
-pub async fn list_issue_labels(pool: &PgPool, issue_id: Uuid) -> Result<Vec<Label>> {
-    Ok(sqlx::query_as::<_, Label>(
-        r#"
-        SELECT l.* FROM labels l
-        JOIN issue_labels il ON il.label_id = l.id
-        WHERE il.issue_id = $1
-        ORDER BY lower(l.name)
-        "#,
-    )
-    .bind(issue_id)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn list_pull_labels(pool: &PgPool, pull_id: Uuid) -> Result<Vec<Label>> {
-    Ok(sqlx::query_as::<_, Label>(
-        r#"
-        SELECT l.* FROM labels l
-        JOIN pull_labels pl ON pl.label_id = l.id
-        WHERE pl.pull_id = $1
-        ORDER BY lower(l.name)
-        "#,
-    )
-    .bind(pull_id)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn set_issue_labels(pool: &PgPool, issue_id: Uuid, label_ids: &[Uuid]) -> Result<()> {
-    let mut tx = pool.begin().await?;
-    sqlx::query("DELETE FROM issue_labels WHERE issue_id = $1")
-        .bind(issue_id)
-        .execute(&mut *tx)
-        .await?;
-    for id in label_ids {
-        sqlx::query(
-            "INSERT INTO issue_labels (issue_id, label_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
-        )
-        .bind(issue_id)
-        .bind(id)
-        .execute(&mut *tx)
-        .await?;
-    }
-    sqlx::query("UPDATE issues SET updated_at = now() WHERE id = $1")
-        .bind(issue_id)
-        .execute(&mut *tx)
-        .await?;
-    tx.commit().await?;
-    Ok(())
-}
-
-pub async fn set_pull_labels(pool: &PgPool, pull_id: Uuid, label_ids: &[Uuid]) -> Result<()> {
-    let mut tx = pool.begin().await?;
-    sqlx::query("DELETE FROM pull_labels WHERE pull_id = $1")
-        .bind(pull_id)
-        .execute(&mut *tx)
-        .await?;
-    for id in label_ids {
-        sqlx::query(
-            "INSERT INTO pull_labels (pull_id, label_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
-        )
-        .bind(pull_id)
-        .bind(id)
-        .execute(&mut *tx)
-        .await?;
-    }
-    sqlx::query("UPDATE pull_requests SET updated_at = now() WHERE id = $1")
-        .bind(pull_id)
-        .execute(&mut *tx)
-        .await?;
-    tx.commit().await?;
-    Ok(())
-}
-
-pub async fn labels_for_issues(pool: &PgPool, issue_ids: &[Uuid]) -> Result<Vec<(Uuid, Label)>> {
-    if issue_ids.is_empty() {
-        return Ok(Vec::new());
-    }
-    let rows = sqlx::query_as::<_, IssueLabelRow>(
-        r#"
-        SELECT il.issue_id, l.id, l.repo_id, l.name, l.color, l.description, l.created_at
-        FROM issue_labels il
-        JOIN labels l ON l.id = il.label_id
-        WHERE il.issue_id = ANY($1)
-        ORDER BY lower(l.name)
-        "#,
-    )
-    .bind(issue_ids)
-    .fetch_all(pool)
-    .await?;
-    Ok(rows.into_iter().map(Into::into).collect())
-}
-
-pub async fn labels_for_pulls(pool: &PgPool, pull_ids: &[Uuid]) -> Result<Vec<(Uuid, Label)>> {
-    if pull_ids.is_empty() {
-        return Ok(Vec::new());
-    }
-    let rows = sqlx::query_as::<_, PullLabelRow>(
-        r#"
-        SELECT pl.pull_id, l.id, l.repo_id, l.name, l.color, l.description, l.created_at
-        FROM pull_labels pl
-        JOIN labels l ON l.id = pl.label_id
-        WHERE pl.pull_id = ANY($1)
-        ORDER BY lower(l.name)
-        "#,
-    )
-    .bind(pull_ids)
-    .fetch_all(pool)
-    .await?;
-    Ok(rows.into_iter().map(Into::into).collect())
-}
-
-pub async fn get_milestones_by_ids(pool: &PgPool, ids: &[Uuid]) -> Result<Vec<Milestone>> {
-    if ids.is_empty() {
-        return Ok(Vec::new());
-    }
-    Ok(sqlx::query_as::<_, Milestone>(
-        "SELECT * FROM milestones WHERE id = ANY($1)",
-    )
-    .bind(ids)
-    .fetch_all(pool)
-    .await?)
-}
-
-pub async fn filter_repo_label_ids(
-    pool: &PgPool,
-    repo_id: Uuid,
-    label_ids: &[Uuid],
-) -> Result<Vec<Uuid>> {
-    if label_ids.is_empty() {
-        return Ok(Vec::new());
-    }
-    let rows: Vec<(Uuid,)> = sqlx::query_as(
-        "SELECT id FROM labels WHERE repo_id = $1 AND id = ANY($2)",
-    )
-    .bind(repo_id)
-    .bind(label_ids)
-    .fetch_all(pool)
-    .await?;
-    Ok(rows.into_iter().map(|r| r.0).collect())
-}
src/git/blame.rs +0 −134
diff --git a/src/git/blame.rs b/src/git/blame.rs
deleted file mode 100644
index 1b55286..0000000
--- a/src/git/blame.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-//! Blame and path history helpers (git2).
-
-use anyhow::{anyhow, Result};
-use git2::{BlameOptions, Commit, Oid, Repository as G2Repo, Sort};
-use std::collections::HashMap;
-use std::path::Path;
-
-use super::repo::{read_blob, resolve_ref, CommitInfo};
-
-/// One displayed line in a blame view.
-#[derive(Debug, Clone)]
-pub struct BlameLine {
-    pub line_no: usize,
-    pub content: String,
-    pub commit_id: String,
-    pub short_id: String,
-    pub author: String,
-    pub time: i64,
-    pub summary: String,
-    /// First line of a contiguous blame hunk (same final commit).
-    pub hunk_start: bool,
-}
-
-/// Commits that introduced a change to `path` (blob OID differs from parent), newest first.
-pub fn list_commits_for_path(
-    repo: &G2Repo,
-    reference: &str,
-    path: &str,
-    limit: usize,
-) -> Result<Vec<CommitInfo>> {
-    let oid = resolve_ref(repo, reference)?;
-    let mut revwalk = repo.revwalk()?;
-    revwalk.push(oid)?;
-    revwalk.set_sorting(Sort::TIME)?;
-    let mut out = Vec::new();
-    for id in revwalk {
-        let id = id?;
-        let c = repo.find_commit(id)?;
-        if !path_changed_in_commit(&c, path)? {
-            continue;
-        }
-        let author = c.author();
-        out.push(CommitInfo {
-            id: id.to_string(),
-            short_id: id.to_string()[..7.min(id.to_string().len())].to_string(),
-            message: c.summary().unwrap_or("").to_string(),
-            author: author.name().unwrap_or("").to_string(),
-            email: author.email().unwrap_or("").to_string(),
-            time: author.when().seconds(),
-        });
-        if out.len() >= limit {
-            break;
-        }
-    }
-    Ok(out)
-}
-
-fn path_changed_in_commit(commit: &Commit, path: &str) -> Result<bool> {
-    let tree = commit.tree()?;
-    let new_id = tree.get_path(Path::new(path)).ok().map(|e| e.id());
-    if commit.parent_count() == 0 {
-        return Ok(new_id.is_some());
-    }
-    let parent_tree = commit.parent(0)?.tree()?;
-    let old_id = parent_tree.get_path(Path::new(path)).ok().map(|e| e.id());
-    Ok(old_id != new_id)
-}
-
-/// Per-line blame for a text file at `reference`:`path`.
-pub fn blame_file(repo: &G2Repo, reference: &str, path: &str) -> Result<Vec<BlameLine>> {
-    let (data, binary) = read_blob(repo, reference, path)?;
-    if binary {
-        return Err(anyhow!("binary file"));
-    }
-    let oid = resolve_ref(repo, reference)?;
-    let mut opts = BlameOptions::new();
-    opts.newest_commit(oid);
-    let blame = repo.blame_file(Path::new(path), Some(&mut opts))?;
-
-    let text = String::from_utf8_lossy(&data);
-    let mut lines: Vec<&str> = text.split('\n').collect();
-    if text.ends_with('\n') {
-        lines.pop();
-    }
-
-    let mut summaries: HashMap<Oid, String> = HashMap::new();
-    let mut out = Vec::with_capacity(lines.len());
-    let mut prev_commit: Option<Oid> = None;
-
-    for (i, line) in lines.iter().enumerate() {
-        let line_no = i + 1;
-        let Some(hunk) = blame.get_line(line_no) else {
-            out.push(BlameLine {
-                line_no,
-                content: (*line).to_string(),
-                commit_id: String::new(),
-                short_id: String::new(),
-                author: String::new(),
-                time: 0,
-                summary: String::new(),
-                hunk_start: true,
-            });
-            continue;
-        };
-        let commit_oid = hunk.final_commit_id();
-        let commit_id = commit_oid.to_string();
-        let short_id = commit_id[..7.min(commit_id.len())].to_string();
-        let sig = hunk.final_signature();
-        let author = sig.name().unwrap_or("").to_string();
-        let time = sig.when().seconds();
-        let summary = summaries
-            .entry(commit_oid)
-            .or_insert_with(|| {
-                repo.find_commit(commit_oid)
-                    .ok()
-                    .and_then(|c| c.summary().map(|s| s.to_string()))
-                    .unwrap_or_default()
-            })
-            .clone();
-        let hunk_start = prev_commit != Some(commit_oid);
-        prev_commit = Some(commit_oid);
-        out.push(BlameLine {
-            line_no,
-            content: (*line).to_string(),
-            commit_id,
-            short_id,
-            author,
-            time,
-            summary,
-            hunk_start,
-        });
-    }
-    Ok(out)
-}
src/git/http.rs +0 −12
diff --git a/src/git/http.rs b/src/git/http.rs
index a816ab9..05a5e73 100644
--- a/src/git/http.rs
+++ b/src/git/http.rs
@@ -166,18 +166,6 @@ async fn post_push_hooks(
         .bind(repo.id)
         .execute(&state.pool)
         .await;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_PUSH,
-        "push".into(),
-        repo.clone(),
-        owner.to_string(),
-        Some(user.clone()),
-        serde_json::json!({
-            "ref": format!("refs/heads/{}", repo.default_branch),
-            "default_branch": repo.default_branch,
-        }),
-    );
     Ok(())
 }
 
src/git/mod.rs +0 −2
diff --git a/src/git/mod.rs b/src/git/mod.rs
index 9dc4ce8..b402e6d 100644
--- a/src/git/mod.rs
+++ b/src/git/mod.rs
@@ -1,4 +1,3 @@
-pub mod blame;
 pub mod http;
 pub mod languages;
 pub mod lfs;
@@ -6,6 +5,5 @@ pub mod repo;
 pub mod ssh;
 pub mod verify;
 
-pub use blame::{blame_file, list_commits_for_path};
 pub use repo::*;
 pub use verify::{extract_commit_signature, verify_commit_signature};
src/git/repo.rs +0 −84
diff --git a/src/git/repo.rs b/src/git/repo.rs
index 16cc994..06e53dd 100644
--- a/src/git/repo.rs
+++ b/src/git/repo.rs
@@ -79,83 +79,6 @@ pub fn rename_branch(repo: &G2Repo, old: &str, new: &str) -> Result<()> {
     Ok(())
 }
 
-/// Whether `url` is an allowed remote for pull-mirroring (no local/file paths).
-pub fn is_safe_mirror_url(url: &str) -> bool {
-    let url = url.trim();
-    if url.is_empty() || url.contains('\0') || url.contains('\n') || url.contains('\r') {
-        return false;
-    }
-    let lower = url.to_ascii_lowercase();
-    if lower.starts_with("file:")
-        || lower.starts_with('/')
-        || lower.starts_with("\\\\")
-        || (url.len() >= 2 && url.as_bytes()[1] == b':')
-    {
-        return false;
-    }
-    lower.starts_with("https://")
-        || lower.starts_with("http://")
-        || lower.starts_with("git://")
-        || lower.starts_with("ssh://")
-        || (url.contains('@') && url.contains(':') && !lower.contains("://"))
-}
-
-/// Fetch refs from `remote_url` into the existing bare repo (pull mirror).
-pub fn mirror_fetch(repos_root: &Path, owner: &str, name: &str, remote_url: &str) -> Result<()> {
-    if !is_safe_mirror_url(remote_url) {
-        return Err(anyhow!("unsupported or unsafe mirror URL"));
-    }
-    let path = bare_path(repos_root, owner, name);
-    if !path.exists() {
-        return Err(anyhow!("local bare repository missing"));
-    }
-    let path_str = path
-        .to_str()
-        .ok_or_else(|| anyhow!("repository path is not valid UTF-8"))?;
-    let url = remote_url.trim();
-
-    // Dedicated remote so we do not clobber a user's `origin`.
-    let _ = std::process::Command::new("git")
-        .args(["-C", path_str, "remote", "remove", "kitgit-mirror"])
-        .output();
-    let add = std::process::Command::new("git")
-        .args(["-C", path_str, "remote", "add", "kitgit-mirror", url])
-        .output()
-        .with_context(|| "git remote add")?;
-    if !add.status.success() {
-        return Err(anyhow!(
-            "git remote add failed: {}",
-            String::from_utf8_lossy(&add.stderr).trim()
-        ));
-    }
-
-    let fetch = std::process::Command::new("git")
-        .args([
-            "-C",
-            path_str,
-            "fetch",
-            "kitgit-mirror",
-            "+refs/heads/*:refs/heads/*",
-            "+refs/tags/*:refs/tags/*",
-            "--prune",
-            "--force",
-        ])
-        .output()
-        .with_context(|| "git fetch mirror")?;
-    if !fetch.status.success() {
-        return Err(anyhow!(
-            "git fetch failed: {}",
-            String::from_utf8_lossy(&fetch.stderr).trim()
-        ));
-    }
-
-    if let Ok(repo) = G2Repo::open_bare(&path) {
-        let _ = repo.config()?.set_bool("http.receivepack", true);
-        let _ = repo.config()?.set_bool("http.uploadpack", true);
-    }
-    Ok(())
-}
-
 /// Copy a bare repository on disk (for forks).
 pub fn clone_bare(src: &Path, dest: &Path) -> Result<()> {
     if dest.exists() {
@@ -391,13 +314,6 @@ pub fn list_commits(repo: &G2Repo, reference: &str, limit: usize) -> Result<Vec<
     Ok(out)
 }
 
-pub fn count_commits(repo: &G2Repo, reference: &str) -> Result<usize> {
-    let oid = resolve_ref(repo, reference)?;
-    let mut revwalk = repo.revwalk()?;
-    revwalk.push(oid)?;
-    Ok(revwalk.count())
-}
-
 pub fn get_commit(repo: &G2Repo, id: &str) -> Result<CommitInfo> {
     let oid = Oid::from_str(id)?;
     let c = repo.find_commit(oid)?;
src/git/ssh.rs +37 −104
diff --git a/src/git/ssh.rs b/src/git/ssh.rs
index 89f0474..95d2ba2 100644
--- a/src/git/ssh.rs
+++ b/src/git/ssh.rs
@@ -101,46 +101,28 @@ impl Server for SshServer {
             state: self.state.clone(),
             user_id: None,
             username: None,
-            deploy_key: None,
             stdin: None,
             receive_meta: None,
         }
     }
 }
 
-/// Authenticated via a repo-scoped deploy key (not a user key).
-struct DeployKeyAuth {
-    repo_id: Uuid,
-    read_only: bool,
-    created_by: Option<Uuid>,
-    name: String,
-}
-
 struct SshHandler {
     state: AppState,
     user_id: Option<Uuid>,
     username: Option<String>,
-    deploy_key: Option<DeployKeyAuth>,
     stdin: Option<Arc<Mutex<tokio::process::ChildStdin>>>,
     receive_meta: Option<(String, String)>,
 }
 
 impl SshHandler {
-    fn is_authenticated(&self) -> bool {
-        self.user_id.is_some() || self.deploy_key.is_some()
-    }
-
     /// Print `static/text.txt` (with `{user}` substituted) and close — no shell access.
     async fn greet_and_quit(
         &self,
         channel: ChannelId,
         session: &mut Session,
     ) -> Result<(), anyhow::Error> {
-        let username = self
-            .username
-            .as_deref()
-            .or_else(|| self.deploy_key.as_ref().map(|d| d.name.as_str()))
-            .unwrap_or("git");
+        let username = self.username.as_deref().unwrap_or("git");
         let path = self.state.config.static_dir.join("text.txt");
         let mut msg = tokio::fs::read_to_string(&path).await.unwrap_or_else(|_| {
             format!(
@@ -177,20 +159,6 @@ impl Handler for SshHandler {
             }
             self.user_id = Some(user.id);
             self.username = Some(user.username);
-            self.deploy_key = None;
-            return Ok(Auth::Accept);
-        }
-        if let Some(key) =
-            crate::db::deploy_keys::deploy_key_by_fingerprint(&self.state.pool, &fp).await?
-        {
-            self.user_id = None;
-            self.username = Some(format!("deploy:{}", key.name));
-            self.deploy_key = Some(DeployKeyAuth {
-                repo_id: key.repo_id,
-                read_only: key.read_only,
-                created_by: key.created_by,
-                name: key.name,
-            });
             return Ok(Auth::Accept);
         }
         tracing::debug!("ssh publickey rejected fp={fp}");
@@ -237,7 +205,7 @@ impl Handler for SshHandler {
         channel: ChannelId,
         session: &mut Session,
     ) -> Result<(), Self::Error> {
-        if !self.is_authenticated() {
+        if self.user_id.is_none() {
             session.channel_failure(channel)?;
             return Ok(());
         }
@@ -251,10 +219,10 @@ impl Handler for SshHandler {
         session: &mut Session,
     ) -> Result<(), Self::Error> {
         let cmd = String::from_utf8_lossy(data).to_string();
-        if !self.is_authenticated() {
+        let Some(user_id) = self.user_id else {
             session.channel_failure(channel)?;
             return Ok(());
-        }
+        };
 
         let (service, repo_path) = match parse_git_command(&cmd) {
             Ok(v) => v,
@@ -271,26 +239,11 @@ impl Handler for SshHandler {
             session.close(channel)?;
             return Ok(());
         };
-
-        let allowed = if let Some(dk) = &self.deploy_key {
-            // Deploy keys only unlock the single repo they were added to.
-            if dk.repo_id != repo.id {
-                false
-            } else {
-                match service {
-                    "git-upload-pack" => true,
-                    "git-receive-pack" => !dk.read_only && !repo.archived,
-                    _ => false,
-                }
-            }
-        } else {
-            let user_id = self.user_id.expect("authenticated without deploy key");
-            let access = queries::repo_access(&self.state.pool, &repo, Some(user_id)).await?;
-            match service {
-                "git-upload-pack" => access.can_read(),
-                "git-receive-pack" => access.can_write() && !repo.archived,
-                _ => false,
-            }
+        let access = queries::repo_access(&self.state.pool, &repo, Some(user_id)).await?;
+        let allowed = match service {
+            "git-upload-pack" => access.can_read(),
+            "git-receive-pack" => access.can_write() && !repo.archived,
+            _ => false,
         };
         if !allowed {
             session.data(channel, b"access denied\n".as_slice())?;
@@ -358,55 +311,35 @@ impl Handler for SshHandler {
             let mut g = stdin.lock().await;
             let _ = g.shutdown().await;
         }
-        let Some((owner, name)) = self.receive_meta.take() else {
-            return Ok(());
-        };
-        let Ok(Some((repo, _))) = queries::get_repo(&self.state.pool, &owner, &name).await else {
-            return Ok(());
-        };
-        let actor_id = self
-            .user_id
-            .or_else(|| self.deploy_key.as_ref().and_then(|d| d.created_by));
-        let mut sender = None;
-        if let Some(uid) = actor_id {
-            if let Ok(Some(user)) = queries::get_user_by_id(&self.state.pool, uid).await {
-                let _ = queries::record_activity(
-                    &self.state.pool,
-                    Some(user.id),
-                    Some(repo.id),
-                    "push",
-                    "pushed",
-                    serde_json::json!({}),
-                )
-                .await;
-                let _ = queries::bump_commit_activity(
-                    &self.state.pool,
-                    user.id,
-                    chrono::Utc::now().date_naive(),
-                    1,
-                )
-                .await;
-                sender = Some(user);
-            }
-        }
-        if let Ok(g) = crate::git::open_bare(&self.state.config.repos_dir(), &owner, &name) {
-            if let Ok(files) = crate::git::walk_files(&g, &repo.default_branch) {
-                let stats = crate::git::languages::detect_languages(&files);
-                let _ = queries::set_language_stats(&self.state.pool, repo.id, stats).await;
+        if let (Some(uid), Some((owner, name))) = (self.user_id, self.receive_meta.take()) {
+            if let Ok(Some((repo, _))) = queries::get_repo(&self.state.pool, &owner, &name).await {
+                if let Ok(Some(user)) = queries::get_user_by_id(&self.state.pool, uid).await {
+                    let _ = queries::record_activity(
+                        &self.state.pool,
+                        Some(user.id),
+                        Some(repo.id),
+                        "push",
+                        "pushed",
+                        serde_json::json!({}),
+                    )
+                    .await;
+                    let _ = queries::bump_commit_activity(
+                        &self.state.pool,
+                        user.id,
+                        chrono::Utc::now().date_naive(),
+                        1,
+                    )
+                    .await;
+                    if let Ok(g) = crate::git::open_bare(&self.state.config.repos_dir(), &owner, &name)
+                    {
+                        if let Ok(files) = crate::git::walk_files(&g, &repo.default_branch) {
+                            let stats = crate::git::languages::detect_languages(&files);
+                            let _ = queries::set_language_stats(&self.state.pool, repo.id, stats).await;
+                        }
+                    }
+                }
             }
         }
-        crate::webhooks::spawn_dispatch(
-            self.state.pool.clone(),
-            crate::webhooks::EVENT_PUSH,
-            "push".into(),
-            repo.clone(),
-            owner,
-            sender,
-            serde_json::json!({
-                "ref": format!("refs/heads/{}", repo.default_branch),
-                "default_branch": repo.default_branch,
-            }),
-        );
         Ok(())
     }
 }
@@ -444,4 +377,4 @@ fn split_repo_path(path: &str) -> Result<(String, String)> {
     let owner = parts.next().context("owner")?.to_string();
     let name = parts.next().context("name")?.to_string();
     Ok((owner, name))
-}
\ No newline at end of file
+}
src/main.rs +0 −1
diff --git a/src/main.rs b/src/main.rs
index 9be6dec..2a913a5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,7 +8,6 @@ mod mfa;
 mod og;
 mod state;
 mod web;
-mod webhooks;
 
 use crate::auth::AuthState;
 use crate::config::Config;
src/web/mod.rs +7 −189
diff --git a/src/web/mod.rs b/src/web/mod.rs
index 2c3401a..6c6969c 100644
--- a/src/web/mod.rs
+++ b/src/web/mod.rs
@@ -4,74 +4,16 @@ pub mod templates;
 
 use crate::git;
 use crate::state::AppState;
-use axum::body::{Body, to_bytes};
 use axum::extract::{DefaultBodyLimit, Request};
-use axum::http::{header, HeaderValue, StatusCode};
+use axum::http::{header, StatusCode};
 use axum::middleware::{from_fn, Next};
 use axum::response::{IntoResponse, Response};
 use axum::routing::{get, post};
 use axum::Router;
-use std::time::{Duration, Instant};
 use tower_http::limit::RequestBodyLimitLayer;
 use tower_http::services::ServeDir;
 
-const RENDER_MS_MARKER: &[u8] = b"<!--kg-render-ms-->";
-
-fn format_render_duration(elapsed: Duration) -> String {
-    let ms = elapsed.as_secs_f64() * 1000.0;
-    if ms < 10.0 {
-        format!("{ms:.1}ms")
-    } else {
-        format!("{:.0}ms", ms.round())
-    }
-}
-
-fn inject_render_marker(html: &[u8], label: &str) -> Option<Vec<u8>> {
-    let pos = html
-        .windows(RENDER_MS_MARKER.len())
-        .position(|window| window == RENDER_MS_MARKER)?;
-    let mut out = Vec::with_capacity(html.len() - RENDER_MS_MARKER.len() + label.len());
-    out.extend_from_slice(&html[..pos]);
-    out.extend_from_slice(label.as_bytes());
-    out.extend_from_slice(&html[pos + RENDER_MS_MARKER.len()..]);
-    Some(out)
-}
-
-/// Measure HTML page handler time and fill the layout footer timing marker.
-async fn inject_render_timing(req: Request, next: Next) -> Response {
-    let start = Instant::now();
-    let res = next.run(req).await;
-    let elapsed = start.elapsed();
-
-    let is_html = res
-        .headers()
-        .get(header::CONTENT_TYPE)
-        .and_then(|v| v.to_str().ok())
-        .is_some_and(|ct| ct.starts_with("text/html"));
-    if !is_html {
-        return res;
-    }
-
-    let (mut parts, body) = res.into_parts();
-    let Ok(bytes) = to_bytes(body, 16 * 1024 * 1024).await else {
-        return Response::from_parts(parts, Body::empty());
-    };
-
-    let label = format_render_duration(elapsed);
-    let body = match inject_render_marker(&bytes, &label) {
-        Some(updated) => {
-            parts.headers.remove(header::CONTENT_LENGTH);
-            if let Ok(len) = HeaderValue::from_str(&updated.len().to_string()) {
-                parts.headers.insert(header::CONTENT_LENGTH, len);
-            }
-            Body::from(updated)
-        }
-        None => Body::from(bytes),
-    };
-    Response::from_parts(parts, body)
-}
-
-/// Rewrite `/owner/repo.git` → `/owner/repo` and drop a trailing slash so
+/// Rewrite `/owner/repo.git` → `/owner/repo` and drop a trailing slash so
 /// browser pages and git smart-HTTP both work with classic forge URLs.
 fn normalize_repo_path(path: &str) -> String {
     let mut path = path.to_string();
@@ -131,7 +73,6 @@ pub fn app_router(state: AppState) -> Router {
         .route("/auth/callback", get(routes::auth_callback))
         .route("/auth/logout", get(routes::auth_logout))
         .route("/admin", get(routes::admin_panel))
-        .route("/admin/users/{username}/audit", get(routes::admin_user_audit))
         .route("/admin/users", post(routes::admin_set_user))
         .route("/admin/users/suspend", post(routes::admin_set_suspended))
         .route("/admin/motd", post(routes::admin_save_motd))
@@ -148,19 +89,6 @@ pub fn app_router(state: AppState) -> Router {
         )
         .route("/admin/repos/{id}/delete", post(routes::admin_repo_delete))
         .route("/site-banner.json", get(routes::site_banner_json))
-        .route("/notifications", get(routes::notifications_list))
-        .route(
-            "/notifications/unread.json",
-            get(routes::notifications_unread_json),
-        )
-        .route(
-            "/notifications/read-all",
-            post(routes::notifications_mark_all_read),
-        )
-        .route(
-            "/notifications/{id}/read",
-            post(routes::notifications_mark_read),
-        )
         .route("/new", get(routes::new_repo_form).post(routes::new_repo))
         .route(
             "/settings/profile",
@@ -184,10 +112,6 @@ pub fn app_router(state: AppState) -> Router {
             post(routes_extra::account_privacy),
         )
         .route(
-            "/settings/account/theme",
-            post(routes_extra::account_theme),
-        )
-        .route(
             "/settings/account/emails",
             post(routes_extra::account_add_email),
         )
@@ -196,10 +120,6 @@ pub fn app_router(state: AppState) -> Router {
             post(routes_extra::account_delete_email),
         )
         .route(
-            "/settings/account/emails/{id}/primary",
-            post(routes_extra::account_set_primary_email),
-        )
-        .route(
             "/settings/account/sessions/{id}/revoke",
             post(routes_extra::account_revoke_session),
         )
@@ -242,8 +162,6 @@ pub fn app_router(state: AppState) -> Router {
         .route("/{owner}/{repo}/og.png", get(routes::repo_og_image))
         .route("/{owner}/{repo}/tree/{*rest}", get(routes::repo_tree))
         .route("/{owner}/{repo}/blob/{*rest}", get(routes::repo_blob))
-        .route("/{owner}/{repo}/blame/{*rest}", get(routes::repo_blame))
-        .route("/{owner}/{repo}/history/{*rest}", get(routes::repo_history))
         .route("/{owner}/{repo}/raw/{*rest}", get(routes_extra::repo_raw))
         .route("/{owner}/{repo}/star", post(routes_extra::repo_star))
         .route("/{owner}/{repo}/watch", post(routes_extra::repo_watch))
@@ -259,20 +177,20 @@ pub fn app_router(state: AppState) -> Router {
         .route("/{owner}/{repo}/upload", post(routes::repo_upload))
         .route("/{owner}/{repo}/branches", get(routes::repo_branches))
         .route(
-            "/{owner}/{repo}/branches/rename",
+            "/{owner}/{repo}/branches/{branch}/rename",
             post(routes_extra::branch_rename),
         )
         .route(
-            "/{owner}/{repo}/branches/delete",
+            "/{owner}/{repo}/branches/{branch}/delete",
             post(routes_extra::branch_delete),
         )
         .route("/{owner}/{repo}/tags", post(routes_extra::tag_create))
         .route(
-            "/{owner}/{repo}/tags/rename",
+            "/{owner}/{repo}/tags/{tag}/rename",
             post(routes_extra::tag_rename),
         )
         .route(
-            "/{owner}/{repo}/tags/delete",
+            "/{owner}/{repo}/tags/{tag}/delete",
             post(routes_extra::tag_delete),
         )
         .route(
@@ -293,14 +211,6 @@ pub fn app_router(state: AppState) -> Router {
             post(routes::issue_reopen),
         )
         .route(
-            "/{owner}/{repo}/issues/{number}/labels",
-            post(routes::issue_labels_save),
-        )
-        .route(
-            "/{owner}/{repo}/issues/{number}/milestone",
-            post(routes::issue_milestone_save),
-        )
-        .route(
             "/{owner}/{repo}/pulls",
             get(routes::pulls_list).post(routes::pull_create),
         )
@@ -318,50 +228,6 @@ pub fn app_router(state: AppState) -> Router {
             post(routes::pull_close),
         )
         .route(
-            "/{owner}/{repo}/pulls/{number}/labels",
-            post(routes::pull_labels_save),
-        )
-        .route(
-            "/{owner}/{repo}/pulls/{number}/milestone",
-            post(routes::pull_milestone_save),
-        )
-        .route(
-            "/{owner}/{repo}/pulls/{number}/review",
-            post(routes::pull_review),
-        )
-        .route(
-            "/{owner}/{repo}/labels",
-            get(routes_extra::labels_list).post(routes_extra::label_create),
-        )
-        .route(
-            "/{owner}/{repo}/labels/{id}/update",
-            post(routes_extra::label_update),
-        )
-        .route(
-            "/{owner}/{repo}/labels/{id}/delete",
-            post(routes_extra::label_delete),
-        )
-        .route(
-            "/{owner}/{repo}/milestones",
-            get(routes_extra::milestones_list).post(routes_extra::milestone_create),
-        )
-        .route(
-            "/{owner}/{repo}/milestones/{id}/update",
-            post(routes_extra::milestone_update),
-        )
-        .route(
-            "/{owner}/{repo}/milestones/{id}/close",
-            post(routes_extra::milestone_close),
-        )
-        .route(
-            "/{owner}/{repo}/milestones/{id}/reopen",
-            post(routes_extra::milestone_reopen),
-        )
-        .route(
-            "/{owner}/{repo}/milestones/{id}/delete",
-            post(routes_extra::milestone_delete),
-        )
-        .route(
             "/{owner}/{repo}/releases",
             get(routes::releases_list).post(routes::release_create),
         )
@@ -397,26 +263,6 @@ pub fn app_router(state: AppState) -> Router {
             post(routes::collab_remove),
         )
         .route(
-            "/{owner}/{repo}/settings/deploy-keys",
-            post(routes::deploy_key_add),
-        )
-        .route(
-            "/{owner}/{repo}/settings/deploy-keys/{id}/delete",
-            post(routes::deploy_key_delete),
-        )
-        .route(
-            "/{owner}/{repo}/settings/webhooks",
-            post(routes::webhook_add),
-        )
-        .route(
-            "/{owner}/{repo}/settings/webhooks/{id}/delete",
-            post(routes::webhook_delete),
-        )
-        .route(
-            "/{owner}/{repo}/settings/webhooks/{id}/toggle",
-            post(routes::webhook_toggle),
-        )
-        .route(
             "/{owner}/{repo}/settings/branch-rules",
             post(routes_extra::branch_rule_add),
         )
@@ -425,18 +271,6 @@ pub fn app_router(state: AppState) -> Router {
             post(routes_extra::branch_rule_delete),
         )
         .route(
-            "/{owner}/{repo}/settings/mirror",
-            post(routes_extra::mirror_save),
-        )
-        .route(
-            "/{owner}/{repo}/settings/mirror/sync",
-            post(routes_extra::mirror_sync),
-        )
-        .route(
-            "/{owner}/{repo}/settings/mirror/delete",
-            post(routes_extra::mirror_delete),
-        )
-        .route(
             "/{owner}/{repo}/settings/danger/archive",
             post(routes::repo_archive),
         )
@@ -450,7 +284,6 @@ pub fn app_router(state: AppState) -> Router {
         )
         .route("/{username}", get(routes::profile))
         .nest_service("/static", ServeDir::new(static_dir))
-        .layer(from_fn(inject_render_timing))
         .layer(from_fn(normalize_git_url))
         .layer(DefaultBodyLimit::max(100 * 1024 * 1024))
         .layer(RequestBodyLimitLayer::new(100 * 1024 * 1024))
@@ -459,8 +292,7 @@ pub fn app_router(state: AppState) -> Router {
 
 #[cfg(test)]
 mod path_tests {
-    use super::{format_render_duration, inject_render_marker, normalize_repo_path};
-    use std::time::Duration;
+    use super::normalize_repo_path;
 
     #[test]
     fn strips_git_suffix_and_slash() {
@@ -470,19 +302,5 @@ mod path_tests {
         assert_eq!(normalize_repo_path("/o/r/"), "/o/r");
         assert_eq!(normalize_repo_path("/"), "/");
     }
-
-    #[test]
-    fn injects_render_timing_marker() {
-        let html = b"<footer>rendered in <!--kg-render-ms--></footer>";
-        let out = inject_render_marker(html, "4.2ms").unwrap();
-        assert_eq!(out, b"<footer>rendered in 4.2ms</footer>");
-        assert!(inject_render_marker(b"<footer>kitgit</footer>", "1ms").is_none());
-    }
-
-    #[test]
-    fn formats_render_duration() {
-        assert_eq!(format_render_duration(Duration::from_micros(4200)), "4.2ms");
-        assert_eq!(format_render_duration(Duration::from_millis(42)), "42ms");
-    }
 }
 
src/web/routes.rs +115 −1313
diff --git a/src/web/routes.rs b/src/web/routes.rs
index b99d0d7..2db68d1 100644
--- a/src/web/routes.rs
+++ b/src/web/routes.rs
@@ -20,7 +20,7 @@ use serde::Deserialize;
 use std::path::PathBuf;
 use uuid::Uuid;
 
-// ── helpers ──────────────────────────────────────────────────────────────────
+// ── helpers ──────────────────────────────────────────────────────────────────
 
 pub type AppResult<T> = Result<T, AppError>;
 
@@ -113,95 +113,6 @@ fn redirect_with_cookies(to: &str, cookies: Vec<HeaderValue>) -> Response {
     res
 }
 
-/// Best-effort client IP / User-Agent from reverse-proxy headers.
-pub fn request_client_meta(headers: &HeaderMap) -> (Option<String>, Option<String>) {
-    let ua = headers
-        .get(axum::http::header::USER_AGENT)
-        .and_then(|v| v.to_str().ok())
-        .map(|s| s.chars().take(512).collect::<String>());
-    let ip = headers
-        .get("x-forwarded-for")
-        .and_then(|v| v.to_str().ok())
-        .and_then(|s| s.split(',').next())
-        .map(|s| s.trim().to_string())
-        .filter(|s| !s.is_empty())
-        .or_else(|| {
-            headers
-                .get("x-real-ip")
-                .and_then(|v| v.to_str().ok())
-                .map(|s| s.trim().to_string())
-                .filter(|s| !s.is_empty())
-        });
-    (ip, ua)
-}
-
-/// Write an audit_log row; failures are logged and never fail the request.
-pub async fn record_audit(
-    state: &AppState,
-    headers: &HeaderMap,
-    user_id: Uuid,
-    actor_id: Option<Uuid>,
-    action: &str,
-    metadata: serde_json::Value,
-) {
-    let (ip, ua) = request_client_meta(headers);
-    if let Err(e) = queries::record_audit_log(
-        &state.pool,
-        user_id,
-        actor_id,
-        action,
-        ip.as_deref(),
-        ua.as_deref(),
-        metadata,
-    )
-    .await
-    {
-        tracing::warn!("audit_log write failed ({action}): {e:#}");
-    }
-}
-
-fn audit_action_label(action: &str) -> &'static str {
-    match action {
-        "login.success" => "Signed in",
-        "login.failure" => "Failed sign-in",
-        "logout" => "Signed out",
-        "ssh_key.add" => "Added SSH key",
-        "ssh_key.delete" => "Removed SSH key",
-        "gpg_key.add" => "Added GPG key",
-        "gpg_key.delete" => "Removed GPG key",
-        "session.revoke" => "Revoked session",
-        "session.revoke_others" => "Revoked other sessions",
-        "mfa.enable" => "Enabled MFA",
-        "mfa.disable" => "Disabled MFA",
-        "username.change" => "Changed username",
-        "password.change" => "Changed password",
-        "email.add" => "Added email",
-        "email.delete" => "Removed email",
-        "privacy.update" => "Updated privacy settings",
-        _ => "Account activity",
-    }
-}
-
-pub fn audit_entries_view(rows: Vec<crate::db::models::AuditLog>) -> Vec<AuditEntryView> {
-    rows.into_iter()
-        .map(|e| {
-            let meta = if e.metadata.is_null() || e.metadata == serde_json::json!({}) {
-                String::new()
-            } else {
-                e.metadata.to_string()
-            };
-            AuditEntryView {
-                action: e.action.clone(),
-                action_label: audit_action_label(&e.action).to_string(),
-                ip: e.ip.unwrap_or_default(),
-                user_agent: e.user_agent.unwrap_or_default(),
-                metadata: meta,
-                created_at: e.created_at,
-            }
-        })
-        .collect()
-}
-
 pub fn avatar_url_for(user: &User) -> String {
     let bust = user.updated_at.timestamp();
     // Always go through our avatar endpoint when a local file exists.
@@ -209,7 +120,7 @@ pub fn avatar_url_for(user: &User) -> String {
         return format!("/avatars/{}?v={}", user.id, bust);
     }
     // External OIDC pictures are fine in <img src>, but never use our own
-    // /avatars/{id} URL as avatar_url — that creates a redirect loop.
+    // /avatars/{id} URL as avatar_url — that creates a redirect loop.
     if let Some(ref url) = user.avatar_url {
         if !url.is_empty() && !is_self_avatar_url(url, user.id) {
             return url.clone();
@@ -333,12 +244,12 @@ fn language_stat_views(stats: serde_json::Value) -> Vec<LanguageStatView> {
 
 fn reaction_label(emoji: &str) -> String {
     match emoji {
-        "+1" => "👍".into(),
-        "-1" => "├░┼╕ΓÇÿ┼╜".into(),
-        "heart" => "❤️".into(),
-        "laugh" => "😄".into(),
-        "rocket" => "🚀".into(),
-        "eyes" => "├░┼╕ΓÇÿΓé¼".into(),
+        "+1" => "👍".into(),
+        "-1" => "👎".into(),
+        "heart" => "❤️".into(),
+        "laugh" => "😄".into(),
+        "rocket" => "🚀".into(),
+        "eyes" => "👀".into(),
         other => other.to_string(),
     }
 }
@@ -383,9 +294,9 @@ enum CommitRefKind {
     Pull,
     /// Explicit issue wording (`issue #N`).
     Issue,
-    /// Closing keywords (`fixes #N`, `closes #N`, …): prefer issue if it exists, else pull.
+    /// Closing keywords (`fixes #N`, `closes #N`, …): prefer issue if it exists, else pull.
     Closing,
-    /// Bare `#N` ΓÇö default to issues.
+    /// Bare `#N` — default to issues.
     Bare,
 }
 
@@ -451,7 +362,7 @@ async fn linkify_commit_message(
     while i < bytes.len() {
         if bytes[i] == b'#' {
             if let Some((len, number)) = parse_hash_number(&message[i..]) {
-                // Avoid matching mid-word like `C#1` or `foo#1` ΓÇö require start or non-alnum before.
+                // Avoid matching mid-word like `C#1` or `foo#1` — require start or non-alnum before.
                 let ok_boundary = i == 0
                     || !message[..i]
                         .chars()
@@ -570,14 +481,16 @@ fn append_diff_line(out: &mut String, line: &str) {
     out.push('\n');
 }
 
-fn render_diff_html(diff: &str, full: bool) -> (String, bool, usize) {
+fn render_diff_html(diff: &str) -> String {
     let lines: Vec<&str> = diff.lines().collect();
-    let max = if full {
-        None
+    let (html, truncated, total) = render_diff_lines(&lines, Some(DIFF_RENDER_MAX_LINES));
+    if truncated {
+        format!(
+            "{html}<p class=\"kg-diff__truncated\">Large diffs are not rendered by default. Showing the first {DIFF_RENDER_MAX_LINES} of {total} lines.</p>"
+        )
     } else {
-        Some(DIFF_RENDER_MAX_LINES)
-    };
-    render_diff_lines(&lines, max)
+        html
+    }
 }
 
 /// Render unified-diff lines. When `max_lines` is set, stop after that many
@@ -623,7 +536,7 @@ fn anchor_for_path(path: &str, idx: usize) -> String {
     format!("diff-{idx}-{safe}")
 }
 
-fn parse_diff_files(diff: &str, full: bool) -> Vec<DiffFileView> {
+fn parse_diff_files(diff: &str) -> Vec<DiffFileView> {
     let mut files = Vec::new();
     if diff.trim().is_empty() {
         return files;
@@ -633,23 +546,23 @@ fn parse_diff_files(diff: &str, full: bool) -> Vec<DiffFileView> {
     for line in diff.lines() {
         if line.starts_with("diff --git ") {
             if let Some((path, lines)) = current.take() {
-                files.push(build_diff_file(files.len(), path, &lines, full));
+                files.push(build_diff_file(files.len(), path, &lines));
             }
             current = Some((diff_file_path(line), vec![line.to_string()]));
         } else if let Some((_, ref mut lines)) = current {
             lines.push(line.to_string());
         } else {
-            // Preamble without a file header — treat as one blob.
+            // Preamble without a file header — treat as one blob.
             current = Some(("diff".into(), vec![line.to_string()]));
         }
     }
     if let Some((path, lines)) = current {
-        files.push(build_diff_file(files.len(), path, &lines, full));
+        files.push(build_diff_file(files.len(), path, &lines));
     }
     files
 }
 
-fn build_diff_file(idx: usize, path: String, lines: &[String], full: bool) -> DiffFileView {
+fn build_diff_file(idx: usize, path: String, lines: &[String]) -> DiffFileView {
     let mut additions = 0u32;
     let mut deletions = 0u32;
     let mut binary = false;
@@ -664,11 +577,6 @@ fn build_diff_file(idx: usize, path: String, lines: &[String], full: bool) -> Di
         }
     }
     let refs: Vec<&str> = lines.iter().map(|s| s.as_str()).collect();
-    let max = if full {
-        None
-    } else {
-        Some(DIFF_RENDER_MAX_LINES)
-    };
     let (html, truncated, total_lines) = if binary {
         (
             String::from(
@@ -678,7 +586,7 @@ fn build_diff_file(idx: usize, path: String, lines: &[String], full: bool) -> Di
             refs.len(),
         )
     } else {
-        render_diff_lines(&refs, max)
+        render_diff_lines(&refs, Some(DIFF_RENDER_MAX_LINES))
     };
     DiffFileView {
         anchor: anchor_for_path(&path, idx),
@@ -711,7 +619,7 @@ async fn latest_commit_view(
     Some(commit_view(state, c, extracted).await)
 }
 
-pub fn clone_urls(state: &AppState, owner: &str, repo: &str) -> (String, String) {
+fn clone_urls(state: &AppState, owner: &str, repo: &str) -> (String, String) {
     let base = state.config.public_url.trim_end_matches('/');
     // Always advertise the classic `.git` HTTP URL (middleware strips it).
     let http = format!("{base}/{owner}/{repo}.git");
@@ -721,7 +629,7 @@ pub fn clone_urls(state: &AppState, owner: &str, repo: &str) -> (String, String)
         .unwrap_or_else(|| "localhost".into());
     let port = state.config.ssh_advertise_port();
     // Port 22: GitHub-style `git@host:owner/repo.git` (default SSH port).
-    // Other ports: ssh:// form ΓÇö `git@host:2222/path` is parsed as path `2222/path` on port 22.
+    // Other ports: ssh:// form — `git@host:2222/path` is parsed as path `2222/path` on port 22.
     let ssh = if port == 22 {
         format!("git@{host}:{owner}/{repo}.git")
     } else {
@@ -806,33 +714,6 @@ fn breadcrumbs(path: &str) -> Vec<(String, String)> {
     out
 }
 
-fn query_full(v: Option<&str>) -> bool {
-    matches!(v, Some("1") | Some("true") | Some("yes") | Some("on"))
-}
-
-async fn review_views(
-    pool: &sqlx::PgPool,
-    reviews: Vec<crate::db::models::PullReview>,
-) -> AppResult<Vec<PullReviewView>> {
-    let mut out = Vec::with_capacity(reviews.len());
-    for r in reviews {
-        let reviewer = queries::get_user_by_id(pool, r.reviewer_id)
-            .await?
-            .ok_or_else(AppError::not_found)?;
-        let avatar_url = avatar_url_for(&reviewer);
-        out.push(PullReviewView {
-            id: r.id,
-            reviewer,
-            avatar_url,
-            state: r.state,
-            body: r.body.clone(),
-            body_html: render_markdown(&r.body),
-            created_at: r.created_at,
-        });
-    }
-    Ok(out)
-}
-
 async fn comment_views(
     pool: &sqlx::PgPool,
     comments: Vec<Comment>,
@@ -946,7 +827,7 @@ fn map_activity_rows(
     activities
 }
 
-// ── home / auth ──────────────────────────────────────────────────────────────
+// ── home / auth ──────────────────────────────────────────────────────────────
 
 pub async fn home(
     State(state): State<AppState>,
@@ -1128,46 +1009,22 @@ pub struct LoginForm {
 
 pub async fn auth_login_submit(
     State(state): State<AppState>,
-    headers: HeaderMap,
     Form(form): Form<LoginForm>,
 ) -> AppResult<Response> {
     match auth::login_with_password(&state.auth, &form.username, &form.password).await {
-        Ok(LoginOutcome::Complete { user, token }) => {
-            record_audit(
-                &state,
-                &headers,
-                user.id,
-                Some(user.id),
-                "login.success",
-                serde_json::json!({ "method": "password" }),
-            )
-            .await;
-            Ok(redirect_with_cookies(
-                "/",
-                vec![
-                    session_cookie_header(&token, 14 * 24 * 3600),
-                    clear_mfa_pending_cookie(),
-                ],
-            ))
-        }
+        Ok(LoginOutcome::Complete { token, .. }) => Ok(redirect_with_cookies(
+            "/",
+            vec![
+                session_cookie_header(&token, 14 * 24 * 3600),
+                clear_mfa_pending_cookie(),
+            ],
+        )),
         Ok(LoginOutcome::MfaRequired { pending_token }) => Ok(redirect_with_cookie(
             "/auth/mfa",
             mfa_pending_cookie_header(&pending_token, 10 * 60),
         )),
         Err(e) => {
             tracing::warn!("login failed: {e:#}");
-            let uname = form.username.trim().to_ascii_lowercase();
-            if let Ok(Some(u)) = queries::get_user_by_username(&state.pool, &uname).await {
-                record_audit(
-                    &state,
-                    &headers,
-                    u.id,
-                    None,
-                    "login.failure",
-                    serde_json::json!({ "method": "password", "username": uname }),
-                )
-                .await;
-            }
             Ok(LoginTemplate {
                 viewer: None,
                 error: Some(crate::mfa::sanitize_user_error(&e.to_string())),
@@ -1208,24 +1065,13 @@ pub async fn auth_mfa_submit(
         return Ok(redirect("/auth/login"));
     };
     match auth::complete_mfa_login(&state.auth, &pending, &form.code).await {
-        Ok((user, token)) => {
-            record_audit(
-                &state,
-                &headers,
-                user.id,
-                Some(user.id),
-                "login.success",
-                serde_json::json!({ "method": "password+mfa" }),
-            )
-            .await;
-            Ok(redirect_with_cookies(
-                "/",
-                vec![
-                    session_cookie_header(&token, 14 * 24 * 3600),
-                    clear_mfa_pending_cookie(),
-                ],
-            ))
-        }
+        Ok((_user, token)) => Ok(redirect_with_cookies(
+            "/",
+            vec![
+                session_cookie_header(&token, 14 * 24 * 3600),
+                clear_mfa_pending_cookie(),
+            ],
+        )),
         Err(e) => {
             tracing::warn!("mfa challenge failed: {e:#}");
             let msg = crate::mfa::sanitize_user_error(&e.to_string());
@@ -1241,7 +1087,7 @@ pub async fn auth_mfa_submit(
     }
 }
 
-/// Legacy OIDC browser start ΓÇö disabled so users never leave kitgit UI.
+/// Legacy OIDC browser start — disabled so users never leave kitgit UI.
 pub async fn auth_oidc_start() -> AppResult<Response> {
     Ok(redirect("/auth/login"))
 }
@@ -1375,7 +1221,6 @@ pub struct CallbackQuery {
 
 pub async fn auth_callback(
     State(state): State<AppState>,
-    headers: HeaderMap,
     Query(q): Query<CallbackQuery>,
 ) -> AppResult<Response> {
     if let Some(err) = q.error {
@@ -1383,16 +1228,7 @@ pub async fn auth_callback(
     }
     let code = q.code.ok_or_else(|| AppError::bad("missing code"))?;
     let st = q.state.ok_or_else(|| AppError::bad("missing state"))?;
-    let (user, token) = auth::finish_login(&state.auth, &code, &st).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "login.success",
-        serde_json::json!({ "method": "oidc" }),
-    )
-    .await;
+    let (_user, token) = auth::finish_login(&state.auth, &code, &st).await?;
     let cookie = session_cookie_header(&token, 14 * 24 * 3600);
     Ok(redirect_with_cookie("/", cookie))
 }
@@ -1401,22 +1237,11 @@ pub async fn auth_logout(
     State(state): State<AppState>,
     headers: HeaderMap,
 ) -> AppResult<Response> {
-    if let Ok(Some(user)) = current_user(&state.auth, &headers).await {
-        record_audit(
-            &state,
-            &headers,
-            user.id,
-            Some(user.id),
-            "logout",
-            serde_json::json!({}),
-        )
-        .await;
-    }
     auth::logout(&state.auth, &headers).await?;
     Ok(redirect_with_cookie("/", clear_session_cookie()))
 }
 
-// ── site admin panel ─────────────────────────────────────────────────────────
+// ── site admin panel ─────────────────────────────────────────────────────────
 
 async fn require_site_admin(auth: &AuthState, headers: &HeaderMap) -> AppResult<User> {
     let user = require_login(auth, headers).await?;
@@ -1570,24 +1395,6 @@ pub async fn admin_panel(
     })
 }
 
-pub async fn admin_user_audit(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path(username): Path<String>,
-) -> AppResult<impl IntoResponse> {
-    let viewer = require_site_admin(&state.auth, &headers).await?;
-    let user = queries::get_user_by_username(&state.pool, &username)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    let audit_rows = queries::list_audit_log_for_user(&state.pool, user.id, 100).await?;
-    let audit_entries = audit_entries_view(audit_rows);
-    Ok(AdminUserAuditTemplate {
-        viewer: Some(viewer),
-        user,
-        audit_entries,
-    })
-}
-
 #[derive(Deserialize)]
 pub struct AdminToggleForm {
     pub user_id: Uuid,
@@ -1788,61 +1595,7 @@ pub async fn site_banner_json(State(state): State<AppState>) -> impl IntoRespons
     axum::Json(serde_json::json!({ "message": message }))
 }
 
-fn notif_snippet(body: &str, max: usize) -> String {
-    let compact: String = body.split_whitespace().collect::<Vec<_>>().join(" ");
-    if compact.chars().count() <= max {
-        compact
-    } else {
-        let truncated: String = compact.chars().take(max.saturating_sub(1)).collect();
-        format!("{truncated}…")
-    }
-}
-
-pub async fn notifications_list(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-) -> AppResult<impl IntoResponse> {
-    let viewer = require_login(&state.auth, &headers).await?;
-    let notifications = queries::list_notifications(&state.pool, viewer.id, 100).await?;
-    let unread_count = queries::unread_notification_count(&state.pool, viewer.id).await?;
-    Ok(NotificationsTemplate {
-        viewer: Some(viewer),
-        notifications,
-        unread_count,
-    })
-}
-
-pub async fn notifications_unread_json(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-) -> AppResult<impl IntoResponse> {
-    let viewer = require_login(&state.auth, &headers).await?;
-    let count = queries::unread_notification_count(&state.pool, viewer.id).await?;
-    Ok(axum::Json(serde_json::json!({ "count": count })))
-}
-
-pub async fn notifications_mark_read(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path(id): Path<Uuid>,
-) -> AppResult<Response> {
-    let viewer = require_login(&state.auth, &headers).await?;
-    queries::mark_notification_read(&state.pool, id, viewer.id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    Ok(redirect_see_other("/notifications"))
-}
-
-pub async fn notifications_mark_all_read(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-) -> AppResult<Response> {
-    let viewer = require_login(&state.auth, &headers).await?;
-    queries::mark_all_notifications_read(&state.pool, viewer.id).await?;
-    Ok(redirect_see_other("/notifications"))
-}
-
-// ── new repo ─────────────────────────────────────────────────────────────────
+// ── new repo ─────────────────────────────────────────────────────────────────
 
 pub async fn new_repo_form(
     State(state): State<AppState>,
@@ -1933,7 +1686,7 @@ pub async fn new_repo(
     Ok(redirect_see_other(&format!("/{}/{}", user.username, name)))
 }
 
-// ── profile settings ─────────────────────────────────────────────────────────
+// ── profile settings ─────────────────────────────────────────────────────────
 
 pub async fn profile_settings(
     State(state): State<AppState>,
@@ -2048,7 +1801,7 @@ pub async fn profile_settings_save(
     Ok(redirect_see_other("/settings/profile"))
 }
 
-// ── SSH keys ─────────────────────────────────────────────────────────────────
+// ── SSH keys ─────────────────────────────────────────────────────────────────
 
 pub async fn keys_settings(
     State(state): State<AppState>,
@@ -2096,15 +1849,6 @@ pub async fn keys_add(
     queries::add_ssh_key(&state.pool, user.id, name, public_key, &fp, key_usage)
         .await
         .map_err(|e| AppError::bad(format!("could not add key: {e}")))?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "ssh_key.add",
-        serde_json::json!({ "name": name, "fingerprint": fp, "key_usage": key_usage }),
-    )
-    .await;
     Ok(redirect_see_other("/settings/keys"))
 }
 
@@ -2131,19 +1875,10 @@ pub async fn keys_delete(
 ) -> AppResult<Response> {
     let user = require_login(&state.auth, &headers).await?;
     queries::delete_ssh_key(&state.pool, user.id, id).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "ssh_key.delete",
-        serde_json::json!({ "key_id": id }),
-    )
-    .await;
     Ok(redirect_see_other("/settings/keys"))
 }
 
-// ── avatar ───────────────────────────────────────────────────────────────────
+// ── avatar ───────────────────────────────────────────────────────────────────
 
 pub async fn avatar(
     State(state): State<AppState>,
@@ -2190,7 +1925,7 @@ pub async fn avatar(
         .unwrap())
 }
 
-// ── profile ──────────────────────────────────────────────────────────────────
+// ── profile ──────────────────────────────────────────────────────────────────
 
 pub async fn profile(
     State(state): State<AppState>,
@@ -2232,7 +1967,7 @@ pub async fn profile(
     })
 }
 
-// ── repository browse ────────────────────────────────────────────────────────
+// ── repository browse ────────────────────────────────────────────────────────
 
 pub async fn repo_home(
     State(state): State<AppState>,
@@ -2256,11 +1991,16 @@ pub async fn repo_home(
         branches = git::list_branches(g).unwrap_or_default();
         if git::resolve_ref(g, &current_branch).is_ok() {
             empty = false;
-            entries = tree_entry_views(
-                g,
-                &current_branch,
-                git::list_tree(g, &current_branch, "").unwrap_or_default(),
-            );
+            entries = git::list_tree(g, &current_branch, "")
+                .unwrap_or_default()
+                .into_iter()
+                .map(|e| TreeEntryView {
+                    name: e.name,
+                    path: e.path,
+                    is_dir: e.is_dir,
+                    mode: e.mode,
+                })
+                .collect();
             if let Ok(Some((name, src))) = git::find_readme(g, &current_branch, "") {
                 readme_html = Some(readme_to_html(
                     &name,
@@ -2290,10 +2030,6 @@ pub async fn repo_home(
         }
         None => None,
     };
-    let commit_count = match grepo.as_ref() {
-        Some(g) => git::count_commits(g, &current_branch).unwrap_or(0),
-        None => 0,
-    };
     let languages = language_stat_views(languages);
 
     let forked_from = if let Some(fid) = repository.fork_of_id {
@@ -2329,7 +2065,6 @@ pub async fn repo_home(
         languages,
         empty,
         latest_commit,
-        commit_count,
         forked_from,
         starred,
         watching,
@@ -2373,12 +2108,16 @@ pub async fn repo_tree(
     let grepo = git::open_bare(&state.config.repos_dir(), &owner, &repo)
         .map_err(|_| AppError::not_found())?;
     let (branch, path) = split_ref_path(&grepo, &rest);
-    let entries = tree_entry_views(
-        &grepo,
-        &branch,
-        git::list_tree(&grepo, &branch, &path)
-            .map_err(|e| AppError::not_found().with_message(e.to_string()))?,
-    );
+    let entries = git::list_tree(&grepo, &branch, &path)
+        .map_err(|e| AppError::not_found().with_message(e.to_string()))?
+        .into_iter()
+        .map(|e| TreeEntryView {
+            name: e.name,
+            path: e.path,
+            is_dir: e.is_dir,
+            mode: e.mode,
+        })
+        .collect();
     let readme_html = match git::find_readme(&grepo, &branch, &path) {
         Ok(Some((name, src))) => Some(readme_to_html(
             &name,
@@ -2394,7 +2133,6 @@ pub async fn repo_tree(
     let branches = git::list_branches(&grepo).unwrap_or_default();
     let prepared = prepare_latest_commit(&grepo, &branch);
     let latest_commit = latest_commit_view(&state, prepared).await;
-    let commit_count = git::count_commits(&grepo, &branch).unwrap_or(0);
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(RepoTreeTemplate {
         viewer,
@@ -2408,7 +2146,6 @@ pub async fn repo_tree(
         entries,
         readme_html,
         latest_commit,
-        commit_count,
         clone_http,
         clone_ssh,
     })
@@ -2469,125 +2206,6 @@ pub async fn repo_blob(
     })
 }
 
-fn tree_entry_views(grepo: &git2::Repository, branch: &str, entries: Vec<git::TreeEntry>) -> Vec<TreeEntryView> {
-    entries
-        .into_iter()
-        .map(|e| {
-            let (commit_message, commit_time) = git::list_commits_for_path(grepo, branch, &e.path, 1)
-                .ok()
-                .and_then(|mut v| v.pop())
-                .map(|c| (c.message, format_unix_time(c.time)))
-                .unwrap_or_else(|| (String::new(), String::new()));
-            TreeEntryView {
-                name: e.name,
-                path: e.path,
-                is_dir: e.is_dir,
-                commit_message,
-                commit_time,
-            }
-        })
-        .collect()
-}
-
-
-pub async fn repo_blame(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, rest)): Path<(String, String, String)>,
-) -> AppResult<impl IntoResponse> {
-    let (repository, owner_user, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let grepo = git::open_bare(&state.config.repos_dir(), &owner, &repo)
-        .map_err(|_| AppError::not_found())?;
-    let (branch, path) = split_ref_path(&grepo, &rest);
-    if path.is_empty() {
-        return Err(AppError::bad("missing file path"));
-    }
-    let binary = match git::read_blob(&grepo, &branch, &path) {
-        Ok((_, binary)) => binary,
-        Err(_) => return Err(AppError::not_found()),
-    };
-    let lines = if binary {
-        Vec::new()
-    } else {
-        git::blame_file(&grepo, &branch, &path)
-            .map_err(|e| AppError::not_found().with_message(e.to_string()))?
-            .into_iter()
-            .map(|l| BlameLineView {
-                line_no: l.line_no,
-                content: l.content,
-                commit_id: l.commit_id,
-                short_id: l.short_id,
-                author: l.author,
-                time_display: format_unix_time(l.time),
-                summary: l.summary,
-                hunk_start: l.hunk_start,
-            })
-            .collect()
-    };
-    let branches = git::list_branches(&grepo).unwrap_or_default();
-    let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
-    Ok(RepoBlameTemplate {
-        viewer,
-        owner: owner_user,
-        repo: repository,
-        access,
-        branches,
-        branch,
-        path: path.clone(),
-        breadcrumbs: breadcrumbs(&path),
-        lines,
-        binary,
-        clone_http,
-        clone_ssh,
-    })
-}
-
-pub async fn repo_history(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, rest)): Path<(String, String, String)>,
-) -> AppResult<impl IntoResponse> {
-    let (repository, owner_user, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let grepo = git::open_bare(&state.config.repos_dir(), &owner, &repo)
-        .map_err(|_| AppError::not_found())?;
-    let (branch, path) = split_ref_path(&grepo, &rest);
-    if path.is_empty() {
-        return Err(AppError::bad("missing file path"));
-    }
-    let raw_commits = git::list_commits_for_path(&grepo, &branch, &path, 100).unwrap_or_default();
-    if raw_commits.is_empty()
-        && git::read_blob(&grepo, &branch, &path).is_err()
-        && !git::path_is_dir(&grepo, &branch, &path)
-    {
-        return Err(AppError::not_found());
-    }
-    let prepared: Vec<_> = raw_commits
-        .into_iter()
-        .map(|c| {
-            let extracted = git::extract_commit_signature(&grepo, &c.id);
-            (c, extracted)
-        })
-        .collect();
-    let commits = commit_views(&state, prepared).await;
-    let branches = git::list_branches(&grepo).unwrap_or_default();
-    let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
-    Ok(RepoHistoryTemplate {
-        viewer,
-        owner: owner_user,
-        repo: repository,
-        access,
-        branches,
-        branch,
-        path: path.clone(),
-        breadcrumbs: breadcrumbs(&path),
-        commits,
-        clone_http,
-        clone_ssh,
-    })
-}
-
 #[derive(Deserialize)]
 pub struct BranchQuery {
     pub branch: Option<String>,
@@ -2630,16 +2248,10 @@ pub async fn repo_commits(
     })
 }
 
-#[derive(Deserialize)]
-pub struct FullDiffQuery {
-    pub full: Option<String>,
-}
-
 pub async fn repo_commit(
     State(state): State<AppState>,
     headers: HeaderMap,
     Path((owner, repo, id)): Path<(String, String, String)>,
-    Query(q): Query<FullDiffQuery>,
 ) -> AppResult<impl IntoResponse> {
     let (repository, owner_user, viewer, access) =
         load_repo_context(&state, &owner, &repo, &headers).await?;
@@ -2648,8 +2260,7 @@ pub async fn repo_commit(
     let commit = git::get_commit(&grepo, &id).map_err(|_| AppError::not_found())?;
     let extracted = git::extract_commit_signature(&grepo, &commit.id);
     let diff = git::commit_diff(&grepo, &id).unwrap_or_default();
-    let show_full = query_full(q.full.as_deref());
-    let (diff_html, truncated, total_lines) = render_diff_html(&diff, show_full);
+    let diff_html = render_diff_html(&diff);
     let message_html = linkify_commit_message(
         &state.pool,
         repository.id,
@@ -2666,9 +2277,6 @@ pub async fn repo_commit(
         commit: commit_view(&state, commit, extracted).await,
         message_html,
         diff_html,
-        show_full,
-        truncated,
-        total_lines,
         clone_http: clone_urls(&state, &owner, &repo).0,
         clone_ssh: clone_urls(&state, &owner, &repo).1,
     })
@@ -2678,7 +2286,6 @@ pub async fn repo_diff(
     State(state): State<AppState>,
     headers: HeaderMap,
     Path((owner, repo, id)): Path<(String, String, String)>,
-    Query(q): Query<FullDiffQuery>,
 ) -> AppResult<impl IntoResponse> {
     let (repository, owner_user, viewer, access) =
         load_repo_context(&state, &owner, &repo, &headers).await?;
@@ -2687,8 +2294,7 @@ pub async fn repo_diff(
     let commit = git::get_commit(&grepo, &id).map_err(|_| AppError::not_found())?;
     let extracted = git::extract_commit_signature(&grepo, &commit.id);
     let diff = git::commit_diff(&grepo, &id).unwrap_or_default();
-    let show_full = query_full(q.full.as_deref());
-    let (diff_html, truncated, total_lines) = render_diff_html(&diff, show_full);
+    let diff_html = render_diff_html(&diff);
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(RepoDiffTemplate {
         viewer,
@@ -2697,9 +2303,6 @@ pub async fn repo_diff(
         access,
         commit: commit_view(&state, commit, extracted).await,
         diff_html,
-        show_full,
-        truncated,
-        total_lines,
         clone_http,
         clone_ssh,
     })
@@ -2908,7 +2511,7 @@ pub async fn repo_branches(
             let updated = git::list_commits(&grepo, &name, 1)
                 .ok()
                 .and_then(|mut v| v.pop())
-                .map(|c| format_unix_time(c.time))
+                .map(|c| c.time.to_string())
                 .unwrap_or_default();
             BranchRow {
                 name,
@@ -2936,7 +2539,7 @@ pub async fn repo_branches(
             short_id: t.short_id,
             target: t.target,
             message: t.message,
-            updated: format_unix_time(t.time),
+            updated: t.time.to_string(),
         })
         .collect();
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
@@ -2958,7 +2561,6 @@ async fn commit_view(
     c: git::CommitInfo,
     extracted: Option<(String, Vec<u8>)>,
 ) -> CommitView {
-    let signed = extracted.is_some();
     let verification = match extracted {
         Some((sig, payload)) => {
             git::verify_commit_signature(
@@ -2991,13 +2593,8 @@ async fn commit_view(
         short_id: c.short_id,
         message: c.message,
         author: c.author,
-        email: c.email.clone(),
-        author_username: queries::username_by_email(&state.pool, &c.email)
-            .await
-            .ok()
-            .flatten(),
+        email: c.email,
         time: c.time,
-        signed,
         verified,
         verify_kind,
         verify_fingerprint,
@@ -3017,107 +2614,11 @@ async fn commit_views(
     out
 }
 
-// ── issues ───────────────────────────────────────────────────────────────────
+// ── issues ───────────────────────────────────────────────────────────────────
 
 #[derive(Deserialize)]
 pub struct StateFilter {
     pub state: Option<String>,
-    pub label: Option<Uuid>,
-    pub milestone: Option<Uuid>,
-}
-
-fn parse_optional_uuid(raw: &Option<String>) -> Option<Uuid> {
-    raw.as_deref()
-        .map(str::trim)
-        .filter(|s| !s.is_empty())
-        .and_then(|s| Uuid::parse_str(s).ok())
-}
-
-async fn build_issue_list_items(
-    pool: &sqlx::PgPool,
-    issues: Vec<crate::db::models::Issue>,
-) -> AppResult<Vec<IssueListItem>> {
-    let ids: Vec<Uuid> = issues.iter().map(|i| i.id).collect();
-    let label_rows = queries::labels_for_issues(pool, &ids).await?;
-    let mut labels_by_issue: std::collections::HashMap<Uuid, Vec<crate::db::models::Label>> =
-        std::collections::HashMap::new();
-    for (issue_id, label) in label_rows {
-        labels_by_issue.entry(issue_id).or_default().push(label);
-    }
-    let milestone_ids: Vec<Uuid> = issues.iter().filter_map(|i| i.milestone_id).collect();
-    let milestones = queries::get_milestones_by_ids(pool, &milestone_ids).await?;
-    let milestones_by_id: std::collections::HashMap<Uuid, crate::db::models::Milestone> =
-        milestones.into_iter().map(|m| (m.id, m)).collect();
-    Ok(issues
-        .into_iter()
-        .map(|issue| {
-            let milestone = issue
-                .milestone_id
-                .and_then(|id| milestones_by_id.get(&id).cloned());
-            let labels = labels_by_issue.remove(&issue.id).unwrap_or_default();
-            IssueListItem {
-                issue,
-                labels,
-                milestone,
-            }
-        })
-        .collect())
-}
-
-async fn build_pull_list_items(
-    pool: &sqlx::PgPool,
-    pulls: Vec<crate::db::models::PullRequest>,
-) -> AppResult<Vec<PullListItem>> {
-    let ids: Vec<Uuid> = pulls.iter().map(|p| p.id).collect();
-    let label_rows = queries::labels_for_pulls(pool, &ids).await?;
-    let mut labels_by_pull: std::collections::HashMap<Uuid, Vec<crate::db::models::Label>> =
-        std::collections::HashMap::new();
-    for (pull_id, label) in label_rows {
-        labels_by_pull.entry(pull_id).or_default().push(label);
-    }
-    let milestone_ids: Vec<Uuid> = pulls.iter().filter_map(|p| p.milestone_id).collect();
-    let milestones = queries::get_milestones_by_ids(pool, &milestone_ids).await?;
-    let milestones_by_id: std::collections::HashMap<Uuid, crate::db::models::Milestone> =
-        milestones.into_iter().map(|m| (m.id, m)).collect();
-    Ok(pulls
-        .into_iter()
-        .map(|pull| {
-            let milestone = pull
-                .milestone_id
-                .and_then(|id| milestones_by_id.get(&id).cloned());
-            let labels = labels_by_pull.remove(&pull.id).unwrap_or_default();
-            PullListItem {
-                pull,
-                labels,
-                milestone,
-            }
-        })
-        .collect())
-}
-
-fn label_options(
-    all: Vec<crate::db::models::Label>,
-    selected: &[crate::db::models::Label],
-) -> Vec<LabelOption> {
-    let selected_ids: std::collections::HashSet<Uuid> = selected.iter().map(|l| l.id).collect();
-    all.into_iter()
-        .map(|label| LabelOption {
-            selected: selected_ids.contains(&label.id),
-            label,
-        })
-        .collect()
-}
-
-fn milestone_options(
-    all: Vec<crate::db::models::Milestone>,
-    selected_id: Option<Uuid>,
-) -> Vec<MilestoneOption> {
-    all.into_iter()
-        .map(|milestone| MilestoneOption {
-            selected: Some(milestone.id) == selected_id,
-            milestone,
-        })
-        .collect()
 }
 
 pub async fn issues_list(
@@ -3131,39 +2632,20 @@ pub async fn issues_list(
     if !repository.issues_enabled {
         return Err(AppError::not_found());
     }
-    let state_filter = match q.state.as_deref() {
+    // Default to open-only (GitHub-style) when no state query param is set.
+    let filter = match q.state.as_deref() {
         Some("closed") => "closed",
-        Some("all") => "all",
         _ => "open",
     };
-    let state_arg = if state_filter == "all" {
-        None
-    } else {
-        Some(state_filter)
-    };
-    let issues = queries::list_issues_filtered(
-        &state.pool,
-        repository.id,
-        state_arg,
-        q.label,
-        q.milestone,
-    )
-    .await?;
-    let items = build_issue_list_items(&state.pool, issues).await?;
-    let labels = queries::list_labels(&state.pool, repository.id).await?;
-    let milestones = queries::list_milestones(&state.pool, repository.id, None).await?;
+    let issues = queries::list_issues(&state.pool, repository.id, Some(filter)).await?;
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(IssuesListTemplate {
         viewer,
         owner: owner_user,
         repo: repository,
         access,
-        issues: items,
-        labels,
-        milestones,
-        state_filter: state_filter.to_string(),
-        label_filter: q.label,
-        milestone_filter: q.milestone,
+        issues,
+        state_filter: filter.to_string(),
         clone_http,
         clone_ssh,
     })
@@ -3180,16 +2662,12 @@ pub async fn issue_new(
         return Err(AppError::not_found());
     }
     let _ = viewer.as_ref().ok_or_else(AppError::unauthorized)?;
-    let labels = queries::list_labels(&state.pool, repository.id).await?;
-    let milestones = queries::list_open_milestones(&state.pool, repository.id).await?;
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(IssueNewTemplate {
         viewer,
         owner: owner_user,
         repo: repository,
         access,
-        labels,
-        milestones,
         error: None,
         clone_http,
         clone_ssh,
@@ -3200,9 +2678,6 @@ pub async fn issue_new(
 pub struct IssueCreateForm {
     pub title: String,
     pub body: Option<String>,
-    #[serde(default)]
-    pub label_id: Vec<Uuid>,
-    pub milestone_id: Option<String>,
 }
 
 pub async fn issue_create(
@@ -3219,16 +2694,12 @@ pub async fn issue_create(
     let user = viewer.ok_or_else(AppError::unauthorized)?;
     let title = form.title.trim();
     if title.is_empty() {
-        let labels = queries::list_labels(&state.pool, repository.id).await?;
-        let milestones = queries::list_open_milestones(&state.pool, repository.id).await?;
         let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
         return Ok(IssueNewTemplate {
             viewer: Some(user),
             owner: owner_user,
             repo: repository,
             access,
-            labels,
-            milestones,
             error: Some("title required".into()),
             clone_http,
             clone_ssh,
@@ -3239,19 +2710,6 @@ pub async fn issue_create(
     let number = queries::next_issue_number(&state.pool, repository.id).await?;
     let issue =
         queries::create_issue(&state.pool, repository.id, user.id, number, title, &body).await?;
-    let label_ids =
-        queries::filter_repo_label_ids(&state.pool, repository.id, &form.label_id).await?;
-    if !label_ids.is_empty() {
-        queries::set_issue_labels(&state.pool, issue.id, &label_ids).await?;
-    }
-    if let Some(mid) = parse_optional_uuid(&form.milestone_id) {
-        if queries::get_milestone(&state.pool, repository.id, mid)
-            .await?
-            .is_some()
-        {
-            queries::set_issue_milestone(&state.pool, issue.id, Some(mid)).await?;
-        }
-    }
     queries::record_activity(
         &state.pool,
         Some(user.id),
@@ -3261,32 +2719,10 @@ pub async fn issue_create(
         serde_json::json!({ "number": number }),
     )
     .await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_ISSUES,
-        "opened".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "number": issue.number,
-            "title": issue.title,
-            "body": issue.body,
-            "state": issue.state,
-        }),
-    );
-    let href = format!("/{owner}/{repo}/issues/{number}");
-    queries::notify_repo_watchers(
-        &state.pool,
-        repository.id,
-        user.id,
-        "issue.open",
-        &format!("{} opened issue #{number}: {title}", user.username),
-        &notif_snippet(&body, 160),
-        &href,
-    )
-    .await?;
-    Ok(redirect_see_other(&href))
+    Ok(redirect_see_other(&format!(
+        "/{owner}/{repo}/issues/{}",
+        issue.number
+    )))
 }
 
 pub async fn issue_view(
@@ -3311,24 +2747,6 @@ pub async fn issue_view(
         viewer.as_ref().map(|u| u.id),
     )
     .await?;
-    let labels = queries::list_issue_labels(&state.pool, issue.id).await?;
-    let all_labels = queries::list_labels(&state.pool, repository.id).await?;
-    let label_options = label_options(all_labels, &labels);
-    let milestone = match issue.milestone_id {
-        Some(id) => queries::get_milestone(&state.pool, repository.id, id).await?,
-        None => None,
-    };
-    let mut milestone_choices = queries::list_open_milestones(&state.pool, repository.id).await?;
-    if let Some(ref m) = milestone {
-        if m.state != "open" && !milestone_choices.iter().any(|x| x.id == m.id) {
-            milestone_choices.push(m.clone());
-        }
-    }
-    let milestone_options = milestone_options(milestone_choices, issue.milestone_id);
-    let can_triage = viewer
-        .as_ref()
-        .map(|u| access.can_write() || issue.author_id == u.id)
-        .unwrap_or(false);
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(IssueViewTemplate {
         viewer,
@@ -3340,81 +2758,12 @@ pub async fn issue_view(
         issue,
         author,
         comments,
-        labels,
-        label_options,
-        milestone,
-        milestone_options,
-        can_triage,
         clone_http,
         clone_ssh,
     })
 }
 
 #[derive(Deserialize)]
-pub struct LabelsAssignForm {
-    #[serde(default)]
-    pub label_id: Vec<Uuid>,
-}
-
-#[derive(Deserialize)]
-pub struct MilestoneAssignForm {
-    pub milestone_id: Option<String>,
-}
-
-pub async fn issue_labels_save(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, number)): Path<(String, String, i32)>,
-    Form(form): Form<LabelsAssignForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.ok_or_else(AppError::unauthorized)?;
-    let issue = queries::get_issue(&state.pool, repository.id, number)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    if !(access.can_write() || issue.author_id == user.id) {
-        return Err(AppError::forbidden());
-    }
-    let label_ids =
-        queries::filter_repo_label_ids(&state.pool, repository.id, &form.label_id).await?;
-    queries::set_issue_labels(&state.pool, issue.id, &label_ids).await?;
-    Ok(redirect_see_other(&format!(
-        "/{owner}/{repo}/issues/{number}"
-    )))
-}
-
-pub async fn issue_milestone_save(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, number)): Path<(String, String, i32)>,
-    Form(form): Form<MilestoneAssignForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.ok_or_else(AppError::unauthorized)?;
-    let issue = queries::get_issue(&state.pool, repository.id, number)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    if !(access.can_write() || issue.author_id == user.id) {
-        return Err(AppError::forbidden());
-    }
-    let milestone_id = parse_optional_uuid(&form.milestone_id);
-    if let Some(mid) = milestone_id {
-        if queries::get_milestone(&state.pool, repository.id, mid)
-            .await?
-            .is_none()
-        {
-            return Err(AppError::bad("unknown milestone"));
-        }
-    }
-    queries::set_issue_milestone(&state.pool, issue.id, milestone_id).await?;
-    Ok(redirect_see_other(&format!(
-        "/{owner}/{repo}/issues/{number}"
-    )))
-}
-
-#[derive(Deserialize)]
 pub struct CommentForm {
     pub body: String,
 }
@@ -3453,20 +2802,9 @@ pub async fn issue_comment(
         serde_json::json!({ "number": number }),
     )
     .await?;
-    let participants = queries::list_issue_participant_ids(&state.pool, issue.id).await?;
-    let href = format!("/{owner}/{repo}/issues/{number}");
-    queries::notify_users(
-        &state.pool,
-        &participants,
-        user.id,
-        "issue.comment",
-        &format!("{} commented on issue #{number}", user.username),
-        &notif_snippet(body, 160),
-        &href,
-        Some(repository.id),
-    )
-    .await?;
-    Ok(redirect_see_other(&href))
+    Ok(redirect_see_other(&format!(
+        "/{owner}/{repo}/issues/{number}"
+    )))
 }
 
 pub async fn issue_close(
@@ -3484,20 +2822,6 @@ pub async fn issue_close(
         return Err(AppError::forbidden());
     }
     queries::set_issue_state(&state.pool, issue.id, "closed").await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_ISSUES,
-        "closed".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "number": issue.number,
-            "title": issue.title,
-            "body": issue.body,
-            "state": "closed",
-        }),
-    );
     Ok(redirect_see_other(&format!(
         "/{owner}/{repo}/issues/{number}"
     )))
@@ -3518,26 +2842,12 @@ pub async fn issue_reopen(
         return Err(AppError::forbidden());
     }
     queries::set_issue_state(&state.pool, issue.id, "open").await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_ISSUES,
-        "reopened".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "number": issue.number,
-            "title": issue.title,
-            "body": issue.body,
-            "state": "open",
-        }),
-    );
     Ok(redirect_see_other(&format!(
         "/{owner}/{repo}/issues/{number}"
     )))
 }
 
-// --- pull requests ---
+// ── pull requests ────────────────────────────────────────────────────────────
 
 pub async fn pulls_list(
     State(state): State<AppState>,
@@ -3550,33 +2860,19 @@ pub async fn pulls_list(
     if !repository.pulls_enabled {
         return Err(AppError::not_found());
     }
-    let filter = match q.state.as_deref() {
-        Some("closed") | Some("merged") | Some("open") => q.state.as_deref(),
-        _ => Some("open"),
-    };
-    let pulls = queries::list_pulls_filtered(
-        &state.pool,
-        repository.id,
-        filter,
-        q.label,
-        q.milestone,
-    )
-    .await?;
-    let items = build_pull_list_items(&state.pool, pulls).await?;
-    let labels = queries::list_labels(&state.pool, repository.id).await?;
-    let milestones = queries::list_milestones(&state.pool, repository.id, None).await?;
+    let filter = q
+        .state
+        .as_deref()
+        .filter(|s| *s == "open" || *s == "closed" || *s == "merged");
+    let pulls = queries::list_pulls(&state.pool, repository.id, filter).await?;
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(PullsListTemplate {
         viewer,
         owner: owner_user,
         repo: repository,
         access,
-        pulls: items,
-        labels,
-        milestones,
+        pulls,
         state_filter: filter.unwrap_or("open").to_string(),
-        label_filter: q.label,
-        milestone_filter: q.milestone,
         clone_http,
         clone_ssh,
     })
@@ -3597,8 +2893,6 @@ pub async fn pull_new(
         .ok()
         .and_then(|g| git::list_branches(&g).ok())
         .unwrap_or_default();
-    let labels = queries::list_labels(&state.pool, repository.id).await?;
-    let milestones = queries::list_open_milestones(&state.pool, repository.id).await?;
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(PullNewTemplate {
         viewer,
@@ -3606,8 +2900,6 @@ pub async fn pull_new(
         repo: repository,
         access,
         branches,
-        labels,
-        milestones,
         error: None,
         clone_http,
         clone_ssh,
@@ -3621,9 +2913,6 @@ pub struct PullCreateForm {
     pub body: Option<String>,
     pub source_branch: String,
     pub target_branch: String,
-    #[serde(default)]
-    pub label_id: Vec<Uuid>,
-    pub milestone_id: Option<String>,
 }
 
 pub async fn pull_create(
@@ -3646,8 +2935,6 @@ pub async fn pull_create(
             .ok()
             .and_then(|g| git::list_branches(&g).ok())
             .unwrap_or_default();
-        let labels = queries::list_labels(&state.pool, repository.id).await?;
-        let milestones = queries::list_open_milestones(&state.pool, repository.id).await?;
         let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
         return Ok(PullNewTemplate {
             viewer: Some(user),
@@ -3655,8 +2942,6 @@ pub async fn pull_create(
             repo: repository,
             access,
             branches,
-            labels,
-            milestones,
             error: Some("title, source, and target required".into()),
             clone_http,
             clone_ssh,
@@ -3680,19 +2965,6 @@ pub async fn pull_create(
         target,
     )
     .await?;
-    let label_ids =
-        queries::filter_repo_label_ids(&state.pool, repository.id, &form.label_id).await?;
-    if !label_ids.is_empty() {
-        queries::set_pull_labels(&state.pool, pull.id, &label_ids).await?;
-    }
-    if let Some(mid) = parse_optional_uuid(&form.milestone_id) {
-        if queries::get_milestone(&state.pool, repository.id, mid)
-            .await?
-            .is_some()
-        {
-            queries::set_pull_milestone(&state.pool, pull.id, Some(mid)).await?;
-        }
-    }
     queries::record_activity(
         &state.pool,
         Some(user.id),
@@ -3702,40 +2974,15 @@ pub async fn pull_create(
         serde_json::json!({ "number": number }),
     )
     .await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_PULL_REQUEST,
-        "opened".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "number": pull.number,
-            "title": pull.title,
-            "body": pull.body,
-            "state": pull.state,
-            "source_branch": pull.source_branch,
-            "target_branch": pull.target_branch,
-        }),
-    );
-    let href = format!("/{owner}/{repo}/pulls/{number}");
-    queries::notify_repo_watchers(
-        &state.pool,
-        repository.id,
-        user.id,
-        "pull.open",
-        &format!("{} opened pull #{number}: {title}", user.username),
-        &notif_snippet(&body, 160),
-        &href,
-    )
-    .await?;
-    Ok(redirect_see_other(&href))
+    Ok(redirect_see_other(&format!(
+        "/{owner}/{repo}/pulls/{}",
+        pull.number
+    )))
 }
 
 #[derive(Deserialize)]
 pub struct PullTabQuery {
     pub tab: Option<String>,
-    pub full: Option<String>,
 }
 
 pub async fn pull_view(
@@ -3761,11 +3008,6 @@ pub async fn pull_view(
         viewer.as_ref().map(|u| u.id),
     )
     .await?;
-    let reviews = review_views(
-        &state.pool,
-        queries::list_reviews_for_pull(&state.pool, pull.id).await?,
-    )
-    .await?;
 
     let tab = match q.tab.as_deref() {
         Some("commits") => "commits",
@@ -3773,7 +3015,6 @@ pub async fn pull_view(
         _ => "conversation",
     }
     .to_string();
-    let show_full = query_full(q.full.as_deref());
 
     let mut commits = Vec::new();
     let mut diff_files = Vec::new();
@@ -3789,7 +3030,7 @@ pub async fn pull_view(
         commits = commit_views(&state, prepared).await;
         let diff =
             git::branch_diff(&g, &pull.source_branch, &pull.target_branch).unwrap_or_default();
-        diff_files = parse_diff_files(&diff, show_full);
+        diff_files = parse_diff_files(&diff);
     }
 
     let can_merge = access.can_write() && pull.state == "open";
@@ -3804,30 +3045,7 @@ pub async fn pull_view(
         merge_styles.push("rebase".to_string());
     }
 
-    let labels = queries::list_pull_labels(&state.pool, pull.id).await?;
-    let all_labels = queries::list_labels(&state.pool, repository.id).await?;
-    let label_options = label_options(all_labels, &labels);
-    let milestone = match pull.milestone_id {
-        Some(id) => queries::get_milestone(&state.pool, repository.id, id).await?,
-        None => None,
-    };
-    let mut milestone_choices = queries::list_open_milestones(&state.pool, repository.id).await?;
-    if let Some(ref m) = milestone {
-        if m.state != "open" && !milestone_choices.iter().any(|x| x.id == m.id) {
-            milestone_choices.push(m.clone());
-        }
-    }
-    let milestone_options = milestone_options(milestone_choices, pull.milestone_id);
-    let can_triage = viewer
-        .as_ref()
-        .map(|u| access.can_write() || pull.author_id == u.id)
-        .unwrap_or(false);
-    let is_author = viewer
-        .as_ref()
-        .map(|u| u.id == pull.author_id)
-        .unwrap_or(false);
-
-    let conversation_count = comments.len() + reviews.len() + 1;
+    let conversation_count = comments.len() + 1;
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     Ok(PullViewTemplate {
         viewer,
@@ -3839,78 +3057,17 @@ pub async fn pull_view(
         pull,
         author,
         comments,
-        reviews,
         commits,
         conversation_count,
         diff_files,
         tab,
-        show_full,
         can_merge,
         merge_styles,
-        labels,
-        label_options,
-        milestone,
-        milestone_options,
-        can_triage,
-        is_author,
         clone_http,
         clone_ssh,
     })
 }
 
-pub async fn pull_labels_save(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, number)): Path<(String, String, i32)>,
-    Form(form): Form<LabelsAssignForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.ok_or_else(AppError::unauthorized)?;
-    let pull = queries::get_pull(&state.pool, repository.id, number)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    if !(access.can_write() || pull.author_id == user.id) {
-        return Err(AppError::forbidden());
-    }
-    let label_ids =
-        queries::filter_repo_label_ids(&state.pool, repository.id, &form.label_id).await?;
-    queries::set_pull_labels(&state.pool, pull.id, &label_ids).await?;
-    Ok(redirect_see_other(&format!(
-        "/{owner}/{repo}/pulls/{number}"
-    )))
-}
-
-pub async fn pull_milestone_save(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, number)): Path<(String, String, i32)>,
-    Form(form): Form<MilestoneAssignForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.ok_or_else(AppError::unauthorized)?;
-    let pull = queries::get_pull(&state.pool, repository.id, number)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    if !(access.can_write() || pull.author_id == user.id) {
-        return Err(AppError::forbidden());
-    }
-    let milestone_id = parse_optional_uuid(&form.milestone_id);
-    if let Some(mid) = milestone_id {
-        if queries::get_milestone(&state.pool, repository.id, mid)
-            .await?
-            .is_none()
-        {
-            return Err(AppError::bad("unknown milestone"));
-        }
-    }
-    queries::set_pull_milestone(&state.pool, pull.id, milestone_id).await?;
-    Ok(redirect_see_other(&format!(
-        "/{owner}/{repo}/pulls/{number}"
-    )))
-}
-
 pub async fn pull_comment(
     State(state): State<AppState>,
     headers: HeaderMap,
@@ -3945,86 +3102,7 @@ pub async fn pull_comment(
         serde_json::json!({ "number": number }),
     )
     .await?;
-    let participants = queries::list_pull_participant_ids(&state.pool, pull.id).await?;
-    let href = format!("/{owner}/{repo}/pulls/{number}");
-    queries::notify_users(
-        &state.pool,
-        &participants,
-        user.id,
-        "pull.comment",
-        &format!("{} commented on pull #{number}", user.username),
-        &notif_snippet(body, 160),
-        &href,
-        Some(repository.id),
-    )
-    .await?;
-    Ok(redirect_see_other(&href))
-}
-
-#[derive(Deserialize)]
-pub struct PullReviewForm {
-    pub state: String,
-    pub body: Option<String>,
-}
-
-pub async fn pull_review(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, number)): Path<(String, String, i32)>,
-    Form(form): Form<PullReviewForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, _a) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.ok_or_else(AppError::unauthorized)?;
-    let pull = queries::get_pull(&state.pool, repository.id, number)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    if pull.state != "open" {
-        return Err(AppError::bad("pull request is not open"));
-    }
-    let state_name = match form.state.trim() {
-        "approved" => "approved",
-        "changes_requested" => "changes_requested",
-        "commented" => "commented",
-        _ => return Err(AppError::bad("invalid review state")),
-    };
-    if user.id == pull.author_id && state_name != "commented" {
-        return Err(AppError::bad(
-            "cannot approve or request changes on your own pull request",
-        ));
-    }
-    let body = form.body.as_deref().unwrap_or("").trim();
-    queries::create_review(&state.pool, pull.id, user.id, state_name, body).await?;
-    queries::record_activity(
-        &state.pool,
-        Some(user.id),
-        Some(repository.id),
-        "pull.review",
-        &format!("reviewed pull #{number} ({state_name})"),
-        serde_json::json!({ "number": number, "state": state_name }),
-    )
-    .await?;
-    let participants = queries::list_pull_participant_ids(&state.pool, pull.id).await?;
-    let href = format!("/{owner}/{repo}/pulls/{number}");
-    let summary = match state_name {
-        "approved" => format!("{} approved pull #{number}", user.username),
-        "changes_requested" => {
-            format!("{} requested changes on pull #{number}", user.username)
-        }
-        _ => format!("{} left a review on pull #{number}", user.username),
-    };
-    queries::notify_users(
-        &state.pool,
-        &participants,
-        user.id,
-        "pull.review",
-        &summary,
-        &notif_snippet(body, 160),
-        &href,
-        Some(repository.id),
-    )
-    .await?;
-    Ok(redirect_see_other(&href))
+    Ok(redirect_see_other(&format!("/{owner}/{repo}/pulls/{number}")))
 }
 
 #[derive(Deserialize)]
@@ -4096,23 +3174,6 @@ pub async fn pull_merge(
         serde_json::json!({ "number": number, "commit": merge_commit }),
     )
     .await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_PULL_REQUEST,
-        "merged".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "number": pull.number,
-            "title": pull.title,
-            "body": pull.body,
-            "state": "merged",
-            "source_branch": pull.source_branch,
-            "target_branch": pull.target_branch,
-            "merge_commit": merge_commit,
-        }),
-    );
     Ok(redirect_see_other(&format!("/{owner}/{repo}/pulls/{number}")))
 }
 
@@ -4134,26 +3195,10 @@ pub async fn pull_close(
         return Err(AppError::bad("pull is not open"));
     }
     queries::set_pull_state(&state.pool, pull.id, "closed", None).await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_PULL_REQUEST,
-        "closed".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "number": pull.number,
-            "title": pull.title,
-            "body": pull.body,
-            "state": "closed",
-            "source_branch": pull.source_branch,
-            "target_branch": pull.target_branch,
-        }),
-    );
     Ok(redirect_see_other(&format!("/{owner}/{repo}/pulls/{number}")))
 }
 
-// ── releases ─────────────────────────────────────────────────────────────────
+// ── releases ─────────────────────────────────────────────────────────────────
 
 
 fn asset_views(assets: Vec<crate::db::models::ReleaseAsset>) -> Vec<ReleaseAssetView> {
@@ -4341,35 +3386,10 @@ pub async fn release_create(
         serde_json::json!({ "tag": tag }),
     )
     .await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_RELEASE,
-        "created".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "tag_name": release.tag_name,
-            "title": release.title,
-            "body": release.body,
-            "is_prerelease": release.is_prerelease,
-            "is_draft": release.is_draft,
-        }),
-    );
-    let href = format!("/{owner}/{repo}/releases/{}", release.tag_name);
-    if !is_draft {
-        queries::notify_repo_watchers(
-            &state.pool,
-            repository.id,
-            user.id,
-            "release.publish",
-            &format!("{} published release {}", user.username, release.tag_name),
-            &title,
-            &href,
-        )
-        .await?;
-    }
-    Ok(redirect_see_other(&href))
+    Ok(redirect_see_other(&format!(
+        "/{owner}/{repo}/releases/{}",
+        release.tag_name
+    )))
 }
 
 pub async fn release_view(
@@ -4491,7 +3511,6 @@ pub async fn release_update(
     let release = queries::get_release(&state.pool, repository.id, &tag)
         .await?
         .ok_or_else(AppError::not_found)?;
-    let was_draft = release.is_draft;
 
     let new_tag = form.tag_name.trim().to_string();
     let title = form.title.trim().to_string();
@@ -4586,35 +3605,10 @@ pub async fn release_update(
         serde_json::json!({ "tag": updated.tag_name }),
     )
     .await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_RELEASE,
-        "edited".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "tag_name": updated.tag_name,
-            "title": updated.title,
-            "body": updated.body,
-            "is_prerelease": updated.is_prerelease,
-            "is_draft": updated.is_draft,
-        }),
-    );
-    let href = format!("/{owner}/{repo}/releases/{}", updated.tag_name);
-    if was_draft && !updated.is_draft {
-        queries::notify_repo_watchers(
-            &state.pool,
-            repository.id,
-            user.id,
-            "release.publish",
-            &format!("{} published release {}", user.username, updated.tag_name),
-            &updated.title,
-            &href,
-        )
-        .await?;
-    }
-    Ok(redirect_see_other(&href))
+    Ok(redirect_see_other(&format!(
+        "/{owner}/{repo}/releases/{}",
+        updated.tag_name
+    )))
 }
 
 pub async fn release_delete(
@@ -4655,21 +3649,6 @@ pub async fn release_delete(
         serde_json::json!({ "tag": tag }),
     )
     .await?;
-    crate::webhooks::spawn_dispatch(
-        state.pool.clone(),
-        crate::webhooks::EVENT_RELEASE,
-        "deleted".into(),
-        repository.clone(),
-        owner.clone(),
-        Some(user.clone()),
-        serde_json::json!({
-            "tag_name": tag,
-            "title": release.title,
-            "body": release.body,
-            "is_prerelease": release.is_prerelease,
-            "is_draft": release.is_draft,
-        }),
-    );
     Ok(redirect_see_other(&format!("/{owner}/{repo}/releases")))
 }
 
@@ -4801,7 +3780,7 @@ fn serve_file(path: &std::path::Path, filename: &str, content_type: &str) -> App
         .unwrap())
 }
 
-// ── repo settings ────────────────────────────────────────────────────────────
+// ── repo settings ────────────────────────────────────────────────────────────
 
 pub async fn repo_settings(
     State(state): State<AppState>,
@@ -4810,8 +3789,7 @@ pub async fn repo_settings(
 ) -> AppResult<impl IntoResponse> {
     let (repository, owner_user, viewer, access) =
         load_repo_context(&state, &owner, &repo, &headers).await?;
-    let is_site_admin = viewer.as_ref().map(|u| u.is_site_admin).unwrap_or(false);
-    if !access.can_admin() && !is_site_admin {
+    if !access.can_admin() {
         return Err(AppError::forbidden());
     }
     let collaborators = queries::list_collaborators(&state.pool, repository.id).await?;
@@ -4835,46 +3813,7 @@ pub async fn repo_settings(
     }
     let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
     let branch_rules = queries::list_branch_rules(&state.pool, repository.id).await?;
-    let mirror = queries::get_repo_mirror(&state.pool, repository.id).await?;
-    let deploy_keys = crate::db::deploy_keys::list_deploy_keys(&state.pool, repository.id).await?;
-    let webhooks: Vec<WebhookView> = queries::list_webhooks(&state.pool, repository.id)
-        .await?
-        .into_iter()
-        .map(|h| WebhookView {
-            id: h.id,
-            url: h.url,
-            has_secret: !h.secret.is_empty(),
-            events_label: if h.events.is_empty() {
-                "(none)".into()
-            } else {
-                h.events.join(", ")
-            },
-            active: h.active,
-            created_at: h.created_at.to_rfc3339(),
-        })
-        .collect();
-    let webhook_deliveries: Vec<WebhookDeliveryView> =
-        queries::list_recent_webhook_deliveries(&state.pool, repository.id, 20)
-            .await?
-            .into_iter()
-            .map(|(d, url)| {
-                let status_label = match (d.success, d.status_code, d.error.as_deref()) {
-                    (true, Some(code), _) => format!("{code}"),
-                    (false, Some(code), Some(err)) => format!("{code}: {err}"),
-                    (false, Some(code), None) => format!("{code}"),
-                    (false, None, Some(err)) => err.to_string(),
-                    _ => "unknown".into(),
-                };
-                WebhookDeliveryView {
-                    event: d.event,
-                    action: d.action,
-                    success: d.success,
-                    status_label,
-                    webhook_url: url,
-                    created_at: d.created_at.to_rfc3339(),
-                }
-            })
-            .collect();
+    let is_site_admin = viewer.as_ref().map(|u| u.is_site_admin).unwrap_or(false);
     Ok(RepoSettingsTemplate {
         viewer,
         owner: owner_user,
@@ -4885,10 +3824,6 @@ pub async fn repo_settings(
         clone_http,
         clone_ssh,
         branch_rules,
-        mirror,
-        deploy_keys,
-        webhooks,
-        webhook_deliveries,
         is_site_admin,
         error: None,
     })
@@ -5019,139 +3954,6 @@ pub async fn collab_remove(
 }
 
 #[derive(Deserialize)]
-pub struct DeployKeyAddForm {
-    pub name: String,
-    pub public_key: String,
-    /// When set (checkbox), key may push; otherwise read-only.
-    pub write_access: Option<String>,
-}
-
-pub async fn deploy_key_add(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Form(form): Form<DeployKeyAddForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let created_by = viewer.map(|u| u.id);
-    let name = form.name.trim();
-    let public_key = form.public_key.trim();
-    if name.is_empty() || public_key.is_empty() {
-        return Err(AppError::bad("name and public key required"));
-    }
-    let fp = fingerprint_ssh_pubkey(public_key).map_err(|e| AppError::bad(e.to_string()))?;
-    let read_only = !checkbox(&form.write_access);
-    crate::db::deploy_keys::add_deploy_key(
-        &state.pool,
-        repository.id,
-        name,
-        public_key,
-        &fp,
-        read_only,
-        created_by,
-    )
-    .await
-    .map_err(|e| AppError::bad(format!("could not add deploy key: {e}")))?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
-pub async fn deploy_key_delete(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-) -> AppResult<Response> {
-    let (repository, _o, _viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    crate::db::deploy_keys::delete_deploy_key(&state.pool, repository.id, id).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
-#[derive(Deserialize)]
-pub struct WebhookAddForm {
-    pub url: String,
-    pub secret: Option<String>,
-    pub event_push: Option<String>,
-    pub event_issues: Option<String>,
-    pub event_pull_request: Option<String>,
-    pub event_release: Option<String>,
-}
-
-pub async fn webhook_add(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Form(form): Form<WebhookAddForm>,
-) -> AppResult<Response> {
-    let (repository, _o, _viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let url = form.url.trim();
-    if !(url.starts_with("http://") || url.starts_with("https://")) {
-        return Err(AppError::bad("url must start with http:// or https://"));
-    }
-    let mut events = Vec::new();
-    if checkbox(&form.event_push) {
-        events.push(crate::webhooks::EVENT_PUSH.to_string());
-    }
-    if checkbox(&form.event_issues) {
-        events.push(crate::webhooks::EVENT_ISSUES.to_string());
-    }
-    if checkbox(&form.event_pull_request) {
-        events.push(crate::webhooks::EVENT_PULL_REQUEST.to_string());
-    }
-    if checkbox(&form.event_release) {
-        events.push(crate::webhooks::EVENT_RELEASE.to_string());
-    }
-    let events = crate::webhooks::normalize_events(&events);
-    if events.is_empty() {
-        return Err(AppError::bad("select at least one event"));
-    }
-    let secret = form.secret.as_deref().unwrap_or("").trim();
-    queries::create_webhook(&state.pool, repository.id, url, secret, &events).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
-pub async fn webhook_delete(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-) -> AppResult<Response> {
-    let (repository, _o, _viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    queries::delete_webhook(&state.pool, id, repository.id).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
-pub async fn webhook_toggle(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-) -> AppResult<Response> {
-    let (repository, _o, _viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let hook = queries::get_webhook(&state.pool, id, repository.id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    queries::set_webhook_active(&state.pool, id, repository.id, !hook.active).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
-#[derive(Deserialize)]
 pub struct ArchiveForm {
     pub archived: Option<String>,
 }
src/web/routes_extra.rs +13 −599
diff --git a/src/web/routes_extra.rs b/src/web/routes_extra.rs
index b5767bf..712ab5f 100644
--- a/src/web/routes_extra.rs
+++ b/src/web/routes_extra.rs
@@ -1,15 +1,14 @@
 //! Social, account, branch, raw, and admin helpers.
 
 use super::routes::{
-    audit_entries_view, avatar_url_for, clone_urls, load_repo_context, record_audit,
-    redirect_see_other, require_login, AppError, AppResult,
+    avatar_url_for, load_repo_context, redirect_see_other, require_login, AppError, AppResult,
 };
 use crate::auth::{self, clear_session_cookie, hash_token, token_from_headers};
 use crate::db::queries;
 use crate::git;
 use crate::state::AppState;
 use crate::web::templates::*;
-use axum::extract::{Form, Path, Query, State};
+use axum::extract::{Form, Path, State};
 use axum::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE, SET_COOKIE};
 use axum::http::{HeaderMap, HeaderValue, StatusCode};
 use axum::response::{IntoResponse, Response};
@@ -234,14 +233,13 @@ pub async fn branch_rule_delete(
 
 #[derive(Deserialize)]
 pub struct RenameBranchForm {
-    pub branch: String,
     pub new_name: String,
 }
 
 pub async fn branch_rename(
     State(state): State<AppState>,
     headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
+    Path((owner, repo, branch)): Path<(String, String, String)>,
     Form(form): Form<RenameBranchForm>,
 ) -> AppResult<Response> {
     let (repository, _, viewer, access) =
@@ -250,38 +248,22 @@ pub async fn branch_rename(
     if !access.can_write() {
         return Err(AppError::forbidden());
     }
-    let branch = form.branch.trim();
-    if branch.is_empty() {
-        return Err(AppError::bad("branch required"));
-    }
     if branch == repository.default_branch {
         return Err(AppError::bad("cannot rename default branch here"));
     }
     let new_name = form.new_name.trim();
-    // Allow hierarchical names like feat/foo; reject empty / traversal / whitespace.
-    if new_name.is_empty()
-        || new_name.contains("..")
-        || new_name.contains(char::is_whitespace)
-        || new_name.starts_with('/')
-        || new_name.ends_with('/')
-    {
+    if new_name.is_empty() || new_name.contains('/') {
         return Err(AppError::bad("invalid branch name"));
     }
     let grepo = git::open_bare(&state.config.repos_dir(), &owner, &repo)?;
-    git::rename_branch(&grepo, branch, new_name)?;
+    git::rename_branch(&grepo, &branch, new_name)?;
     Ok(redirect_see_other(&format!("/{owner}/{repo}/branches")))
 }
 
-#[derive(Deserialize)]
-pub struct DeleteBranchForm {
-    pub branch: String,
-}
-
 pub async fn branch_delete(
     State(state): State<AppState>,
     headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Form(form): Form<DeleteBranchForm>,
+    Path((owner, repo, branch)): Path<(String, String, String)>,
 ) -> AppResult<Response> {
     let (repository, _, viewer, access) =
         load_repo_context(&state, &owner, &repo, &headers).await?;
@@ -289,15 +271,11 @@ pub async fn branch_delete(
     if !access.can_write() {
         return Err(AppError::forbidden());
     }
-    let branch = form.branch.trim();
-    if branch.is_empty() {
-        return Err(AppError::bad("branch required"));
-    }
     if branch == repository.default_branch {
         return Err(AppError::bad("cannot delete default branch"));
     }
     let grepo = git::open_bare(&state.config.repos_dir(), &owner, &repo)?;
-    git::delete_branch(&grepo, branch)?;
+    git::delete_branch(&grepo, &branch)?;
     Ok(redirect_see_other(&format!("/{owner}/{repo}/branches")))
 }
 
@@ -343,14 +321,13 @@ pub async fn tag_create(
 
 #[derive(Deserialize)]
 pub struct RenameTagForm {
-    pub tag: String,
     pub new_name: String,
 }
 
 pub async fn tag_rename(
     State(state): State<AppState>,
     headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
+    Path((owner, repo, tag)): Path<(String, String, String)>,
     Form(form): Form<RenameTagForm>,
 ) -> AppResult<Response> {
     let (repository, _, viewer, access) =
@@ -359,25 +336,21 @@ pub async fn tag_rename(
     if !access.can_write() {
         return Err(AppError::forbidden());
     }
-    let tag = form.tag.trim();
-    if tag.is_empty() {
-        return Err(AppError::bad("tag required"));
-    }
     let new_name = form.new_name.trim();
     if new_name.is_empty() || new_name.contains("..") {
         return Err(AppError::bad("invalid tag name"));
     }
     let grepo = git::open_bare(&state.config.repos_dir(), &owner, &repo)?;
-    git::rename_tag(&grepo, tag, new_name)
+    git::rename_tag(&grepo, &tag, new_name)
         .map_err(|e| AppError::bad(format!("rename tag failed: {e}")))?;
     if let Some(release) =
-        queries::rename_release_tag(&state.pool, repository.id, tag, new_name).await?
+        queries::rename_release_tag(&state.pool, repository.id, &tag, new_name).await?
     {
         let old_dir = state
             .config
             .releases_dir()
             .join(repository.id.to_string())
-            .join(tag);
+            .join(&tag);
         let new_dir = state
             .config
             .releases_dir()
@@ -394,16 +367,10 @@ pub async fn tag_rename(
     Ok(redirect_see_other(&format!("/{owner}/{repo}/branches")))
 }
 
-#[derive(Deserialize)]
-pub struct DeleteTagForm {
-    pub tag: String,
-}
-
 pub async fn tag_delete(
     State(state): State<AppState>,
     headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Form(form): Form<DeleteTagForm>,
+    Path((owner, repo, tag)): Path<(String, String, String)>,
 ) -> AppResult<Response> {
     let (_repository, _, viewer, access) =
         load_repo_context(&state, &owner, &repo, &headers).await?;
@@ -411,12 +378,8 @@ pub async fn tag_delete(
     if !access.can_write() {
         return Err(AppError::forbidden());
     }
-    let tag = form.tag.trim();
-    if tag.is_empty() {
-        return Err(AppError::bad("tag required"));
-    }
     let grepo = git::open_bare(&state.config.repos_dir(), &owner, &repo)?;
-    git::delete_tag(&grepo, tag).map_err(|e| AppError::bad(format!("delete tag failed: {e}")))?;
+    git::delete_tag(&grepo, &tag).map_err(|e| AppError::bad(format!("delete tag failed: {e}")))?;
     Ok(redirect_see_other(&format!("/{owner}/{repo}/branches")))
 }
 
@@ -451,15 +414,12 @@ pub async fn account_settings(
             }
         })
         .collect();
-    let audit_rows = queries::list_audit_log_for_user(&state.pool, user.id, 50).await?;
-    let audit_entries = audit_entries_view(audit_rows);
     Ok(AccountSettingsTemplate {
         viewer: Some(user.clone()),
         user,
         emails,
         sessions,
         current_session_id,
-        audit_entries,
         error: None,
         message: None,
     })
@@ -557,15 +517,6 @@ pub async fn mfa_confirm(
     let hashes: Vec<String> = codes.iter().map(|c| crate::mfa::hash_recovery_code(c)).collect();
     let secret = pending.to_string();
     queries::mfa_confirm_enroll(&state.pool, user.id, &secret, &hashes).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "mfa.enable",
-        serde_json::json!({}),
-    )
-    .await;
     let mfa = queries::get_user_mfa(&state.pool, user.id).await?;
     Ok(mfa_page(
         user,
@@ -615,15 +566,6 @@ pub async fn mfa_disable(
         .into_response());
     }
     queries::mfa_disable(&state.pool, user.id).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "mfa.disable",
-        serde_json::json!({}),
-    )
-    .await;
     Ok(redirect_see_other("/settings/mfa").into_response())
 }
 
@@ -668,15 +610,6 @@ pub async fn account_change_username(
             std::fs::rename(&old_root, &new_root)?;
         }
         queries::update_username(&state.pool, user.id, &username).await?;
-        record_audit(
-            &state,
-            &headers,
-            user.id,
-            Some(user.id),
-            "username.change",
-            serde_json::json!({ "from": user.username, "to": username }),
-        )
-        .await;
     }
     Ok(redirect_see_other("/settings/account"))
 }
@@ -706,15 +639,6 @@ pub async fn account_change_password(
     auth::authentik_set_password(&state.auth, pk, &form.new_password)
         .await
         .map_err(|e| AppError::bad(crate::mfa::sanitize_user_error(&e.to_string())))?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "password.change",
-        serde_json::json!({}),
-    )
-    .await;
     Ok(redirect_see_other("/settings/account"))
 }
 
@@ -737,37 +661,6 @@ pub async fn account_privacy(
         checkbox(&form.vigilant_mode),
     )
     .await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "privacy.update",
-        serde_json::json!({
-            "show_email": checkbox(&form.show_email),
-            "vigilant_mode": checkbox(&form.vigilant_mode),
-        }),
-    )
-    .await;
-    Ok(redirect_see_other("/settings/account"))
-}
-
-#[derive(Deserialize)]
-pub struct ThemeForm {
-    pub theme: String,
-}
-
-pub async fn account_theme(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Form(form): Form<ThemeForm>,
-) -> AppResult<Response> {
-    let user = require_login(&state.auth, &headers).await?;
-    let theme = match form.theme.trim() {
-        "light" | "dark" | "system" => form.theme.trim(),
-        _ => return Err(AppError::bad("theme must be light, dark, or system")),
-    };
-    queries::update_user_theme(&state.pool, user.id, theme).await?;
     Ok(redirect_see_other("/settings/account"))
 }
 
@@ -787,15 +680,6 @@ pub async fn account_add_email(
         return Err(AppError::bad("invalid email"));
     }
     queries::add_user_email(&state.pool, user.id, email).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "email.add",
-        serde_json::json!({ "email": email }),
-    )
-    .await;
     Ok(redirect_see_other("/settings/account"))
 }
 
@@ -806,34 +690,6 @@ pub async fn account_delete_email(
 ) -> AppResult<Response> {
     let user = require_login(&state.auth, &headers).await?;
     queries::delete_user_email(&state.pool, id, user.id).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "email.delete",
-        serde_json::json!({ "email_id": id }),
-    )
-    .await;
-    Ok(redirect_see_other("/settings/account"))
-}
-
-pub async fn account_set_primary_email(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path(id): Path<Uuid>,
-) -> AppResult<Response> {
-    let user = require_login(&state.auth, &headers).await?;
-    queries::set_primary_email(&state.pool, user.id, id).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "email.primary",
-        serde_json::json!({ "email_id": id }),
-    )
-    .await;
     Ok(redirect_see_other("/settings/account"))
 }
 
@@ -844,15 +700,6 @@ pub async fn account_revoke_session(
 ) -> AppResult<Response> {
     let user = require_login(&state.auth, &headers).await?;
     queries::delete_session_by_id(&state.pool, id, user.id).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "session.revoke",
-        serde_json::json!({ "session_id": id }),
-    )
-    .await;
     Ok(redirect_see_other("/settings/account"))
 }
 
@@ -865,15 +712,6 @@ pub async fn account_revoke_others(
         return Err(AppError::unauthorized());
     };
     queries::delete_other_sessions(&state.pool, user.id, &hash_token(&token)).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "session.revoke_others",
-        serde_json::json!({}),
-    )
-    .await;
     Ok(redirect_see_other("/settings/account"))
 }
 
@@ -949,15 +787,6 @@ pub async fn gpg_add(
     let fp = crate::git::verify::gpg_fingerprint_from_armor(key)
         .unwrap_or_else(|_| crate::git::verify::gpg_fingerprint_fallback(key));
     queries::add_gpg_key(&state.pool, user.id, form.name.trim(), key, &fp).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "gpg_key.add",
-        serde_json::json!({ "name": form.name.trim(), "fingerprint": fp }),
-    )
-    .await;
     Ok(redirect_see_other("/settings/keys"))
 }
 
@@ -968,426 +797,11 @@ pub async fn gpg_delete(
 ) -> AppResult<Response> {
     let user = require_login(&state.auth, &headers).await?;
     queries::delete_gpg_key(&state.pool, id, user.id).await?;
-    record_audit(
-        &state,
-        &headers,
-        user.id,
-        Some(user.id),
-        "gpg_key.delete",
-        serde_json::json!({ "key_id": id }),
-    )
-    .await;
     Ok(redirect_see_other("/settings/keys"))
 }
 
-fn can_manage_mirror(access: crate::db::models::Access, viewer: &Option<crate::db::models::User>) -> bool {
-    access.can_admin() || viewer.as_ref().map(|u| u.is_site_admin).unwrap_or(false)
-}
-
-fn truncate_err(msg: &str) -> String {
-    const MAX: usize = 2000;
-    let msg = msg.trim();
-    if msg.len() <= MAX {
-        msg.to_string()
-    } else {
-        format!("{}…", &msg[..MAX])
-    }
-}
-
-#[derive(Deserialize)]
-pub struct MirrorForm {
-    pub remote_url: String,
-    pub enabled: Option<String>,
-}
-
-pub async fn mirror_save(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Form(form): Form<MirrorForm>,
-) -> AppResult<Response> {
-    let (repository, _, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.as_ref().ok_or_else(AppError::unauthorized)?;
-    if !can_manage_mirror(access, &viewer) {
-        return Err(AppError::forbidden());
-    }
-    let url = form.remote_url.trim();
-    if url.is_empty() {
-        return Err(AppError::bad("mirror URL required"));
-    }
-    if !git::is_safe_mirror_url(url) {
-        return Err(AppError::bad(
-            "mirror URL must be http(s), git://, ssh://, or git@host:path",
-        ));
-    }
-    queries::upsert_repo_mirror(
-        &state.pool,
-        repository.id,
-        url,
-        checkbox(&form.enabled),
-        user.id,
-    )
-    .await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
-pub async fn mirror_sync(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-) -> AppResult<Response> {
-    let (repository, _, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.as_ref().ok_or_else(AppError::unauthorized)?;
-    if !can_manage_mirror(access, &viewer) {
-        return Err(AppError::forbidden());
-    }
-    let mirror = queries::get_repo_mirror(&state.pool, repository.id)
-        .await?
-        .ok_or_else(|| AppError::bad("no mirror configured"))?;
-    if !mirror.enabled {
-        return Err(AppError::bad("mirror is disabled"));
-    }
-
-    let repos_dir = state.config.repos_dir();
-    let owner_s = owner.clone();
-    let repo_s = repo.clone();
-    let remote_url = mirror.remote_url.clone();
-    let result = tokio::task::spawn_blocking(move || {
-        git::mirror_fetch(&repos_dir, &owner_s, &repo_s, &remote_url)
-    })
-    .await
-    .map_err(|e| AppError::internal(format!("mirror sync join: {e}")))?;
-
-    match result {
-        Ok(()) => {
-            queries::set_mirror_sync_result(&state.pool, repository.id, true, None).await?;
-        }
-        Err(e) => {
-            let msg = truncate_err(&format!("{e:#}"));
-            queries::set_mirror_sync_result(&state.pool, repository.id, false, Some(&msg)).await?;
-            return Err(AppError::bad(format!("mirror sync failed: {msg}")));
-        }
-    }
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
-pub async fn mirror_delete(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-) -> AppResult<Response> {
-    let (repository, _, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.as_ref().ok_or_else(AppError::unauthorized)?;
-    if !can_manage_mirror(access, &viewer) {
-        return Err(AppError::forbidden());
-    }
-    queries::delete_repo_mirror(&state.pool, repository.id).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/settings")))
-}
-
 // silence unused import warning for avatar_url_for if not used
 #[allow(dead_code)]
 fn _use_avatar(u: &crate::db::models::User) -> String {
     avatar_url_for(u)
 }
-
-// labels & milestones management handlers for routes_extra.rs
-
-fn normalize_label_color(raw: &str) -> Option<String> {
-    let hex = raw.trim().trim_start_matches('#').to_lowercase();
-    if hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
-        Some(hex)
-    } else {
-        None
-    }
-}
-
-fn parse_due_on(raw: &Option<String>) -> Option<chrono::NaiveDate> {
-    raw.as_deref()
-        .map(str::trim)
-        .filter(|s| !s.is_empty())
-        .and_then(|s| chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").ok())
-}
-
-pub async fn labels_list(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-) -> AppResult<impl IntoResponse> {
-    let (repository, owner_user, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let labels = queries::list_labels(&state.pool, repository.id).await?;
-    let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
-    Ok(LabelsListTemplate {
-        viewer,
-        owner: owner_user,
-        repo: repository,
-        access,
-        labels,
-        error: None,
-        clone_http,
-        clone_ssh,
-    })
-}
-
-#[derive(Deserialize)]
-pub struct LabelForm {
-    pub name: String,
-    pub color: String,
-    pub description: Option<String>,
-}
-
-pub async fn label_create(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Form(form): Form<LabelForm>,
-) -> AppResult<Response> {
-    let (repository, owner_user, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let name = form.name.trim();
-    let color = normalize_label_color(&form.color);
-    let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
-    if name.is_empty() || color.is_none() {
-        let labels = queries::list_labels(&state.pool, repository.id).await?;
-        return Ok(LabelsListTemplate {
-            viewer: Some(user),
-            owner: owner_user,
-            repo: repository,
-            access,
-            labels,
-            error: Some("name and a 6-digit hex color are required".into()),
-            clone_http,
-            clone_ssh,
-        }
-        .into_response());
-    }
-    let desc = form.description.unwrap_or_default();
-    queries::create_label(&state.pool, repository.id, name, &color.unwrap(), desc.trim())
-        .await
-        .map_err(|e| AppError::bad(format!("could not create label: {e}")))?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/labels")))
-}
-
-pub async fn label_update(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-    Form(form): Form<LabelForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let _ = queries::get_label(&state.pool, repository.id, id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    let name = form.name.trim();
-    let color = normalize_label_color(&form.color).ok_or_else(|| AppError::bad("bad color"))?;
-    if name.is_empty() {
-        return Err(AppError::bad("name required"));
-    }
-    let desc = form.description.unwrap_or_default();
-    queries::update_label(&state.pool, id, name, &color, desc.trim()).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/labels")))
-}
-
-pub async fn label_delete(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let _ = queries::get_label(&state.pool, repository.id, id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    queries::delete_label(&state.pool, id).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/labels")))
-}
-
-#[derive(Deserialize)]
-pub struct MilestoneStateFilter {
-    pub state: Option<String>,
-}
-
-pub async fn milestones_list(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Query(q): Query<MilestoneStateFilter>,
-) -> AppResult<impl IntoResponse> {
-    let (repository, owner_user, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let state_filter = match q.state.as_deref() {
-        Some("closed") => "closed",
-        Some("all") => "all",
-        _ => "open",
-    };
-    let milestones = if state_filter == "all" {
-        queries::list_milestones(&state.pool, repository.id, None).await?
-    } else {
-        queries::list_milestones(&state.pool, repository.id, Some(state_filter)).await?
-    };
-    let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
-    Ok(MilestonesListTemplate {
-        viewer,
-        owner: owner_user,
-        repo: repository,
-        access,
-        milestones,
-        state_filter: state_filter.to_string(),
-        error: None,
-        clone_http,
-        clone_ssh,
-    })
-}
-
-#[derive(Deserialize)]
-pub struct MilestoneForm {
-    pub title: String,
-    pub description: Option<String>,
-    pub due_on: Option<String>,
-}
-
-pub async fn milestone_create(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo)): Path<(String, String)>,
-    Form(form): Form<MilestoneForm>,
-) -> AppResult<Response> {
-    let (repository, owner_user, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let title = form.title.trim();
-    let (clone_http, clone_ssh) = clone_urls(&state, &owner, &repo);
-    if title.is_empty() {
-        let milestones = queries::list_open_milestones(&state.pool, repository.id).await?;
-        return Ok(MilestonesListTemplate {
-            viewer: Some(user),
-            owner: owner_user,
-            repo: repository,
-            access,
-            milestones,
-            state_filter: "open".into(),
-            error: Some("title required".into()),
-            clone_http,
-            clone_ssh,
-        }
-        .into_response());
-    }
-    let desc = form.description.unwrap_or_default();
-    queries::create_milestone(
-        &state.pool,
-        repository.id,
-        title,
-        desc.trim(),
-        parse_due_on(&form.due_on),
-    )
-    .await
-    .map_err(|e| AppError::bad(format!("could not create milestone: {e}")))?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/milestones")))
-}
-
-pub async fn milestone_update(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-    Form(form): Form<MilestoneForm>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let _ = queries::get_milestone(&state.pool, repository.id, id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    let title = form.title.trim();
-    if title.is_empty() {
-        return Err(AppError::bad("title required"));
-    }
-    let desc = form.description.unwrap_or_default();
-    queries::update_milestone(
-        &state.pool,
-        id,
-        title,
-        desc.trim(),
-        parse_due_on(&form.due_on),
-    )
-    .await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/milestones")))
-}
-
-pub async fn milestone_close(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let _ = queries::get_milestone(&state.pool, repository.id, id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    queries::set_milestone_state(&state.pool, id, "closed").await?;
-    Ok(redirect_see_other(&format!(
-        "/{owner}/{repo}/milestones?state=closed"
-    )))
-}
-
-pub async fn milestone_reopen(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let _ = queries::get_milestone(&state.pool, repository.id, id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    queries::set_milestone_state(&state.pool, id, "open").await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/milestones")))
-}
-
-pub async fn milestone_delete(
-    State(state): State<AppState>,
-    headers: HeaderMap,
-    Path((owner, repo, id)): Path<(String, String, Uuid)>,
-) -> AppResult<Response> {
-    let (repository, _o, viewer, access) =
-        load_repo_context(&state, &owner, &repo, &headers).await?;
-    let _user = viewer.ok_or_else(AppError::unauthorized)?;
-    if !access.can_admin() {
-        return Err(AppError::forbidden());
-    }
-    let _ = queries::get_milestone(&state.pool, repository.id, id)
-        .await?
-        .ok_or_else(AppError::not_found)?;
-    queries::delete_milestone(&state.pool, id).await?;
-    Ok(redirect_see_other(&format!("/{owner}/{repo}/milestones")))
-}
src/web/templates.rs +6 −237
diff --git a/src/web/templates.rs b/src/web/templates.rs
index f62af84..15b504a 100644
--- a/src/web/templates.rs
+++ b/src/web/templates.rs
@@ -1,9 +1,8 @@
 use crate::og::SocialMeta;
 use crate::db::models::{
-    Access, BranchRule, CommitDay, GpgKey, Issue, Label, Milestone, Notification, PullRequest,
-    Release, RepoMirror, Repository, SshKey, User, UserEmail,
+    Access, BranchRule, CommitDay, GpgKey, Issue, PullRequest, Release, Repository, SshKey, User,
+    UserEmail,
 };
-use crate::db::DeployKey;
 use askama::Template;
 use askama_web::WebTemplate;
 use chrono::{DateTime, Utc};
@@ -41,34 +40,6 @@ pub struct CommentView {
     pub reactions: Vec<ReactionView>,
 }
 
-pub struct PullReviewView {
-    pub id: Uuid,
-    pub reviewer: User,
-    pub avatar_url: String,
-    pub state: String,
-    pub body: String,
-    pub body_html: String,
-    pub created_at: DateTime<Utc>,
-}
-
-impl PullReviewView {
-    pub fn state_label(&self) -> &'static str {
-        match self.state.as_str() {
-            "approved" => "approved",
-            "changes_requested" => "changes requested",
-            _ => "commented",
-        }
-    }
-
-    pub fn state_class(&self) -> &'static str {
-        match self.state.as_str() {
-            "approved" => "kg-badge--approved",
-            "changes_requested" => "kg-badge--changes",
-            _ => "kg-badge--commented",
-        }
-    }
-}
-
 pub struct DiffFileView {
     pub path: String,
     pub anchor: String,
@@ -84,10 +55,7 @@ pub struct TreeEntryView {
     pub name: String,
     pub path: String,
     pub is_dir: bool,
-    /// Last commit that touched this path (summary).
-    pub commit_message: String,
-    /// Formatted timestamp for that commit.
-    pub commit_time: String,
+    pub mode: String,
 }
 
 pub struct CommitView {
@@ -96,11 +64,7 @@ pub struct CommitView {
     pub message: String,
     pub author: String,
     pub email: String,
-    /// Kitgit username when the commit email matches a known account.
-    pub author_username: Option<String>,
     pub time: i64,
-    /// True when a signature blob was present on the commit (SSH or GPG).
-    pub signed: bool,
     pub verified: bool,
     pub verify_kind: String,
     pub verify_fingerprint: String,
@@ -108,73 +72,12 @@ pub struct CommitView {
     pub verified_at: String,
 }
 
-impl CommitView {
-    pub fn verify_kind_label(&self) -> &'static str {
-        match self.verify_kind.as_str() {
-            "gpg" => "GPG",
-            "ssh" => "SSH",
-            _ => "Unknown",
-        }
-    }
-
-    pub fn time_display(&self) -> String {
-        format_unix_time(self.time)
-    }
-}
-
-/// Human-readable UTC date/time from a Unix timestamp (seconds).
-pub fn format_unix_time(secs: i64) -> String {
-    chrono::DateTime::from_timestamp(secs, 0)
-        .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
-        .unwrap_or_default()
-}
-
 pub struct CollaboratorView {
     pub user: User,
     pub role: String,
     pub avatar_url: String,
 }
 
-pub struct WebhookView {
-    pub id: Uuid,
-    pub url: String,
-    pub has_secret: bool,
-    pub events_label: String,
-    pub active: bool,
-    pub created_at: String,
-}
-
-pub struct WebhookDeliveryView {
-    pub event: String,
-    pub action: String,
-    pub success: bool,
-    pub status_label: String,
-    pub webhook_url: String,
-    pub created_at: String,
-}
-
-pub struct LabelOption {
-    pub label: Label,
-    pub selected: bool,
-}
-
-pub struct MilestoneOption {
-    pub milestone: Milestone,
-    pub selected: bool,
-}
-
-pub struct IssueListItem {
-    pub issue: Issue,
-    pub labels: Vec<Label>,
-    pub milestone: Option<Milestone>,
-}
-
-pub struct PullListItem {
-    pub pull: PullRequest,
-    pub labels: Vec<Label>,
-    pub milestone: Option<Milestone>,
-}
-
 pub struct LanguageStatView {
     pub name: String,
     pub percent: f64,
@@ -206,7 +109,7 @@ pub struct ReleaseAssetView {
     pub content_type: String,
 }
 
-// ── page templates ────────────────────────────────────────────────────────────
+// ── page templates ───────────────────────────────────────────────────────────
 
 #[derive(Template, WebTemplate)]
 #[template(path = "home.html")]
@@ -305,20 +208,10 @@ pub struct AccountSettingsTemplate {
     pub emails: Vec<UserEmail>,
     pub sessions: Vec<SessionView>,
     pub current_session_id: Option<Uuid>,
-    pub audit_entries: Vec<AuditEntryView>,
     pub error: Option<String>,
     pub message: Option<String>,
 }
 
-pub struct AuditEntryView {
-    pub action: String,
-    pub action_label: String,
-    pub ip: String,
-    pub user_agent: String,
-    pub metadata: String,
-    pub created_at: DateTime<Utc>,
-}
-
 #[derive(Template, WebTemplate)]
 #[template(path = "mfa_challenge.html")]
 pub struct MfaChallengeTemplate {
@@ -390,7 +283,6 @@ pub struct RepoHomeTemplate {
     pub languages: Vec<LanguageStatView>,
     pub empty: bool,
     pub latest_commit: Option<CommitView>,
-    pub commit_count: usize,
     pub starred: bool,
     pub watching: bool,
     pub forked_from: Option<(String, String)>,
@@ -411,7 +303,6 @@ pub struct RepoTreeTemplate {
     pub entries: Vec<TreeEntryView>,
     pub readme_html: Option<String>,
     pub latest_commit: Option<CommitView>,
-    pub commit_count: usize,
     pub clone_http: String,
     pub clone_ssh: String,
 }
@@ -435,50 +326,6 @@ pub struct RepoBlobTemplate {
     pub clone_ssh: String,
 }
 
-pub struct BlameLineView {
-    pub line_no: usize,
-    pub content: String,
-    pub commit_id: String,
-    pub short_id: String,
-    pub author: String,
-    pub time_display: String,
-    pub summary: String,
-    pub hunk_start: bool,
-}
-
-#[derive(Template, WebTemplate)]
-#[template(path = "repo_blame.html")]
-pub struct RepoBlameTemplate {
-    pub viewer: Option<User>,
-    pub owner: User,
-    pub repo: Repository,
-    pub access: Access,
-    pub branches: Vec<String>,
-    pub branch: String,
-    pub path: String,
-    pub breadcrumbs: Vec<(String, String)>,
-    pub lines: Vec<BlameLineView>,
-    pub binary: bool,
-    pub clone_http: String,
-    pub clone_ssh: String,
-}
-
-#[derive(Template, WebTemplate)]
-#[template(path = "repo_history.html")]
-pub struct RepoHistoryTemplate {
-    pub viewer: Option<User>,
-    pub owner: User,
-    pub repo: Repository,
-    pub access: Access,
-    pub branches: Vec<String>,
-    pub branch: String,
-    pub path: String,
-    pub breadcrumbs: Vec<(String, String)>,
-    pub commits: Vec<CommitView>,
-    pub clone_http: String,
-    pub clone_ssh: String,
-}
-
 #[derive(Template, WebTemplate)]
 #[template(path = "repo_commits.html")]
 pub struct RepoCommitsTemplate {
@@ -503,9 +350,6 @@ pub struct RepoCommitTemplate {
     pub commit: CommitView,
     pub message_html: String,
     pub diff_html: String,
-    pub show_full: bool,
-    pub truncated: bool,
-    pub total_lines: usize,
     pub clone_http: String,
     pub clone_ssh: String,
 }
@@ -519,9 +363,6 @@ pub struct RepoDiffTemplate {
     pub access: Access,
     pub commit: CommitView,
     pub diff_html: String,
-    pub show_full: bool,
-    pub truncated: bool,
-    pub total_lines: usize,
     pub clone_http: String,
     pub clone_ssh: String,
 }
@@ -547,12 +388,8 @@ pub struct IssuesListTemplate {
     pub owner: User,
     pub repo: Repository,
     pub access: Access,
-    pub issues: Vec<IssueListItem>,
-    pub labels: Vec<Label>,
-    pub milestones: Vec<Milestone>,
+    pub issues: Vec<Issue>,
     pub state_filter: String,
-    pub label_filter: Option<Uuid>,
-    pub milestone_filter: Option<Uuid>,
     pub clone_http: String,
     pub clone_ssh: String,
 }
@@ -564,8 +401,6 @@ pub struct IssueNewTemplate {
     pub owner: User,
     pub repo: Repository,
     pub access: Access,
-    pub labels: Vec<Label>,
-    pub milestones: Vec<Milestone>,
     pub error: Option<String>,
     pub clone_http: String,
     pub clone_ssh: String,
@@ -583,11 +418,6 @@ pub struct IssueViewTemplate {
     pub author_avatar: String,
     pub body_html: String,
     pub comments: Vec<CommentView>,
-    pub labels: Vec<Label>,
-    pub label_options: Vec<LabelOption>,
-    pub milestone: Option<Milestone>,
-    pub milestone_options: Vec<MilestoneOption>,
-    pub can_triage: bool,
     pub clone_http: String,
     pub clone_ssh: String,
 }
@@ -599,12 +429,8 @@ pub struct PullsListTemplate {
     pub owner: User,
     pub repo: Repository,
     pub access: Access,
-    pub pulls: Vec<PullListItem>,
-    pub labels: Vec<Label>,
-    pub milestones: Vec<Milestone>,
+    pub pulls: Vec<PullRequest>,
     pub state_filter: String,
-    pub label_filter: Option<Uuid>,
-    pub milestone_filter: Option<Uuid>,
     pub clone_http: String,
     pub clone_ssh: String,
 }
@@ -617,8 +443,6 @@ pub struct PullNewTemplate {
     pub repo: Repository,
     pub access: Access,
     pub branches: Vec<String>,
-    pub labels: Vec<Label>,
-    pub milestones: Vec<Milestone>,
     pub error: Option<String>,
     pub clone_http: String,
     pub clone_ssh: String,
@@ -637,47 +461,12 @@ pub struct PullViewTemplate {
     pub author_avatar: String,
     pub body_html: String,
     pub comments: Vec<CommentView>,
-    pub reviews: Vec<PullReviewView>,
     pub commits: Vec<CommitView>,
     pub diff_files: Vec<DiffFileView>,
     pub tab: String,
-    pub show_full: bool,
     pub conversation_count: usize,
     pub can_merge: bool,
     pub merge_styles: Vec<String>,
-    pub labels: Vec<Label>,
-    pub label_options: Vec<LabelOption>,
-    pub milestone: Option<Milestone>,
-    pub milestone_options: Vec<MilestoneOption>,
-    pub can_triage: bool,
-    pub is_author: bool,
-    pub clone_http: String,
-    pub clone_ssh: String,
-}
-
-#[derive(Template, WebTemplate)]
-#[template(path = "labels_list.html")]
-pub struct LabelsListTemplate {
-    pub viewer: Option<User>,
-    pub owner: User,
-    pub repo: Repository,
-    pub access: Access,
-    pub labels: Vec<Label>,
-    pub error: Option<String>,
-    pub clone_http: String,
-    pub clone_ssh: String,
-}
-
-#[derive(Template, WebTemplate)]
-#[template(path = "milestones_list.html")]
-pub struct MilestonesListTemplate {
-    pub viewer: Option<User>,
-    pub owner: User,
-    pub repo: Repository,
-    pub access: Access,
-    pub milestones: Vec<Milestone>,
-    pub state_filter: String,
-    pub error: Option<String>,
     pub clone_http: String,
     pub clone_ssh: String,
 }
@@ -756,10 +545,6 @@ pub struct RepoSettingsTemplate {
     pub collaborators: Vec<CollaboratorView>,
     pub branches: Vec<String>,
     pub branch_rules: Vec<BranchRule>,
-    pub mirror: Option<RepoMirror>,
-    pub deploy_keys: Vec<DeployKey>,
-    pub webhooks: Vec<WebhookView>,
-    pub webhook_deliveries: Vec<WebhookDeliveryView>,
     pub clone_http: String,
     pub clone_ssh: String,
     pub is_site_admin: bool,
@@ -789,14 +574,6 @@ pub struct AdminTemplate {
     pub flash: Option<String>,
 }
 
-#[derive(Template, WebTemplate)]
-#[template(path = "admin_user_audit.html")]
-pub struct AdminUserAuditTemplate {
-    pub viewer: Option<User>,
-    pub user: User,
-    pub audit_entries: Vec<AuditEntryView>,
-}
-
 pub struct AdminInviteView {
     pub id: Uuid,
     pub code: String,
@@ -820,11 +597,3 @@ pub struct AdminStatsView {
     pub active_invites: i64,
     pub disk_label: String,
 }
-
-#[derive(Template, WebTemplate)]
-#[template(path = "notifications.html")]
-pub struct NotificationsTemplate {
-    pub viewer: Option<User>,
-    pub notifications: Vec<Notification>,
-    pub unread_count: i64,
-}
src/webhooks.rs +0 −189
diff --git a/src/webhooks.rs b/src/webhooks.rs
deleted file mode 100644
index f2af0e6..0000000
--- a/src/webhooks.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-//! Repository webhook delivery (JSON POST + optional HMAC-SHA256).
-
-use crate::db::models::{Repository, User, Webhook};
-use crate::db::queries;
-use anyhow::Result;
-use hmac::{Hmac, Mac};
-use serde_json::{json, Value};
-use sha2::Sha256;
-use sqlx::PgPool;
-use std::time::Instant;
-use uuid::Uuid;
-
-type HmacSha256 = Hmac<Sha256>;
-
-pub const EVENT_PUSH: &str = "push";
-pub const EVENT_ISSUES: &str = "issues";
-pub const EVENT_PULL_REQUEST: &str = "pull_request";
-pub const EVENT_RELEASE: &str = "release";
-
-pub const ALL_EVENTS: &[&str] = &[EVENT_PUSH, EVENT_ISSUES, EVENT_PULL_REQUEST, EVENT_RELEASE];
-
-pub fn normalize_events(raw: &[String]) -> Vec<String> {
-    let mut out = Vec::new();
-    for e in raw {
-        let e = e.trim();
-        if ALL_EVENTS.contains(&e) && !out.iter().any(|x| x == e) {
-            out.push(e.to_string());
-        }
-    }
-    out
-}
-
-fn sign_body(secret: &str, body: &[u8]) -> Option<String> {
-    if secret.is_empty() {
-        return None;
-    }
-    let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).ok()?;
-    mac.update(body);
-    Some(format!(
-        "sha256={}",
-        hex::encode(mac.finalize().into_bytes())
-    ))
-}
-
-fn build_envelope(
-    event: &str,
-    action: &str,
-    delivery_id: Uuid,
-    repo: &Repository,
-    owner: &str,
-    sender: Option<&User>,
-    payload: Value,
-) -> Value {
-    json!({
-        "event": event,
-        "action": action,
-        "delivery_id": delivery_id.to_string(),
-        "repository": {
-            "id": repo.id,
-            "name": repo.name,
-            "owner": owner,
-            "full_name": format!("{owner}/{}", repo.name),
-            "default_branch": repo.default_branch,
-            "visibility": repo.visibility,
-            "private": repo.visibility == "private",
-        },
-        "sender": sender.map(|u| json!({
-            "id": u.id,
-            "username": u.username,
-            "display_name": u.display_name,
-        })),
-        "payload": payload,
-    })
-}
-
-/// Fire matching active webhooks for a repo. Failures are logged; callers should ignore errors.
-pub async fn dispatch(
-    pool: &PgPool,
-    event: &str,
-    action: &str,
-    repo: &Repository,
-    owner: &str,
-    sender: Option<&User>,
-    payload: Value,
-) -> Result<()> {
-    let hooks = queries::list_active_webhooks_for_event(pool, repo.id, event).await?;
-    if hooks.is_empty() {
-        return Ok(());
-    }
-    let delivery_id = Uuid::new_v4();
-    let body_val = build_envelope(event, action, delivery_id, repo, owner, sender, payload);
-    let body = serde_json::to_vec(&body_val)?;
-    let client = reqwest::Client::builder()
-        .timeout(std::time::Duration::from_secs(10))
-        .redirect(reqwest::redirect::Policy::none())
-        .user_agent("kitgit-webhooks/0.1")
-        .build()?;
-
-    for hook in hooks {
-        deliver_one(pool, &client, &hook, event, action, delivery_id, &body).await;
-    }
-    Ok(())
-}
-
-/// Spawn a background dispatch so request handlers stay fast.
-pub fn spawn_dispatch(
-    pool: PgPool,
-    event: &'static str,
-    action: String,
-    repo: Repository,
-    owner: String,
-    sender: Option<User>,
-    payload: Value,
-) {
-    tokio::spawn(async move {
-        if let Err(e) = dispatch(
-            &pool,
-            event,
-            &action,
-            &repo,
-            &owner,
-            sender.as_ref(),
-            payload,
-        )
-        .await
-        {
-            tracing::warn!("webhook dispatch failed: {e:#}");
-        }
-    });
-}
-
-async fn deliver_one(
-    pool: &PgPool,
-    client: &reqwest::Client,
-    hook: &Webhook,
-    event: &str,
-    action: &str,
-    delivery_id: Uuid,
-    body: &[u8],
-) {
-    let started = Instant::now();
-    let mut req = client
-        .post(&hook.url)
-        .header("Content-Type", "application/json")
-        .header("X-Kitgit-Event", event)
-        .header("X-Kitgit-Delivery", delivery_id.to_string())
-        .header("X-Kitgit-Action", action)
-        .body(body.to_vec());
-
-    if let Some(sig) = sign_body(&hook.secret, body) {
-        req = req.header("X-Hub-Signature-256", sig);
-    }
-
-    let (success, status_code, error) = match req.send().await {
-        Ok(resp) => {
-            let code = resp.status().as_u16() as i32;
-            let ok = resp.status().is_success();
-            let err = if ok {
-                None
-            } else {
-                let text = resp.text().await.unwrap_or_default();
-                let truncated: String = text.chars().take(500).collect();
-                Some(if truncated.is_empty() {
-                    format!("HTTP {code}")
-                } else {
-                    format!("HTTP {code}: {truncated}")
-                })
-            };
-            (ok, Some(code), err)
-        }
-        Err(e) => (false, None, Some(e.to_string())),
-    };
-
-    let duration_ms = started.elapsed().as_millis().min(i32::MAX as u128) as i32;
-    if let Err(e) = queries::record_webhook_delivery(
-        pool,
-        hook.id,
-        event,
-        action,
-        success,
-        status_code,
-        error.as_deref(),
-        duration_ms,
-    )
-    .await
-    {
-        tracing::warn!("failed to record webhook delivery: {e:#}");
-    }
-}
\ No newline at end of file
static/brand.css +49 −525
diff --git a/static/brand.css b/static/brand.css
index 8f082ff..db813d2 100644
--- a/static/brand.css
+++ b/static/brand.css
@@ -1,8 +1,7 @@
 /* kitgit — brand theme
    ink on paper. no chrome. */
 
-:root,
-[data-theme="light"] {
+:root {
   --kg-bg: #ffffff;
   --kg-bg-raised: #f7f7f7;
   --kg-fg: #0a0a0a;
@@ -35,7 +34,6 @@
   color-scheme: light;
 }
 
-[data-theme="dark"],
 html.kg-dark {
   --kg-bg: #0a0a0a;
   --kg-bg-raised: #141414;
@@ -45,7 +43,6 @@ html.kg-dark {
   --kg-line: #262626;
   --kg-line-strong: #f5f5f5;
   --kg-link: #f5f5f5;
-  --kg-focus: #f5f5f5;
   --kg-danger: #ff4d5a;
   --kg-danger-bg: #2a1214;
   --kg-invert-bg: #f5f5f5;
@@ -54,26 +51,6 @@ html.kg-dark {
   color-scheme: dark;
 }
 
-@media (prefers-color-scheme: dark) {
-  [data-theme="system"] {
-    --kg-bg: #0a0a0a;
-    --kg-bg-raised: #141414;
-    --kg-fg: #f5f5f5;
-    --kg-muted: #8a8a8a;
-    --kg-faint: #5c5c5c;
-    --kg-line: #262626;
-    --kg-line-strong: #f5f5f5;
-    --kg-link: #f5f5f5;
-    --kg-focus: #f5f5f5;
-    --kg-danger: #ff4d5a;
-    --kg-danger-bg: #2a1214;
-    --kg-invert-bg: #f5f5f5;
-    --kg-invert-fg: #0a0a0a;
-    --kg-invert-muted: #525252;
-    color-scheme: dark;
-  }
-}
-
 *,
 *::before,
 *::after {
@@ -193,21 +170,12 @@ a:hover {
 }
 
 .kg-footer {
-  display: flex;
-  justify-content: space-between;
-  align-items: baseline;
-  gap: var(--kg-space-4);
   padding: var(--kg-space-4) 0 var(--kg-space-6);
   color: var(--kg-faint);
   font-size: 0.75rem;
   letter-spacing: 0.06em;
 }
 
-.kg-footer__timing {
-  letter-spacing: 0;
-  font-variant-numeric: tabular-nums;
-}
-
 .kg-display {
   font-weight: 700;
   font-size: clamp(1.5rem, 3vw, 2rem);
@@ -288,6 +256,12 @@ a:hover {
   background: var(--kg-danger-bg);
 }
 
+.kg-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: var(--kg-space-2);
+}
+
 .kg-list {
   list-style: none;
   margin: 0;
@@ -539,145 +513,32 @@ a:hover {
   min-width: 0;
 }
 
-.kg-toolbar {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: var(--kg-space-3);
-  margin: var(--kg-space-3) 0 var(--kg-space-2);
-  min-width: 0;
-}
-
-.kg-toolbar__branch {
-  flex-shrink: 0;
-}
-
-.kg-toolbar__actions {
-  display: flex;
-  align-items: center;
-  gap: var(--kg-space-2);
-  flex-wrap: nowrap;
-  flex-shrink: 0;
-  margin-left: auto;
-}
-
-.kg-latest {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  gap: var(--kg-space-3);
-  flex-wrap: nowrap;
-  border: 1px solid var(--kg-line);
-  padding: 0.55rem 0.75rem;
-  margin: 0 0 var(--kg-space-3);
-  background: var(--kg-bg-raised);
-  min-width: 0;
-}
-
-.kg-latest__main {
-  display: flex;
-  align-items: center;
-  gap: 0.5rem;
-  min-width: 0;
-  flex: 1 1 auto;
-  overflow: hidden;
-}
-
-.kg-latest__author {
-  flex-shrink: 0;
-  font-weight: 600;
-  color: var(--kg-fg);
-  text-decoration: none;
-  white-space: nowrap;
-}
-
-a.kg-latest__author:hover {
-  text-decoration: underline;
-  text-underline-offset: 0.15em;
-}
-
-.kg-latest__msg {
-  color: var(--kg-fg);
-  text-decoration: none;
-  font-size: 0.875rem;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  min-width: 0;
-  flex: 1 1 auto;
-}
-
-.kg-latest__msg:hover {
-  text-decoration: underline;
-  text-underline-offset: 0.15em;
-}
-
-.kg-latest__meta {
-  display: flex;
-  align-items: center;
-  gap: 0.65rem;
-  flex-shrink: 0;
-  white-space: nowrap;
-}
-
-.kg-latest__sha {
-  color: var(--kg-muted);
-  text-decoration: none;
-  font-size: 0.8125rem;
-}
-
-.kg-latest__sha:hover {
-  text-decoration: underline;
-  text-underline-offset: 0.15em;
-}
-
-.kg-latest__sha code {
-  font-size: inherit;
-}
-
-.kg-latest__time {
-  white-space: nowrap;
-}
-
-.kg-latest__count {
-  text-decoration: none;
-  white-space: nowrap;
-}
-
-.kg-latest__count:hover {
-  text-decoration: underline;
-  text-underline-offset: 0.15em;
-}
-
 .kg-branchbar {
   display: flex;
   align-items: center;
   justify-content: space-between;
   gap: var(--kg-space-3);
-  flex-wrap: nowrap;
+  flex-wrap: wrap;
   border: 1px solid var(--kg-line);
   padding: 0.55rem 0.75rem;
   margin: var(--kg-space-3) 0;
   background: var(--kg-bg-raised);
-  min-width: 0;
 }
 
 .kg-branchbar__left {
   display: flex;
   align-items: center;
   gap: var(--kg-space-3);
-  flex-wrap: nowrap;
+  flex-wrap: wrap;
   min-width: 0;
   flex: 1 1 auto;
-  overflow: hidden;
 }
 
 .kg-branchbar__actions {
   display: flex;
   align-items: center;
   gap: var(--kg-space-2);
-  flex-wrap: nowrap;
-  flex-shrink: 0;
+  flex-wrap: wrap;
 }
 
 .kg-branchbar__branch {
@@ -687,16 +548,15 @@ a.kg-latest__author:hover {
   text-decoration: none;
   font-weight: 700;
   color: var(--kg-fg);
-  flex-shrink: 0;
 }
 
 .kg-branchbar__commit {
   display: flex;
   align-items: center;
-  gap: 0.5rem;
+  gap: var(--kg-space-2);
   min-width: 0;
-  flex: 1 1 auto;
-  overflow: hidden;
+  flex: 1;
+  justify-content: flex-end;
 }
 
 .kg-branchbar__sha {
@@ -716,8 +576,7 @@ a.kg-latest__author:hover {
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  min-width: 0;
-  flex: 1 1 auto;
+  max-width: 28rem;
 }
 
 .kg-branchbar__msg:hover,
@@ -788,102 +647,6 @@ a.kg-latest__author:hover {
   align-items: center;
 }
 
-.kg-blame {
-  border: 1px solid var(--kg-line);
-  margin-top: var(--kg-space-3);
-  overflow-x: auto;
-  font-family: var(--kg-font);
-  font-size: 0.8125rem;
-  line-height: 1.5;
-}
-
-.kg-blame__head,
-.kg-blame__row {
-  display: grid;
-  grid-template-columns: minmax(12rem, 16rem) 3.25rem minmax(0, 1fr);
-  align-items: stretch;
-  border-bottom: 1px solid var(--kg-line);
-}
-
-.kg-blame__head {
-  background: var(--kg-bg);
-  color: var(--kg-faint);
-  font-size: 0.75rem;
-  text-transform: uppercase;
-  letter-spacing: 0.04em;
-}
-
-.kg-blame__head > span {
-  padding: 0.45rem 0.65rem;
-}
-
-.kg-blame__row:last-child {
-  border-bottom: 0;
-}
-
-.kg-blame__row--hunk {
-  border-top: 1px solid var(--kg-line);
-}
-
-.kg-blame__row--hunk:first-of-type {
-  border-top: 0;
-}
-
-.kg-blame__meta {
-  display: flex;
-  flex-wrap: wrap;
-  align-items: baseline;
-  gap: 0.35rem 0.55rem;
-  padding: 0.15rem 0.65rem;
-  background: var(--kg-bg);
-  border-right: 1px solid var(--kg-line);
-  min-width: 0;
-  color: var(--kg-muted, var(--kg-faint));
-}
-
-.kg-blame__sha {
-  font-family: inherit;
-  font-variant-numeric: tabular-nums;
-}
-
-.kg-blame__author {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  max-width: 7rem;
-  color: var(--kg-fg);
-}
-
-.kg-blame__date {
-  font-variant-numeric: tabular-nums;
-  color: var(--kg-faint);
-}
-
-.kg-blame__num {
-  margin: 0;
-  padding: 0.15rem 0.55rem;
-  text-align: right;
-  color: var(--kg-faint);
-  user-select: none;
-  font-variant-numeric: tabular-nums;
-  border-right: 1px solid var(--kg-line);
-  background: var(--kg-bg);
-}
-
-.kg-blame__code {
-  margin: 0;
-  padding: 0.15rem 0.75rem;
-  min-width: 0;
-  overflow-x: auto;
-  white-space: pre;
-  font-family: inherit;
-  font-size: inherit;
-  line-height: inherit;
-  background: transparent;
-  border: 0;
-  color: inherit;
-}
-
 .kg-danger-zone {
   margin-top: var(--kg-space-5);
   padding: var(--kg-space-3);
@@ -903,30 +666,15 @@ a.kg-latest__author:hover {
 
 .kg-danger-row {
   display: flex;
-  flex-wrap: nowrap;
-  align-items: center;
+  flex-wrap: wrap;
+  align-items: flex-end;
   gap: var(--kg-space-2);
 }
 
-.kg-danger-row label,
-.kg-danger-confirm {
-  flex: 1 1 auto;
+.kg-danger-row label {
+  flex: 1 1 12rem;
   margin: 0;
-  display: flex;
-  flex-direction: row;
-  flex-wrap: nowrap;
-  align-items: center;
-  gap: 0.5rem;
-  white-space: nowrap;
-}
-
-.kg-danger-confirm input {
-  flex: 1 1 10rem;
-  min-width: 8rem;
-}
-
-.kg-danger-row > button {
-  flex-shrink: 0;
+  gap: 0.25rem;
 }
 
 .kg-settings-section {
@@ -941,7 +689,6 @@ a.kg-latest__author:hover {
 
 .kg-social {
   display: flex;
-  flex-direction: row;
   flex-wrap: wrap;
   gap: var(--kg-space-2);
   margin: 0;
@@ -952,29 +699,6 @@ a.kg-latest__author:hover {
 
 .kg-social form {
   margin: 0;
-  display: inline-flex;
-}
-
-.kg-branchbar__count {
-  display: inline-flex;
-  align-items: center;
-  gap: 0.35em;
-  text-decoration: none;
-  flex-shrink: 0;
-  white-space: nowrap;
-}
-
-.kg-branchbar__count:hover {
-  text-decoration: underline;
-  text-underline-offset: 0.15em;
-}
-
-.kg-actions {
-  display: flex;
-  flex-direction: row;
-  flex-wrap: wrap;
-  gap: var(--kg-space-2);
-  align-items: center;
 }
 
 .kg-reactions {
@@ -1499,38 +1223,12 @@ a.kg-latest__author:hover {
 
 .kg-tree li {
   display: grid;
-  grid-template-columns: 1.5rem minmax(5rem, 12rem) minmax(0, 1fr) 9rem 2rem;
+  grid-template-columns: 1.5rem 1fr auto auto;
   align-items: center;
   gap: var(--kg-space-2);
   padding: 0.35rem var(--kg-space-3);
   border-bottom: 1px solid var(--kg-line);
   font-size: 0.875rem;
-  min-width: 0;
-}
-
-.kg-tree li > a {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  min-width: 0;
-}
-
-.kg-tree__commit {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  min-width: 0;
-}
-
-.kg-tree__time {
-  text-align: right;
-  white-space: nowrap;
-  flex-shrink: 0;
-}
-
-.kg-tree__dl {
-  justify-self: end;
-  flex-shrink: 0;
 }
 
 .kg-tree li:last-child {
@@ -1643,67 +1341,35 @@ a.kg-latest__author:hover {
 .kg-codeview .entity.other.attribute-name { color: #8a4b08; }
 .kg-codeview .punctuation { color: #3d3d3d; }
 
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .comment,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .punctuation.definition.comment { color: #8a8a8a; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .string,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .string.quoted,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .constant.character.escape { color: #5ec49a; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .constant.numeric,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .constant.language,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .constant.character { color: #e0a35a; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .keyword,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .keyword.control,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .keyword.operator,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .storage,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .storage.type,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .storage.modifier { color: #f5f5f5; font-weight: 700; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .entity.name.function,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .support.function { color: #7eb0f0; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .entity.name.class,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .entity.name.struct,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .entity.name.type,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .entity.name.namespace,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .support.type,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .support.class { color: #c9a0ff; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .variable,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .variable.parameter,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .variable.other { color: #f5f5f5; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .meta.annotation,
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .entity.name.tag { color: #ff8a8a; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .entity.other.attribute-name { color: #e0a35a; }
-:is([data-theme="dark"], html.kg-dark) .kg-codeview .punctuation { color: #b0b0b0; }
-
-@media (prefers-color-scheme: dark) {
-  [data-theme="system"] .kg-codeview .comment,
-  [data-theme="system"] .kg-codeview .punctuation.definition.comment { color: #8a8a8a; }
-  [data-theme="system"] .kg-codeview .string,
-  [data-theme="system"] .kg-codeview .string.quoted,
-  [data-theme="system"] .kg-codeview .constant.character.escape { color: #5ec49a; }
-  [data-theme="system"] .kg-codeview .constant.numeric,
-  [data-theme="system"] .kg-codeview .constant.language,
-  [data-theme="system"] .kg-codeview .constant.character { color: #e0a35a; }
-  [data-theme="system"] .kg-codeview .keyword,
-  [data-theme="system"] .kg-codeview .keyword.control,
-  [data-theme="system"] .kg-codeview .keyword.operator,
-  [data-theme="system"] .kg-codeview .storage,
-  [data-theme="system"] .kg-codeview .storage.type,
-  [data-theme="system"] .kg-codeview .storage.modifier { color: #f5f5f5; font-weight: 700; }
-  [data-theme="system"] .kg-codeview .entity.name.function,
-  [data-theme="system"] .kg-codeview .support.function { color: #7eb0f0; }
-  [data-theme="system"] .kg-codeview .entity.name.class,
-  [data-theme="system"] .kg-codeview .entity.name.struct,
-  [data-theme="system"] .kg-codeview .entity.name.type,
-  [data-theme="system"] .kg-codeview .entity.name.namespace,
-  [data-theme="system"] .kg-codeview .support.type,
-  [data-theme="system"] .kg-codeview .support.class { color: #c9a0ff; }
-  [data-theme="system"] .kg-codeview .variable,
-  [data-theme="system"] .kg-codeview .variable.parameter,
-  [data-theme="system"] .kg-codeview .variable.other { color: #f5f5f5; }
-  [data-theme="system"] .kg-codeview .meta.annotation,
-  [data-theme="system"] .kg-codeview .entity.name.tag { color: #ff8a8a; }
-  [data-theme="system"] .kg-codeview .entity.other.attribute-name { color: #e0a35a; }
-  [data-theme="system"] .kg-codeview .punctuation { color: #b0b0b0; }
-}
+html.kg-dark .kg-codeview .comment,
+html.kg-dark .kg-codeview .punctuation.definition.comment { color: #8a8a8a; }
+html.kg-dark .kg-codeview .string,
+html.kg-dark .kg-codeview .string.quoted,
+html.kg-dark .kg-codeview .constant.character.escape { color: #5ec49a; }
+html.kg-dark .kg-codeview .constant.numeric,
+html.kg-dark .kg-codeview .constant.language,
+html.kg-dark .kg-codeview .constant.character { color: #e0a35a; }
+html.kg-dark .kg-codeview .keyword,
+html.kg-dark .kg-codeview .keyword.control,
+html.kg-dark .kg-codeview .keyword.operator,
+html.kg-dark .kg-codeview .storage,
+html.kg-dark .kg-codeview .storage.type,
+html.kg-dark .kg-codeview .storage.modifier { color: #f5f5f5; font-weight: 700; }
+html.kg-dark .kg-codeview .entity.name.function,
+html.kg-dark .kg-codeview .support.function { color: #7eb0f0; }
+html.kg-dark .kg-codeview .entity.name.class,
+html.kg-dark .kg-codeview .entity.name.struct,
+html.kg-dark .kg-codeview .entity.name.type,
+html.kg-dark .kg-codeview .entity.name.namespace,
+html.kg-dark .kg-codeview .support.type,
+html.kg-dark .kg-codeview .support.class { color: #c9a0ff; }
+html.kg-dark .kg-codeview .variable,
+html.kg-dark .kg-codeview .variable.parameter,
+html.kg-dark .kg-codeview .variable.other { color: #f5f5f5; }
+html.kg-dark .kg-codeview .meta.annotation,
+html.kg-dark .kg-codeview .entity.name.tag { color: #ff8a8a; }
+html.kg-dark .kg-codeview .entity.other.attribute-name { color: #e0a35a; }
+html.kg-dark .kg-codeview .punctuation { color: #b0b0b0; }
 
 .kg-readme {
   border: 1px solid var(--kg-line);
@@ -1816,57 +1482,6 @@ a.kg-latest__author:hover {
   color: var(--kg-muted);
 }
 
-.kg-badge--approved {
-  color: #1a7f37;
-  border-color: #1a7f37;
-  background: color-mix(in srgb, #1a7f37 12%, transparent);
-}
-
-.kg-badge--changes {
-  color: #cf222e;
-  border-color: #cf222e;
-  background: color-mix(in srgb, #cf222e 12%, transparent);
-}
-
-.kg-badge--commented {
-  color: var(--kg-muted);
-  border-color: var(--kg-line-strong);
-  background: transparent;
-}
-
-.kg-review-state {
-  border: 1px solid var(--kg-line);
-  border-radius: var(--kg-radius);
-  padding: var(--kg-space-3);
-  margin: 0 0 var(--kg-space-3);
-  display: flex;
-  flex-wrap: wrap;
-  align-items: center;
-  gap: var(--kg-space-3);
-}
-
-.kg-review-state legend {
-  padding: 0 var(--kg-space-1);
-  font-size: 0.8125rem;
-  color: var(--kg-muted);
-  text-transform: lowercase;
-  width: 100%;
-  float: left;
-  margin-bottom: var(--kg-space-1);
-}
-
-.kg-review-state .row {
-  display: inline-flex;
-  align-items: center;
-  gap: var(--kg-space-2);
-  margin: 0;
-}
-
-.kg-diff__truncated .kg-btn {
-  margin-left: var(--kg-space-2);
-  vertical-align: middle;
-}
-
 .kg-badge--verified {
   color: #1a7f37;
   border-color: #1a7f37;
@@ -1877,20 +1492,6 @@ a.kg-latest__author:hover {
   cursor: pointer;
 }
 
-.kg-badge--unverified {
-  color: var(--kg-muted);
-  border-color: var(--kg-line-strong);
-  background: transparent;
-  text-transform: none;
-  letter-spacing: 0.02em;
-  font-weight: 600;
-  cursor: default;
-}
-
-.kg-verify .kg-badge--unverified {
-  cursor: pointer;
-}
-
 .kg-verify {
   position: relative;
   display: inline-block;
@@ -1918,12 +1519,6 @@ a.kg-latest__author:hover {
   box-shadow: 0 8px 24px color-mix(in srgb, var(--kg-fg) 12%, transparent);
 }
 
-.kg-verify__row {
-  margin: 0 0 0.35rem;
-  font-size: 0.8125rem;
-  line-height: 1.4;
-}
-
 .kg-verify__fp {
   margin: 0 0 0.35rem;
   font-size: 0.8125rem;
@@ -1935,29 +1530,6 @@ a.kg-latest__author:hover {
   font-size: 0.75rem;
 }
 
-.kg-verify__panel a {
-  color: inherit;
-  text-decoration: underline;
-}
-
-.kg-signing-hint {
-  margin: 1rem 0 0;
-  padding: var(--kg-space-3);
-  border: 1px dashed var(--kg-line-strong);
-  background: color-mix(in srgb, var(--kg-fg) 3%, transparent);
-  font-size: 0.875rem;
-  line-height: 1.5;
-}
-
-.kg-signing-hint pre {
-  margin: 0.5rem 0 0;
-  padding: var(--kg-space-2);
-  overflow-x: auto;
-  font-size: 0.75rem;
-  background: var(--kg-bg);
-  border: 1px solid var(--kg-line);
-}
-
 .kg-key-row {
   align-items: flex-start;
 }
@@ -2005,51 +1577,3 @@ a.kg-latest__author:hover {
     animation: none !important;
   }
 }
-
-.kg-notif-count {
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-  min-width: 1.25rem;
-  height: 1.25rem;
-  padding: 0 0.35em;
-  margin-left: 0.25em;
-  border-radius: 999px;
-  background: var(--kg-invert-bg);
-  color: var(--kg-invert-fg);
-  font-size: 0.65rem;
-  font-weight: 700;
-  line-height: 1;
-  vertical-align: middle;
-}
-
-.kg-notif-list li {
-  align-items: flex-start;
-}
-
-.kg-notif-main {
-  display: flex;
-  flex-direction: column;
-  gap: 0.2rem;
-  min-width: 0;
-}
-
-.kg-notif-actions {
-  flex-shrink: 0;
-}
-
-.kg-notif--unread {
-  border-left: 2px solid var(--kg-fg);
-  padding-left: 0.65rem;
-  margin-left: -0.65rem;
-}
-
-.kg-label-chip { display:inline-block; padding:0.12em 0.45em; font-size:0.75rem; line-height:1.3; border:1px solid transparent; vertical-align:middle; text-transform:none; letter-spacing:0.02em; }
-.kg-label-row { display:flex; flex-wrap:wrap; gap:0.35rem; margin-bottom:0.5rem; }
-.kg-issue-layout { display:grid; grid-template-columns:minmax(0,1fr) 14rem; gap:1.5rem; align-items:start; }
-.kg-issue-sidebar { font-size:0.9rem; }
-.kg-sidebar-block { margin-bottom:1.25rem; padding-bottom:1rem; border-bottom:1px solid var(--kg-line); }
-.kg-filters { display:flex; flex-wrap:wrap; gap:0.75rem; align-items:end; margin:1rem 0; }
-.kg-fieldset { border:1px solid var(--kg-line); padding:0.75rem; margin:0.75rem 0; }
-@media (max-width:800px) { .kg-issue-layout { grid-template-columns:1fr; } }
-
static/icons/bell.svg +0 −1
diff --git a/static/icons/bell.svg b/static/icons/bell.svg
deleted file mode 100644
index 142388b..0000000
--- a/static/icons/bell.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>
templates/account_settings.html +7 −62
diff --git a/templates/account_settings.html b/templates/account_settings.html
index 2b8cbee..248b87f 100644
--- a/templates/account_settings.html
+++ b/templates/account_settings.html
@@ -20,28 +20,6 @@
   {% endif %}
 
   <div class="kg-settings-section" style="margin-top:1rem;border-top:0;padding-top:0">
-    <span class="kg-kicker">appearance</span>
-    <p class="kg-muted">Ink on paper by day, charcoal by night — or follow your system.</p>
-    <form class="kg-form" method="post" action="/settings/account/theme">
-      <label class="row">
-        <input type="radio" name="theme" value="light"{% if user.theme == "light" %} checked{% endif %} />
-        light
-      </label>
-      <label class="row">
-        <input type="radio" name="theme" value="dark"{% if user.theme == "dark" %} checked{% endif %} />
-        dark
-      </label>
-      <label class="row">
-        <input type="radio" name="theme" value="system"{% if user.theme != "light" && user.theme != "dark" %} checked{% endif %} />
-        system
-      </label>
-      <div class="kg-actions">
-        <button class="kg-btn" type="submit">save theme</button>
-      </div>
-    </form>
-  </div>
-
-  <div class="kg-settings-section">
     <span class="kg-kicker">username</span>
     <form class="kg-form" method="post" action="/settings/account/username">
       <label>
@@ -97,16 +75,11 @@
           {% if e.is_primary %}<span class="kg-badge">primary</span>{% endif %}
           {% if e.verified %}<span class="kg-badge">verified</span>{% endif %}
         </span>
-        <span class="kg-actions">
-          {% if !e.is_primary %}
-          <form method="post" action="/settings/account/emails/{{ e.id }}/primary">
-            <button class="kg-btn kg-btn--ghost" type="submit">make primary</button>
-          </form>
-          <form method="post" action="/settings/account/emails/{{ e.id }}/delete">
-            <button class="kg-btn kg-btn--danger-ghost" type="submit">remove</button>
-          </form>
-          {% endif %}
-        </span>
+        {% if !e.is_primary %}
+        <form method="post" action="/settings/account/emails/{{ e.id }}/delete">
+          <button class="kg-btn kg-btn--danger-ghost" type="submit">remove</button>
+        </form>
+        {% endif %}
       </li>
       {% endfor %}
     </ul>
@@ -158,34 +131,6 @@
   </div>
 
   <div class="kg-settings-section">
-    <span class="kg-kicker">security audit log</span>
-    <p class="kg-muted">Recent sign-ins and account changes for this account.</p>
-    {% if audit_entries.is_empty() %}
-      <p class="kg-muted">No audit events yet.</p>
-    {% else %}
-      <ul class="kg-list" style="margin-top:0.75rem">
-        {% for e in audit_entries %}
-        <li>
-          <span>
-            <strong>{{ e.action_label }}</strong>
-            <span class="kg-meta"> · {{ e.created_at }}</span>
-            {% if !e.ip.is_empty() %}
-              <span class="kg-meta"> · {{ e.ip }}</span>
-            {% endif %}
-            {% if !e.user_agent.is_empty() %}
-              <span class="kg-meta"> · {{ e.user_agent }}</span>
-            {% endif %}
-            {% if !e.metadata.is_empty() %}
-              <span class="kg-meta"> · <code>{{ e.metadata }}</code></span>
-            {% endif %}
-          </span>
-        </li>
-        {% endfor %}
-      </ul>
-    {% endif %}
-  </div>
-
-  <div class="kg-settings-section">
     <span class="kg-kicker">GDPR</span>
     <div class="kg-actions">
       <a class="kg-btn kg-btn--ghost" href="/settings/account/export">export my data</a>
@@ -196,8 +141,8 @@
     <span class="kg-kicker">delete account</span>
     <form class="kg-form" method="post" action="/settings/account/delete">
       <div class="kg-danger-row">
-        <label class="kg-danger-confirm">
-          <span>type <code>{{ user.username }}</code> to delete</span>
+        <label>
+          type <code>{{ user.username }}</code> to delete
           <input type="text" name="confirm" required autocomplete="off" />
         </label>
         <button class="kg-btn kg-btn--danger" type="submit">delete account</button>
templates/admin.html +0 −1
diff --git a/templates/admin.html b/templates/admin.html
index 2723110..f16f5c8 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -104,7 +104,6 @@
         </span>
       </div>
       <div class="kg-admin-actions">
-        <a class="kg-btn kg-btn--ghost" href="/admin/users/{{ u.username }}/audit">audit</a>
         <form method="post" action="/admin/users/suspend" style="display:inline">
           <input type="hidden" name="user_id" value="{{ u.id }}" />
           {% if u.is_suspended %}
templates/admin_user_audit.html +0 −37
diff --git a/templates/admin_user_audit.html b/templates/admin_user_audit.html
deleted file mode 100644
index 5dc5a6b..0000000
--- a/templates/admin_user_audit.html
+++ /dev/null
@@ -1,37 +0,0 @@
-{% extends "layout.html" %}
-{% block title %}audit — {{ user.username }} — kitgit{% endblock %}
-{% block content %}
-<section class="kg-section">
-  <span class="kg-kicker">admin</span>
-  <h1 class="kg-title">Audit log · {{ user.username }}</h1>
-  <p class="kg-muted">
-    <a href="/admin">← back to admin</a>
-    · <a href="/{{ user.username }}">profile</a>
-  </p>
-
-  {% if audit_entries.is_empty() %}
-    <p class="kg-muted" style="margin-top:1.5rem">No audit events for this user.</p>
-  {% else %}
-    <ul class="kg-list" style="margin-top:1.5rem">
-      {% for e in audit_entries %}
-      <li>
-        <span>
-          <strong>{{ e.action_label }}</strong>
-          <code class="kg-meta">{{ e.action }}</code>
-          <span class="kg-meta"> · {{ e.created_at }}</span>
-          {% if !e.ip.is_empty() %}
-            <span class="kg-meta"> · {{ e.ip }}</span>
-          {% endif %}
-          {% if !e.user_agent.is_empty() %}
-            <span class="kg-meta"> · {{ e.user_agent }}</span>
-          {% endif %}
-          {% if !e.metadata.is_empty() %}
-            <span class="kg-meta"> · <code>{{ e.metadata }}</code></span>
-          {% endif %}
-        </span>
-      </li>
-      {% endfor %}
-    </ul>
-  {% endif %}
-</section>
-{% endblock %}
templates/explore.html +3 −3
diff --git a/templates/explore.html b/templates/explore.html
index 257b735..52b996b 100644
--- a/templates/explore.html
+++ b/templates/explore.html
@@ -62,7 +62,7 @@
                 <p class="kg-meta" style="margin:0.25rem 0 0">{{ item.repo.description }}</p>
               {% endif %}
             </div>
-            <span class="kg-meta">{{ item.repo.updated_at.format("%Y-%m-%d") }}</span>
+            <span class="kg-meta">{{ item.repo.updated_at }}</span>
           </li>
           {% endfor %}
         </ul>
@@ -113,7 +113,7 @@
                 {{ item.owner }}/{{ item.repo_name }}#{{ item.number }} {{ item.title }}
               </a>
             </div>
-            <span class="kg-meta">{{ item.updated_at.format("%Y-%m-%d") }}</span>
+            <span class="kg-meta">{{ item.updated_at }}</span>
           </li>
           {% endfor %}
         </ul>
@@ -138,7 +138,7 @@
                 {{ item.owner }}/{{ item.repo_name }}#{{ item.number }} {{ item.title }}
               </a>
             </div>
-            <span class="kg-meta">{{ item.updated_at.format("%Y-%m-%d") }}</span>
+            <span class="kg-meta">{{ item.updated_at }}</span>
           </li>
           {% endfor %}
         </ul>
templates/issue_new.html +0 −22
diff --git a/templates/issue_new.html b/templates/issue_new.html
index 5cabf33..0319760 100644
--- a/templates/issue_new.html
+++ b/templates/issue_new.html
@@ -20,28 +20,6 @@
       body
       <textarea name="body" rows="8"></textarea>
     </label>
-    {% if !labels.is_empty() %}
-    <fieldset class="kg-fieldset">
-      <legend>labels</legend>
-      {% for l in labels %}
-        <label class="row">
-          <input type="checkbox" name="label_id" value="{{ l.id }}" />
-          <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>
-        </label>
-      {% endfor %}
-    </fieldset>
-    {% endif %}
-    {% if !milestones.is_empty() %}
-    <label>
-      milestone
-      <select name="milestone_id">
-        <option value="">none</option>
-        {% for m in milestones %}
-          <option value="{{ m.id }}">{{ m.title }}</option>
-        {% endfor %}
-      </select>
-    </label>
-    {% endif %}
     <div class="kg-actions">
       <button class="kg-btn" type="submit">create</button>
       <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/issues">cancel</a>
templates/issue_view.html +36 −91
diff --git a/templates/issue_view.html b/templates/issue_view.html
index 42e80ee..2158d27 100644
--- a/templates/issue_view.html
+++ b/templates/issue_view.html
@@ -12,101 +12,46 @@
   {% let archive_ref = repo.default_branch %}
   {% include "partials/repo_tabs.html" %}
 
-  <div class="kg-issue-layout">
-    <div class="kg-issue-main">
-      <article class="kg-comment">
-        <header>
-          <span>
-            <img class="kg-avatar kg-avatar--sm" src="{{ author_avatar }}" alt="" />
-            <a href="/{{ author.username }}">{{ author.username }}</a>
-          </span>
-          <span>{{ issue.created_at }}</span>
-        </header>
-        <div class="kg-readme">{{ body_html|safe }}</div>
-      </article>
+  <article class="kg-comment">
+    <header>
+      <span>
+        <img class="kg-avatar kg-avatar--sm" src="{{ author_avatar }}" alt="" />
+        <a href="/{{ author.username }}">{{ author.username }}</a>
+      </span>
+      <span>{{ issue.created_at }}</span>
+    </header>
+    <div class="kg-readme">{{ body_html|safe }}</div>
+  </article>
 
-      {% for c in comments %}
-      <article class="kg-comment">
-        <header>
-          <span>
-            <img class="kg-avatar kg-avatar--sm" src="{{ c.avatar_url }}" alt="" />
-            <a href="/{{ c.author.username }}">{{ c.author.username }}</a>
-          </span>
-          <span>{{ c.created_at }}</span>
-        </header>
-        <div class="kg-readme">{{ c.body_html|safe }}</div>
-        {% include "partials/reactions.html" %}
-      </article>
-      {% endfor %}
+  {% for c in comments %}
+  <article class="kg-comment">
+    <header>
+      <span>
+        <img class="kg-avatar kg-avatar--sm" src="{{ c.avatar_url }}" alt="" />
+        <a href="/{{ c.author.username }}">{{ c.author.username }}</a>
+      </span>
+      <span>{{ c.created_at }}</span>
+    </header>
+    <div class="kg-readme">{{ c.body_html|safe }}</div>
+    {% include "partials/reactions.html" %}
+  </article>
+  {% endfor %}
 
-      {% if viewer.is_some() %}
-        <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}">
-          <label>
-            comment
-            <textarea name="body" rows="4" required></textarea>
-          </label>
-          <div class="kg-actions">
-            <button class="kg-btn" type="submit">comment</button>
-            {% if issue.state == "open" %}
-              <button class="kg-btn kg-btn--ghost" formaction="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}/close" formmethod="post" type="submit" formnovalidate>close</button>
-            {% else %}
-              <button class="kg-btn kg-btn--ghost" formaction="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}/reopen" formmethod="post" type="submit" formnovalidate>reopen</button>
-            {% endif %}
-          </div>
-        </form>
-      {% endif %}
-    </div>
-
-    <aside class="kg-issue-sidebar">
-      <div class="kg-sidebar-block">
-        <h2 class="kg-kicker">labels</h2>
-        {% if labels.is_empty() %}
-          <p class="kg-muted">None yet.</p>
-        {% else %}
-          <div class="kg-label-row">
-            {% for l in labels %}
-              <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>
-            {% endfor %}
-          </div>
-        {% endif %}
-        {% if can_triage %}
-          <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}/labels">
-            {% for opt in label_options %}
-              <label class="row">
-                <input type="checkbox" name="label_id" value="{{ opt.label.id }}"{% if opt.selected %} checked{% endif %} />
-                <span class="kg-label-chip" style="background:{{ opt.label.bg_color() }};color:{{ opt.label.text_color() }}">{{ opt.label.name }}</span>
-              </label>
-            {% endfor %}
-            {% if !label_options.is_empty() %}
-              <button class="kg-btn kg-btn--ghost" type="submit">save labels</button>
-            {% else %}
-              <p class="kg-muted"><a href="/{{ owner.username }}/{{ repo.name }}/labels">Manage labels</a></p>
-            {% endif %}
-          </form>
-        {% endif %}
-      </div>
-      <div class="kg-sidebar-block">
-        <h2 class="kg-kicker">milestone</h2>
-        {% if let Some(m) = milestone %}
-          <p>{{ m.title }}{% if let Some(due) = m.due_on %} · due {{ due }}{% endif %}</p>
+  {% if viewer.is_some() %}
+    <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}">
+      <label>
+        comment
+        <textarea name="body" rows="4" required></textarea>
+      </label>
+      <div class="kg-actions">
+        <button class="kg-btn" type="submit">comment</button>
+        {% if issue.state == "open" %}
+          <button class="kg-btn kg-btn--ghost" formaction="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}/close" formmethod="post" type="submit" formnovalidate>close</button>
         {% else %}
-          <p class="kg-muted">No milestone</p>
-        {% endif %}
-        {% if can_triage %}
-          <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}/milestone">
-            <label>
-              <select name="milestone_id">
-                <option value="">none</option>
-                {% for opt in milestone_options %}
-                  <option value="{{ opt.milestone.id }}"{% if opt.selected %} selected{% endif %}>{{ opt.milestone.title }}</option>
-                {% endfor %}
-              </select>
-            </label>
-            <button class="kg-btn kg-btn--ghost" type="submit">save milestone</button>
-          </form>
+          <button class="kg-btn kg-btn--ghost" formaction="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}/reopen" formmethod="post" type="submit" formnovalidate>reopen</button>
         {% endif %}
       </div>
-    </aside>
-  </div>
+    </form>
+  {% endif %}
 </section>
 {% endblock %}
templates/issues_list.html +4 −12
diff --git a/templates/issues_list.html b/templates/issues_list.html
index 0cfb259..6c1629d 100644
--- a/templates/issues_list.html
+++ b/templates/issues_list.html
@@ -18,27 +18,19 @@
     {% if viewer.is_some() %}
       <a class="kg-btn" href="/{{ owner.username }}/{{ repo.name }}/issues/new">new issue</a>
     {% endif %}
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/labels">labels</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones">milestones</a>
   </div>
 
   {% if issues.is_empty() %}
     <p class="kg-muted">No {{ state_filter }} issues.</p>
   {% else %}
     <ul class="kg-list">
-      {% for item in issues %}
+      {% for issue in issues %}
       <li>
         <span>
-          <span class="kg-badge {% if item.issue.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ item.issue.state }}</span>
-          <a href="/{{ owner.username }}/{{ repo.name }}/issues/{{ item.issue.number }}">#{{ item.issue.number }} {{ item.issue.title }}</a>
-          {% for l in item.labels %}
-            <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>
-          {% endfor %}
-          {% if let Some(m) = item.milestone %}
-            <span class="kg-meta">milestone: {{ m.title }}</span>
-          {% endif %}
+          <span class="kg-badge {% if issue.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ issue.state }}</span>
+          <a href="/{{ owner.username }}/{{ repo.name }}/issues/{{ issue.number }}">#{{ issue.number }} {{ issue.title }}</a>
         </span>
-        <span class="kg-meta">{{ item.issue.updated_at.format("%Y-%m-%d") }}</span>
+        <span class="kg-meta">{{ issue.updated_at }}</span>
       </li>
       {% endfor %}
     </ul>
templates/keys_settings.html +1 −16
diff --git a/templates/keys_settings.html b/templates/keys_settings.html
index 64d02b4..ecd9ef5 100644
--- a/templates/keys_settings.html
+++ b/templates/keys_settings.html
@@ -17,7 +17,7 @@
   {% endif %}
 
   <span class="kg-kicker" style="margin-top:1.5rem">SSH keys</span>
-  <p class="kg-muted">Authentication keys let you push/pull over SSH. Signing keys verify signed commits on kitgit (Verified badge).</p>
+  <p class="kg-muted">Authentication keys let you push/pull over SSH. Signing keys verify signed commits.</p>
   {% if !keys.is_empty() %}
     <ul class="kg-list">
       {% for key in keys %}
@@ -70,14 +70,6 @@
     </div>
   </form>
 
-  <div class="kg-signing-hint">
-    <strong>Sign commits with SSH</strong>
-    <p class="kg-muted" style="margin:0.35rem 0 0">Add the public key above with usage <em>Signing</em> or <em>Authentication &amp; Signing</em>, then configure git locally. Author email must match a verified email on your account.</p>
-    <pre>git config --global gpg.format ssh
-git config --global user.signingkey ~/.ssh/id_ed25519.pub
-git config --global commit.gpgsign true</pre>
-  </div>
-
   <span class="kg-kicker" style="margin-top:2.5rem">GPG keys</span>
   <p class="kg-muted">Used for commit verification. Vigilant mode flags unsigned commits on your profile.</p>
   {% if !gpg_keys.is_empty() %}
@@ -112,12 +104,5 @@ git config --global commit.gpgsign true</pre>
       <button class="kg-btn" type="submit">add</button>
     </div>
   </form>
-
-  <div class="kg-signing-hint">
-    <strong>Sign commits with GPG</strong>
-    <p class="kg-muted" style="margin:0.35rem 0 0">Paste your public key above, then point git at the key id. Author email must match a verified email on your account.</p>
-    <pre>git config --global user.signingkey YOUR_KEY_ID
-git config --global commit.gpgsign true</pre>
-  </div>
 </section>
 {% endblock %}
templates/labels_list.html +0 −15
diff --git a/templates/labels_list.html b/templates/labels_list.html
deleted file mode 100644
index d31a1bc..0000000
--- a/templates/labels_list.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "layout.html" %}
-{% block title %}labels · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title"><a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a> · labels</h1>
-  {% let tab = "settings" %}
-  {% let archive_ref = repo.default_branch %}
-  {% include "partials/repo_tabs.html" %}
-  {% if let Some(err) = error %}<div class="kg-flash">{{ err }}</div>{% endif %}
-  <div class="kg-actions"><a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones">milestones</a><a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/settings">settings</a></div>
-  {% if labels.is_empty() %}<p class="kg-muted">No labels yet.</p>{% else %}
-  <ul class="kg-list">{% for l in labels %}<li><span><span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span> <span class="kg-meta">{{ l.description }}</span></span>{% if access.can_admin() %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/labels/{{ l.id }}/delete"><button class="kg-btn kg-btn--danger-ghost" type="submit">delete</button></form>{% endif %}</li>{% if access.can_admin() %}<li><form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/labels/{{ l.id }}/update"><label>name <input name="name" value="{{ l.name }}" required /></label><label>color <input name="color" value="{{ l.color }}" required /></label><label>description <input name="description" value="{{ l.description }}" /></label><button class="kg-btn kg-btn--ghost" type="submit">update</button></form></li>{% endif %}{% endfor %}</ul>{% endif %}
-  {% if access.can_admin() %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/labels"><span class="kg-kicker">new label</span><label>name <input name="name" required /></label><label>color <input name="color" value="0969da" required /></label><label>description <input name="description" /></label><button class="kg-btn" type="submit">create label</button></form>{% endif %}
-</section>
-{% endblock %}
templates/layout.html +2 −23
diff --git a/templates/layout.html b/templates/layout.html
index 16b2e34..f589a61 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en" data-theme="{% if let Some(u) = viewer %}{{ u.theme_pref() }}{% else %}system{% endif %}">
+<html lang="en">
 <head>
   <meta charset="utf-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -23,11 +23,6 @@
         <a href="/explore"><span class="kg-icon" style="--icon:url('/static/icons/code.svg')"></span> explore</a>
         {% if let Some(u) = viewer %}
           <a href="/new"><span class="kg-icon" style="--icon:url('/static/icons/plus.svg')"></span> new</a>
-          <a id="kg-nav-notifications" href="/notifications">
-            <span class="kg-icon" style="--icon:url('/static/icons/bell.svg')"></span>
-            notifications
-            <span id="kg-notif-count" class="kg-notif-count" hidden></span>
-          </a>
           {% if u.is_site_admin %}<a href="/admin"><span class="kg-icon" style="--icon:url('/static/icons/settings.svg')"></span> admin</a>{% endif %}
           <a href="/settings/profile"><span class="kg-icon" style="--icon:url('/static/icons/user.svg')"></span> settings</a>
           <a class="kg-userchip" href="/{{ u.username }}">
@@ -44,10 +39,7 @@
     <main>
       {% block content %}{% endblock %}
     </main>
-    <footer class="kg-footer">
-      <span>kitgit</span>
-      <span class="kg-footer__timing">rendered in <!--kg-render-ms--></span>
-    </footer>
+    <footer class="kg-footer">kitgit</footer>
   </div>
   <script>
   (async () => {
@@ -62,19 +54,6 @@
       }
     } catch (_) {}
   })();
-  (async () => {
-    const badge = document.getElementById('kg-notif-count');
-    if (!badge) return;
-    try {
-      const r = await fetch('/notifications/unread.json', { credentials: 'same-origin' });
-      if (!r.ok) return;
-      const j = await r.json();
-      const n = Number(j && j.count);
-      if (!n) return;
-      badge.textContent = n > 99 ? '99+' : String(n);
-      badge.hidden = false;
-    } catch (_) {}
-  })();
   document.addEventListener('click', (e) => {
     const btn = e.target.closest('[data-copy]');
     if (!btn) return;
templates/milestones_list.html +0 −19
diff --git a/templates/milestones_list.html b/templates/milestones_list.html
deleted file mode 100644
index 7947f22..0000000
--- a/templates/milestones_list.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "layout.html" %}
-{% block title %}milestones · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title"><a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a> · milestones</h1>
-  {% let tab = "settings" %}
-  {% let archive_ref = repo.default_branch %}
-  {% include "partials/repo_tabs.html" %}
-  {% if let Some(err) = error %}<div class="kg-flash">{{ err }}</div>{% endif %}
-  <div class="kg-actions">
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones?state=open"{% if state_filter == "open" %} aria-current="page"{% endif %}>open</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones?state=closed"{% if state_filter == "closed" %} aria-current="page"{% endif %}>closed</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/labels">labels</a>
-  </div>
-  {% if milestones.is_empty() %}<p class="kg-muted">No {{ state_filter }} milestones.</p>{% else %}
-  <ul class="kg-list">{% for m in milestones %}<li><span><span class="kg-badge {% if m.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ m.state }}</span> <strong>{{ m.title }}</strong>{% if let Some(due) = m.due_on %} <span class="kg-meta">due {{ due }}</span>{% endif %}</span>{% if access.can_admin() %}<span class="kg-actions">{% if m.state == "open" %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones/{{ m.id }}/close"><button class="kg-btn kg-btn--ghost" type="submit">close</button></form>{% else %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones/{{ m.id }}/reopen"><button class="kg-btn kg-btn--ghost" type="submit">reopen</button></form>{% endif %}<form method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones/{{ m.id }}/delete"><button class="kg-btn kg-btn--danger-ghost" type="submit">delete</button></form></span>{% endif %}</li>{% endfor %}</ul>{% endif %}
-  {% if access.can_admin() %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/milestones"><span class="kg-kicker">new milestone</span><label>title <input name="title" required /></label><label>due on <input type="date" name="due_on" /></label><label>description <textarea name="description" rows="3"></textarea></label><button class="kg-btn" type="submit">create milestone</button></form>{% endif %}
-</section>
-{% endblock %}
templates/notifications.html +0 −47
diff --git a/templates/notifications.html b/templates/notifications.html
deleted file mode 100644
index 59c90c1..0000000
--- a/templates/notifications.html
+++ /dev/null
@@ -1,47 +0,0 @@
-{% extends "layout.html" %}
-
-{% block title %}notifications — kitgit{% endblock %}
-
-{% block content %}
-<section class="kg-section">
-  <div class="kg-actions" style="justify-content: space-between; align-items: baseline;">
-    <h1 class="kg-title">notifications</h1>
-    {% if unread_count > 0 %}
-      <form method="post" action="/notifications/read-all">
-        <button class="kg-btn kg-btn--ghost" type="submit">mark all read</button>
-      </form>
-    {% endif %}
-  </div>
-
-  {% if notifications.is_empty() %}
-    <p class="kg-muted">No notifications yet.</p>
-  {% else %}
-    <ul class="kg-list kg-notif-list">
-      {% for n in notifications %}
-      <li class="{% if n.read_at.is_none() %}kg-notif--unread{% endif %}">
-        <span class="kg-notif-main">
-          {% if n.href.is_empty() %}
-            <strong>{{ n.title }}</strong>
-          {% else %}
-            <a href="{{ n.href }}"><strong>{{ n.title }}</strong></a>
-          {% endif %}
-          {% if !n.body.is_empty() %}
-            <span class="kg-muted">{{ n.body }}</span>
-          {% endif %}
-          <span class="kg-meta">{{ n.created_at }} · {{ n.kind }}</span>
-        </span>
-        <span class="kg-notif-actions">
-          {% if n.read_at.is_none() %}
-            <form method="post" action="/notifications/{{ n.id }}/read">
-              <button class="kg-btn kg-btn--ghost" type="submit">mark read</button>
-            </form>
-          {% else %}
-            <span class="kg-faint">read</span>
-          {% endif %}
-        </span>
-      </li>
-      {% endfor %}
-    </ul>
-  {% endif %}
-</section>
-{% endblock %}
templates/partials/branch_bar.html +30 −31
diff --git a/templates/partials/branch_bar.html b/templates/partials/branch_bar.html
index 91ce761..5069541 100644
--- a/templates/partials/branch_bar.html
+++ b/templates/partials/branch_bar.html
@@ -1,10 +1,33 @@
-{# Expects: owner, repo, branch, latest_commit: Option<CommitView>, access, upload_path, commit_count #}
-<div class="kg-toolbar">
-  <a class="kg-btn kg-btn--ghost kg-toolbar__branch" href="/{{ owner.username }}/{{ repo.name }}/branches">
-    <span class="kg-icon" style="--icon:url('/static/icons/git-branch.svg')"></span>
-    {{ branch }}
-  </a>
-  <div class="kg-toolbar__actions">
+{# Expects: owner, repo, branch, latest_commit: Option<CommitView>, access (optional for upload), upload_path (optional) #}
+<div class="kg-branchbar">
+  <div class="kg-branchbar__left">
+    <a class="kg-branchbar__branch" href="/{{ owner.username }}/{{ repo.name }}/branches">
+      <span class="kg-icon" style="--icon:url('/static/icons/git-branch.svg')"></span>
+      {{ branch }}
+    </a>
+    {% if let Some(c) = latest_commit %}
+    <div class="kg-branchbar__commit">
+      <a class="kg-branchbar__sha" href="/{{ owner.username }}/{{ repo.name }}/commits" title="View commits">
+        <span class="kg-icon" style="--icon:url('/static/icons/git-commit.svg')"></span>
+        {{ c.short_id }}
+      </a>
+      <a class="kg-branchbar__msg" href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}" title="View commit">{{ c.message }}</a>
+      {% if c.verified %}
+      <details class="kg-verify">
+        <summary class="kg-badge kg-badge--verified">Verified</summary>
+        <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
+          <p class="kg-verify__fp">
+            <strong>{{ c.verify_fingerprint_label }}:</strong>
+            <code>{{ c.verify_fingerprint }}</code>
+          </p>
+          <p class="kg-meta">Verified on {{ c.verified_at }}</p>
+        </div>
+      </details>
+      {% endif %}
+    </div>
+    {% endif %}
+  </div>
+  <div class="kg-branchbar__actions">
     {% if upload_path != "" %}
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/archive.zip?ref={{ branch }}&path={{ upload_path }}" title="Download this folder as ZIP">
       <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
@@ -45,27 +68,3 @@
     {% endif %}
   </div>
 </div>
-
-{% if let Some(c) = latest_commit %}
-<div class="kg-latest">
-  <div class="kg-latest__main">
-    {% if let Some(uname) = c.author_username %}
-      <a class="kg-latest__author" href="/{{ uname }}">{{ c.author }}</a>
-    {% else %}
-      <span class="kg-latest__author">{{ c.author }}</span>
-    {% endif %}
-    <a class="kg-latest__msg" href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}" title="{{ c.message }}">{{ c.message }}</a>
-  </div>
-  <div class="kg-latest__meta">
-    <a class="kg-latest__sha" href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}" title="{{ c.id }}">
-      <code>{{ c.short_id }}</code>
-    </a>
-    {% let show_unverified = false %}
-    {% include "partials/verified_badge.html" %}
-    <time class="kg-meta kg-latest__time">{{ c.time_display() }}</time>
-    <a class="kg-latest__count kg-meta" href="/{{ owner.username }}/{{ repo.name }}/commits" title="{{ commit_count }} commits">
-      {{ commit_count }} commits
-    </a>
-  </div>
-</div>
-{% endif %}
templates/partials/repo_tabs.html +0 −2
diff --git a/templates/partials/repo_tabs.html b/templates/partials/repo_tabs.html
index d56147e..5ca44db 100644
--- a/templates/partials/repo_tabs.html
+++ b/templates/partials/repo_tabs.html
@@ -31,7 +31,6 @@
     </a>
     {% endif %}
   </div>
-  {% if tab == "code" %}
   <details class="kg-dropdown">
     <summary class="kg-btn kg-btn--ghost">
       <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
@@ -59,5 +58,4 @@
       </div>
     </div>
   </details>
-  {% endif %}
 </nav>
templates/partials/verified_badge.html +4 −16
diff --git a/templates/partials/verified_badge.html b/templates/partials/verified_badge.html
index 6697dca..d448c5c 100644
--- a/templates/partials/verified_badge.html
+++ b/templates/partials/verified_badge.html
@@ -1,24 +1,12 @@
-{# Expects CommitView as `c`. Optional: `show_unverified` (bool) — show Unverified for unsigned too. #}
-{% if c.verified %}
+{% if let Some(v) = verification %}
 <details class="kg-verify">
   <summary class="kg-badge kg-badge--verified">Verified</summary>
   <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
-    <p class="kg-verify__row"><strong>Type:</strong> {{ c.verify_kind_label() }}</p>
     <p class="kg-verify__fp">
-      <strong>{{ c.verify_fingerprint_label }}:</strong>
-      <code>{{ c.verify_fingerprint }}</code>
+      <strong>{{ v.fingerprint_label() }}:</strong>
+      <code>{{ v.fingerprint }}</code>
     </p>
-    <p class="kg-meta">Verified on {{ c.verified_at }}</p>
+    <p class="kg-meta">Verified on {{ v.verified_at }}</p>
   </div>
 </details>
-{% else if c.signed %}
-<details class="kg-verify">
-  <summary class="kg-badge kg-badge--unverified">Unverified</summary>
-  <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
-    <p class="kg-verify__row">Signature present but could not be verified.</p>
-    <p class="kg-meta">Add a matching signing SSH or GPG key under <a href="/settings/keys">Settings → SSH / GPG</a>. The commit author email must match a verified email on that account.</p>
-  </div>
-</details>
-{% else if show_unverified %}
-<span class="kg-badge kg-badge--unverified" title="Commit is not signed">Unverified</span>
 {% endif %}
templates/pull_new.html +33 −8
diff --git a/templates/pull_new.html b/templates/pull_new.html
index b8957a8..21d97f5 100644
--- a/templates/pull_new.html
+++ b/templates/pull_new.html
@@ -1,20 +1,45 @@
 {% extends "layout.html" %}
+
 {% block title %}new pull · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
+
 {% block content %}
 <section class="kg-section">
   <h1 class="kg-title">New pull request</h1>
   {% let tab = "pulls" %}
   {% let archive_ref = repo.default_branch %}
   {% include "partials/repo_tabs.html" %}
-  {% if let Some(err) = error %}<div class="kg-flash">{{ err }}</div>{% endif %}
+  {% if let Some(err) = error %}
+    <div class="kg-flash">{{ err }}</div>
+  {% endif %}
   <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls">
-    <label>title <input type="text" name="title" required autofocus /></label>
-    <label>source branch<select name="source_branch" required>{% for b in branches %}<option value="{{ b }}">{{ b }}</option>{% endfor %}</select></label>
-    <label>target branch<select name="target_branch" required>{% for b in branches %}<option value="{{ b }}"{% if b.as_str() == repo.default_branch.as_str() %} selected{% endif %}>{{ b }}</option>{% endfor %}</select></label>
-    <label>body <textarea name="body" rows="8"></textarea></label>
-    {% if !labels.is_empty() %}<fieldset class="kg-fieldset"><legend>labels</legend>{% for l in labels %}<label class="row"><input type="checkbox" name="label_id" value="{{ l.id }}" /> <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span></label>{% endfor %}</fieldset>{% endif %}
-    {% if !milestones.is_empty() %}<label>milestone<select name="milestone_id"><option value="">none</option>{% for m in milestones %}<option value="{{ m.id }}">{{ m.title }}</option>{% endfor %}</select></label>{% endif %}
-    <div class="kg-actions"><button class="kg-btn" type="submit">create</button><a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls">cancel</a></div>
+    <label>
+      title
+      <input type="text" name="title" required autofocus />
+    </label>
+    <label>
+      source branch
+      <select name="source_branch" required>
+        {% for b in branches %}
+          <option value="{{ b }}">{{ b }}</option>
+        {% endfor %}
+      </select>
+    </label>
+    <label>
+      target branch
+      <select name="target_branch" required>
+        {% for b in branches %}
+          <option value="{{ b }}"{% if b.as_str() == repo.default_branch.as_str() %} selected{% endif %}>{{ b }}</option>
+        {% endfor %}
+      </select>
+    </label>
+    <label>
+      body
+      <textarea name="body" rows="8"></textarea>
+    </label>
+    <div class="kg-actions">
+      <button class="kg-btn" type="submit">create</button>
+      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls">cancel</a>
+    </div>
   </form>
 </section>
 {% endblock %}
templates/pull_view.html +14 −63
diff --git a/templates/pull_view.html b/templates/pull_view.html
index 843c4e1..92b3f55 100644
--- a/templates/pull_view.html
+++ b/templates/pull_view.html
@@ -9,19 +9,6 @@
     #{{ pull.number }} {{ pull.title }}
   </h1>
   <p class="kg-meta">{{ pull.source_branch }} → {{ pull.target_branch }}</p>
-  <aside class="kg-issue-sidebar" style="margin:1rem 0">
-    <div class="kg-sidebar-block">
-      <h2 class="kg-kicker">labels</h2>
-      {% if labels.is_empty() %}<p class="kg-muted">None yet.</p>{% else %}<div class="kg-label-row">{% for l in labels %}<span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>{% endfor %}</div>{% endif %}
-      {% if can_triage %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}/labels">{% for opt in label_options %}<label class="row"><input type="checkbox" name="label_id" value="{{ opt.label.id }}"{% if opt.selected %} checked{% endif %} /> <span class="kg-label-chip" style="background:{{ opt.label.bg_color() }};color:{{ opt.label.text_color() }}">{{ opt.label.name }}</span></label>{% endfor %}{% if !label_options.is_empty() %}<button class="kg-btn kg-btn--ghost" type="submit">save labels</button>{% endif %}</form>{% endif %}
-    </div>
-    <div class="kg-sidebar-block">
-      <h2 class="kg-kicker">milestone</h2>
-      {% if let Some(m) = milestone %}<p>{{ m.title }}</p>{% else %}<p class="kg-muted">No milestone</p>{% endif %}
-      {% if can_triage %}<form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}/milestone"><label><select name="milestone_id"><option value="">none</option>{% for opt in milestone_options %}<option value="{{ opt.milestone.id }}"{% if opt.selected %} selected{% endif %}>{{ opt.milestone.title }}</option>{% endfor %}</select></label><button class="kg-btn kg-btn--ghost" type="submit">save milestone</button></form>{% endif %}
-    </div>
-  </aside>
-
   {% let pr_tab = tab %}
   {% let tab = "pulls" %}
   {% let archive_ref = repo.default_branch %}
@@ -38,7 +25,7 @@
       commits
       <span class="kg-pr-tabs__count">{{ commits.len() }}</span>
     </a>
-    <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}?tab=files{% if show_full %}&amp;full=1{% endif %}"{% if pr_tab == "files" %} aria-current="page"{% endif %}>
+    <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}?tab=files"{% if pr_tab == "files" %} aria-current="page"{% endif %}>
       <span class="kg-icon" style="--icon:url('/static/icons/file.svg')"></span>
       files
       <span class="kg-pr-tabs__count">{{ diff_files.len() }}</span>
@@ -71,51 +58,6 @@
   </article>
   {% endfor %}
 
-  {% for r in reviews %}
-  <article class="kg-comment kg-review">
-    <header>
-      <span>
-        <img class="kg-avatar kg-avatar--sm" src="{{ r.avatar_url }}" alt="" />
-        <a href="/{{ r.reviewer.username }}">{{ r.reviewer.username }}</a>
-        <span class="kg-badge {{ r.state_class() }}">{{ r.state_label() }}</span>
-      </span>
-      <span>{{ r.created_at }}</span>
-    </header>
-    {% if !r.body.is_empty() %}
-    <div class="kg-readme">{{ r.body_html|safe }}</div>
-    {% endif %}
-  </article>
-  {% endfor %}
-
-  {% if viewer.is_some() && pull.state == "open" %}
-    <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}/review">
-      <fieldset class="kg-review-state">
-        <legend>review</legend>
-        {% if !is_author %}
-        <label class="row">
-          <input type="radio" name="state" value="approved" />
-          approve
-        </label>
-        <label class="row">
-          <input type="radio" name="state" value="changes_requested" />
-          request changes
-        </label>
-        {% endif %}
-        <label class="row">
-          <input type="radio" name="state" value="commented" checked />
-          comment
-        </label>
-      </fieldset>
-      <label>
-        review notes
-        <textarea name="body" rows="4" placeholder="Leave a review comment (optional)"></textarea>
-      </label>
-      <div class="kg-actions">
-        <button class="kg-btn" type="submit">submit review</button>
-      </div>
-    </form>
-  {% endif %}
-
   {% if viewer.is_some() %}
     <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}">
       <label>
@@ -156,8 +98,18 @@
           <span>
             <a href="/{{ owner.username }}/{{ repo.name }}/commits" title="View commits"><code>{{ c.short_id }}</code></a>
             <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}">{{ c.message }}</a>
-            {% let show_unverified = false %}
-            {% include "partials/verified_badge.html" %}
+            {% if c.verified %}
+            <details class="kg-verify">
+              <summary class="kg-badge kg-badge--verified">Verified</summary>
+              <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
+                <p class="kg-verify__fp">
+                  <strong>{{ c.verify_fingerprint_label }}:</strong>
+                  <code>{{ c.verify_fingerprint }}</code>
+                </p>
+                <p class="kg-meta">Verified on {{ c.verified_at }}</p>
+              </div>
+            </details>
+            {% endif %}
           </span>
           <span class="kg-meta">{{ c.author }}</span>
         </li>
@@ -204,11 +156,10 @@
               </span>
             </header>
             {{ f.html|safe }}
-            {% if f.truncated && !show_full %}
+            {% if f.truncated %}
               <p class="kg-diff__truncated">
                 Large diffs are not rendered by default.
                 Showing the first 50 of {{ f.total_lines }} lines.
-                <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}?tab=files&amp;full=1#{{ f.anchor }}">Show full diff</a>
               </p>
             {% endif %}
           </section>
templates/pulls_list.html +27 −6
diff --git a/templates/pulls_list.html b/templates/pulls_list.html
index 50da817..69425f5 100644
--- a/templates/pulls_list.html
+++ b/templates/pulls_list.html
@@ -1,20 +1,41 @@
 {% extends "layout.html" %}
+
 {% block title %}pulls · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
+
 {% block content %}
 <section class="kg-section">
-  <h1 class="kg-title"><a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a> · pulls</h1>
+  <h1 class="kg-title">
+    <a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a>
+    · pulls
+  </h1>
   {% let tab = "pulls" %}
   {% let archive_ref = repo.default_branch %}
   {% include "partials/repo_tabs.html" %}
+
   <div class="kg-actions">
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls?state=open"{% if state_filter == "open" %} aria-current="page"{% endif %}>open</a>
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls?state=closed"{% if state_filter == "closed" %} aria-current="page"{% endif %}>closed</a>
     <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/pulls?state=merged"{% if state_filter == "merged" %} aria-current="page"{% endif %}>merged</a>
-    {% if viewer.is_some() %}<a class="kg-btn" href="/{{ owner.username }}/{{ repo.name }}/pulls/new">new pull</a>{% endif %}
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/labels">labels</a>
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones">milestones</a>
+    {% if viewer.is_some() %}
+      <a class="kg-btn" href="/{{ owner.username }}/{{ repo.name }}/pulls/new">new pull</a>
+    {% endif %}
   </div>
-  {% if pulls.is_empty() %}<p class="kg-muted">No {{ state_filter }} pulls.</p>{% else %}
-  <ul class="kg-list">{% for item in pulls %}<li><span><span class="kg-badge {% if item.pull.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ item.pull.state }}</span> <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ item.pull.number }}">#{{ item.pull.number }} {{ item.pull.title }}</a> <span class="kg-meta">{{ item.pull.source_branch }} → {{ item.pull.target_branch }}</span>{% for l in item.labels %} <span class="kg-label-chip" style="background:{{ l.bg_color() }};color:{{ l.text_color() }}">{{ l.name }}</span>{% endfor %}{% if let Some(m) = item.milestone %} <span class="kg-meta">milestone: {{ m.title }}</span>{% endif %}</span><span class="kg-meta">{{ item.pull.updated_at.format("%Y-%m-%d") }}</span></li>{% endfor %}</ul>{% endif %}
+
+  {% if pulls.is_empty() %}
+    <p class="kg-muted">No {{ state_filter }} pulls.</p>
+  {% else %}
+    <ul class="kg-list">
+      {% for pull in pulls %}
+      <li>
+        <span>
+          <span class="kg-badge {% if pull.state == "open" %}kg-badge--open{% else %}kg-badge--closed{% endif %}">{{ pull.state }}</span>
+          <a href="/{{ owner.username }}/{{ repo.name }}/pulls/{{ pull.number }}">#{{ pull.number }} {{ pull.title }}</a>
+          <span class="kg-meta">{{ pull.source_branch }} → {{ pull.target_branch }}</span>
+        </span>
+        <span class="kg-meta">{{ pull.updated_at }}</span>
+      </li>
+      {% endfor %}
+    </ul>
+  {% endif %}
 </section>
 {% endblock %}
templates/repo_blame.html +0 −66
diff --git a/templates/repo_blame.html b/templates/repo_blame.html
deleted file mode 100644
index e3f291d..0000000
--- a/templates/repo_blame.html
+++ /dev/null
@@ -1,66 +0,0 @@
-{% extends "layout.html" %}
-
-{% block title %}blame · {{ path }} · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title">
-    <a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a>
-    · blame
-  </h1>
-  {% let tab = "code" %}
-  {% let archive_ref = branch %}
-  {% include "partials/repo_tabs.html" %}
-
-  <p class="kg-meta kg-breadcrumbs">
-    <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}">{{ branch }}</a>
-    {% for (name, full) in breadcrumbs %}
-      /
-      {% if loop.last %}
-        {{ name }}
-      {% else %}
-        <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}/{{ full }}">{{ name }}</a>
-      {% endif %}
-    {% endfor %}
-  </p>
-
-  <div class="kg-blobbar">
-    <span class="kg-meta">{{ path }}</span>
-    <div class="kg-blobbar__actions">
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blob/{{ branch }}/{{ path }}">View file</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/history/{{ branch }}/{{ path }}">History</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/raw/{{ branch }}/{{ path }}">
-        <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
-        Download raw
-      </a>
-    </div>
-  </div>
-
-  {% if binary %}
-    <p class="kg-muted">Binary file — blame is not available.</p>
-  {% else if lines.is_empty() %}
-    <p class="kg-muted">Empty file.</p>
-  {% else %}
-    <div class="kg-blame" role="table" aria-label="Blame for {{ path }}">
-      <div class="kg-blame__head" role="row">
-        <span class="kg-blame__meta" role="columnheader">Commit</span>
-        <span class="kg-blame__num" role="columnheader">#</span>
-        <span class="kg-blame__code" role="columnheader">Line</span>
-      </div>
-      {% for line in lines %}
-      <div class="kg-blame__row{% if line.hunk_start %} kg-blame__row--hunk{% endif %}" role="row">
-        <div class="kg-blame__meta" role="cell">
-          {% if line.hunk_start && !line.commit_id.is_empty() %}
-            <a class="kg-blame__sha" href="/{{ owner.username }}/{{ repo.name }}/commit/{{ line.commit_id }}" title="{{ line.summary }}">{{ line.short_id }}</a>
-            <span class="kg-blame__author" title="{{ line.summary }}">{{ line.author }}</span>
-            <span class="kg-blame__date">{{ line.time_display }}</span>
-          {% endif %}
-        </div>
-        <span class="kg-blame__num" role="cell">{{ line.line_no }}</span>
-        <pre class="kg-blame__code" role="cell">{{ line.content }}</pre>
-      </div>
-      {% endfor %}
-    </div>
-  {% endif %}
-</section>
-{% endblock %}
templates/repo_blob.html +0 −2
diff --git a/templates/repo_blob.html b/templates/repo_blob.html
index f00dbf7..5b0ff48 100644
--- a/templates/repo_blob.html
+++ b/templates/repo_blob.html
@@ -33,8 +33,6 @@
   <div class="kg-blobbar">
     <span class="kg-meta">{{ path }}</span>
     <div class="kg-blobbar__actions">
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blame/{{ branch }}/{{ path }}">Blame</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/history/{{ branch }}/{{ path }}">History</a>
       <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/raw/{{ branch }}/{{ path }}">
         <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
         Download raw
templates/repo_branches.html +4 −8
diff --git a/templates/repo_branches.html b/templates/repo_branches.html
index 03f22f1..5691acd 100644
--- a/templates/repo_branches.html
+++ b/templates/repo_branches.html
@@ -61,15 +61,13 @@
           <td>
             {% if access.can_write() && !b.is_default %}
             <div class="kg-branch-actions">
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/rename" style="display:inline-flex;gap:0.35rem">
-                <input type="hidden" name="branch" value="{{ b.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/{{ b.name }}/rename" style="display:inline-flex;gap:0.35rem">
                 <input type="text" name="new_name" placeholder="rename" required style="width:7rem;font:inherit;border:1px solid var(--kg-line);padding:0.25em 0.4em" />
                 <button class="kg-btn kg-btn--ghost" type="submit" title="Rename">
                   <span class="kg-icon" style="--icon:url('/static/icons/pencil.svg')"></span>
                 </button>
               </form>
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/delete" style="display:inline" onsubmit="return confirm('Delete branch {{ b.name }}?')">
-                <input type="hidden" name="branch" value="{{ b.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/branches/{{ b.name }}/delete" style="display:inline" onsubmit="return confirm('Delete branch {{ b.name }}?')">
                 <button class="kg-btn kg-btn--danger-ghost" type="submit" title="Delete">
                   <span class="kg-icon" style="--icon:url('/static/icons/trash.svg')"></span>
                 </button>
@@ -141,15 +139,13 @@
           <td>
             {% if access.can_write() %}
             <div class="kg-branch-actions">
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/rename" style="display:inline-flex;gap:0.35rem">
-                <input type="hidden" name="tag" value="{{ t.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/{{ t.name }}/rename" style="display:inline-flex;gap:0.35rem">
                 <input type="text" name="new_name" placeholder="rename" required style="width:7rem;font:inherit;border:1px solid var(--kg-line);padding:0.25em 0.4em" />
                 <button class="kg-btn kg-btn--ghost" type="submit" title="Rename">
                   <span class="kg-icon" style="--icon:url('/static/icons/pencil.svg')"></span>
                 </button>
               </form>
-              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/delete" style="display:inline" onsubmit="return confirm('Delete tag {{ t.name }}?')">
-                <input type="hidden" name="tag" value="{{ t.name }}" />
+              <form method="post" action="/{{ owner.username }}/{{ repo.name }}/tags/{{ t.name }}/delete" style="display:inline" onsubmit="return confirm('Delete tag {{ t.name }}?')">
                 <button class="kg-btn kg-btn--danger-ghost" type="submit" title="Delete">
                   <span class="kg-icon" style="--icon:url('/static/icons/trash.svg')"></span>
                 </button>
templates/repo_commit.html +14 −12
diff --git a/templates/repo_commit.html b/templates/repo_commit.html
index 3a965a9..0b1244e 100644
--- a/templates/repo_commit.html
+++ b/templates/repo_commit.html
@@ -15,20 +15,22 @@
   <p><code>{{ commit.id }}</code></p>
   <p class="kg-title">
     {{ message_html|safe }}
-    {% let c = commit %}
-    {% let show_unverified = true %}
-    {% include "partials/verified_badge.html" %}
+    {% if commit.verified %}
+    <details class="kg-verify">
+      <summary class="kg-badge kg-badge--verified">Verified</summary>
+      <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
+        <p class="kg-verify__fp">
+          <strong>{{ commit.verify_fingerprint_label }}:</strong>
+          <code>{{ commit.verify_fingerprint }}</code>
+        </p>
+        <p class="kg-meta">Verified on {{ commit.verified_at }}</p>
+      </div>
+    </details>
+    {% endif %}
   </p>
-  <p class="kg-meta">{{ commit.author }} &lt;{{ commit.email }}&gt; · {{ commit.time_display() }}</p>
+  <p class="kg-meta">{{ commit.author }} &lt;{{ commit.email }}&gt; · {{ commit.time }}</p>
 
-  <p><a href="/{{ owner.username }}/{{ repo.name }}/diff/{{ commit.id }}{% if show_full %}?full=1{% endif %}">view full diff</a></p>
+  <p><a href="/{{ owner.username }}/{{ repo.name }}/diff/{{ commit.id }}">view full diff</a></p>
   {{ diff_html|safe }}
-  {% if truncated && !show_full %}
-  <p class="kg-diff__truncated">
-    Large diffs are not rendered by default.
-    Showing the first 50 of {{ total_lines }} lines.
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/commit/{{ commit.id }}?full=1">Show full diff</a>
-  </p>
-  {% endif %}
 </section>
 {% endblock %}
templates/repo_commits.html +13 −3
diff --git a/templates/repo_commits.html b/templates/repo_commits.html
index 517248d..a763fd2 100644
--- a/templates/repo_commits.html
+++ b/templates/repo_commits.html
@@ -31,11 +31,21 @@
             <code>{{ c.short_id }}</code>
           </a>
           <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}">{{ c.message }}</a>
-          {% let show_unverified = false %}
-          {% include "partials/verified_badge.html" %}
+          {% if c.verified %}
+          <details class="kg-verify">
+            <summary class="kg-badge kg-badge--verified">Verified</summary>
+            <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
+              <p class="kg-verify__fp">
+                <strong>{{ c.verify_fingerprint_label }}:</strong>
+                <code>{{ c.verify_fingerprint }}</code>
+              </p>
+              <p class="kg-meta">Verified on {{ c.verified_at }}</p>
+            </div>
+          </details>
+          {% endif %}
           <span class="kg-muted"> — {{ c.author }}</span>
         </span>
-        <span class="kg-meta">{{ c.time_display() }}</span>
+        <span class="kg-meta">{{ c.time }}</span>
       </li>
       {% endfor %}
     </ul>
templates/repo_diff.html +1 −11
diff --git a/templates/repo_diff.html b/templates/repo_diff.html
index 122515a..129dab6 100644
--- a/templates/repo_diff.html
+++ b/templates/repo_diff.html
@@ -20,19 +20,9 @@
     · {{ commit.author }}
   </p>
   <p class="kg-title">
-    <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ commit.id }}{% if show_full %}?full=1{% endif %}">{{ commit.message }}</a>
-    {% let c = commit %}
-    {% let show_unverified = true %}
-    {% include "partials/verified_badge.html" %}
+    <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ commit.id }}">{{ commit.message }}</a>
   </p>
 
   {{ diff_html|safe }}
-  {% if truncated && !show_full %}
-  <p class="kg-diff__truncated">
-    Large diffs are not rendered by default.
-    Showing the first 50 of {{ total_lines }} lines.
-    <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/diff/{{ commit.id }}?full=1">Show full diff</a>
-  </p>
-  {% endif %}
 </section>
 {% endblock %}
templates/repo_history.html +0 −67
diff --git a/templates/repo_history.html b/templates/repo_history.html
deleted file mode 100644
index d8e95b6..0000000
--- a/templates/repo_history.html
+++ /dev/null
@@ -1,67 +0,0 @@
-{% extends "layout.html" %}
-
-{% block title %}history · {{ path }} · {{ owner.username }}/{{ repo.name }} — kitgit{% endblock %}
-
-{% block content %}
-<section class="kg-section">
-  <h1 class="kg-title">
-    <a href="/{{ owner.username }}/{{ repo.name }}">{{ owner.username }}/{{ repo.name }}</a>
-    · history
-  </h1>
-  {% let tab = "code" %}
-  {% let archive_ref = branch %}
-  {% include "partials/repo_tabs.html" %}
-
-  <p class="kg-meta kg-breadcrumbs">
-    <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}">{{ branch }}</a>
-    {% for (name, full) in breadcrumbs %}
-      /
-      {% if loop.last %}
-        {{ name }}
-      {% else %}
-        <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}/{{ full }}">{{ name }}</a>
-      {% endif %}
-    {% endfor %}
-  </p>
-
-  <div class="kg-blobbar">
-    <span class="kg-meta">{{ path }}</span>
-    <div class="kg-blobbar__actions">
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blob/{{ branch }}/{{ path }}">View file</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/blame/{{ branch }}/{{ path }}">Blame</a>
-    </div>
-  </div>
-
-  {% if commits.is_empty() %}
-    <p class="kg-muted">No commits touch this path.</p>
-  {% else %}
-    <ul class="kg-list">
-      {% for c in commits %}
-      <li>
-        <span>
-          <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}" title="View commit">
-            <span class="kg-icon" style="--icon:url('/static/icons/git-commit.svg')"></span>
-            <code>{{ c.short_id }}</code>
-          </a>
-          <a href="/{{ owner.username }}/{{ repo.name }}/commit/{{ c.id }}">{{ c.message }}</a>
-          {% if c.verified %}
-          <details class="kg-verify">
-            <summary class="kg-badge kg-badge--verified">Verified</summary>
-            <div class="kg-verify__panel" role="dialog" aria-label="Commit signature">
-              <p class="kg-verify__fp">
-                <strong>{{ c.verify_fingerprint_label }}:</strong>
-                <code>{{ c.verify_fingerprint }}</code>
-              </p>
-              <p class="kg-meta">Verified on {{ c.verified_at }}</p>
-            </div>
-          </details>
-          {% endif %}
-          <span class="kg-muted"> — {{ c.author }}</span>
-        </span>
-        <span class="kg-meta">{{ c.time_display() }}</span>
-      </li>
-      {% endfor %}
-    </ul>
-  {% endif %}
-</section>
-{% endblock %}
templates/repo_home.html +2 −4
diff --git a/templates/repo_home.html b/templates/repo_home.html
index f2e3424..7ee008a 100644
--- a/templates/repo_home.html
+++ b/templates/repo_home.html
@@ -104,8 +104,7 @@
         {% if e.is_dir %}
           <span class="kg-icon" style="--icon:url('/static/icons/folder.svg')"></span>
           <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ current_branch }}/{{ e.path }}">{{ e.name }}</a>
-          <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-          <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+          <span class="kg-meta">{{ e.mode }}</span>
           <a class="kg-tree__dl kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/archive.zip?ref={{ current_branch }}&path={{ e.path }}" title="Download {{ e.name }} as ZIP">
             <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
             <span class="kg-sr-only">Download</span>
@@ -113,8 +112,7 @@
         {% else %}
           <span class="kg-icon" style="--icon:url('/static/icons/file.svg')"></span>
           <a href="/{{ owner.username }}/{{ repo.name }}/blob/{{ current_branch }}/{{ e.path }}">{{ e.name }}</a>
-          <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-          <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+          <span class="kg-meta">{{ e.mode }}</span>
         {% endif %}
       </li>
       {% endfor %}
templates/repo_settings.html +2 −176
diff --git a/templates/repo_settings.html b/templates/repo_settings.html
index e6ae11e..effe71d 100644
--- a/templates/repo_settings.html
+++ b/templates/repo_settings.html
@@ -146,73 +146,6 @@
   </div>
 
   <div class="kg-settings-section">
-    <span class="kg-kicker">mirror</span>
-    <p class="kg-muted">Pull from another remote into this repository (fetch heads and tags).</p>
-    {% if let Some(m) = mirror %}
-      <p class="kg-meta" style="margin:0.5rem 0">
-        {% if m.enabled %}
-          <span class="kg-badge">enabled</span>
-        {% else %}
-          <span class="kg-badge">disabled</span>
-        {% endif %}
-        {% if let Some(synced) = m.last_synced_at %}
-          · last synced {{ synced }}
-        {% else %}
-          · never synced
-        {% endif %}
-      </p>
-      {% if let Some(err) = m.last_error %}
-        <div class="kg-flash">{{ err }}</div>
-      {% endif %}
-      <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror" style="margin-top:1rem">
-        <label>
-          remote URL
-          <input type="text" name="remote_url" required value="{{ m.remote_url }}" placeholder="https://github.com/org/repo.git" />
-        </label>
-        <label class="row">
-          <input type="checkbox" name="enabled"{% if m.enabled %} checked{% endif %} />
-          enabled
-        </label>
-        <div class="kg-actions">
-          <button class="kg-btn" type="submit">save mirror</button>
-        </div>
-      </form>
-      <div class="kg-actions" style="margin-top:0.75rem;gap:0.5rem;display:flex;flex-wrap:wrap">
-        <form method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror/sync">
-          <button class="kg-btn kg-btn--ghost" type="submit">sync now</button>
-        </form>
-        <form method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror/delete">
-          <button class="kg-btn kg-btn--danger-ghost" type="submit">remove mirror</button>
-        </form>
-      </div>
-    {% else %}
-      <p class="kg-muted">No mirror configured.</p>
-      <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/mirror" style="margin-top:1rem">
-        <label>
-          remote URL
-          <input type="text" name="remote_url" required placeholder="https://github.com/org/repo.git" />
-        </label>
-        <label class="row">
-          <input type="checkbox" name="enabled" checked />
-          enabled
-        </label>
-        <div class="kg-actions">
-          <button class="kg-btn" type="submit">save mirror</button>
-        </div>
-      </form>
-    {% endif %}
-  </div>
-
-  <div class="kg-settings-section">
-    <span class="kg-kicker">labels & milestones</span>
-    <p class="kg-muted">Organize issues and pull requests.</p>
-    <div class="kg-actions">
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/labels">manage labels</a>
-      <a class="kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/milestones">manage milestones</a>
-    </div>
-  </div>
-
-  <div class="kg-settings-section">
     <span class="kg-kicker">collaborators</span>
     {% if collaborators.is_empty() %}
       <p class="kg-muted">No collaborators.</p>
@@ -252,113 +185,6 @@
     </form>
   </div>
 
-  <div class="kg-settings-section">
-    <span class="kg-kicker">deploy keys</span>
-    <p class="kg-muted">Repo-scoped SSH keys for CI and automation. Read-only by default; write access allows push to this repository only.</p>
-    {% if deploy_keys.is_empty() %}
-      <p class="kg-muted">No deploy keys.</p>
-    {% else %}
-      <ul class="kg-list">
-        {% for key in deploy_keys %}
-        <li>
-          <span>
-            <strong>{{ key.name }}</strong>
-            <span class="kg-meta">{{ key.fingerprint }}</span>
-            <span class="kg-badge">{{ key.permission_label() }}</span>
-          </span>
-          <form method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/deploy-keys/{{ key.id }}/delete">
-            <button class="kg-btn kg-btn--danger-ghost" type="submit">delete</button>
-          </form>
-        </li>
-        {% endfor %}
-      </ul>
-    {% endif %}
-
-    <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/deploy-keys" style="margin-top:1rem">
-      <label>
-        name
-        <input type="text" name="name" required placeholder="ci" />
-      </label>
-      <label>
-        public key
-        <textarea name="public_key" rows="3" required placeholder="ssh-ed25519 AAAA…"></textarea>
-      </label>
-      <label class="row">
-        <input type="checkbox" name="write_access" />
-        allow write access (push)
-      </label>
-      <div class="kg-actions">
-        <button class="kg-btn" type="submit">add deploy key</button>
-      </div>
-    </form>
-  </div>
-
-  <div class="kg-settings-section">
-    <span class="kg-kicker">webhooks</span>
-    <p class="kg-muted">HTTP callbacks for repository events. Optional secret signs payloads as <code>X-Hub-Signature-256</code>.</p>
-    {% if webhooks.is_empty() %}
-      <p class="kg-muted">No webhooks yet.</p>
-    {% else %}
-      <ul class="kg-list">
-        {% for hook in webhooks %}
-        <li>
-          <span>
-            <code>{{ hook.url }}</code>
-            <span class="kg-meta">{{ hook.events_label }}</span>
-            {% if hook.has_secret %}<span class="kg-badge">signed</span>{% endif %}
-            {% if hook.active %}<span class="kg-badge">active</span>{% else %}<span class="kg-badge">inactive</span>{% endif %}
-          </span>
-          <span class="kg-actions" style="gap:0.35rem">
-            <form method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/webhooks/{{ hook.id }}/toggle">
-              <button class="kg-btn kg-btn--ghost" type="submit">{% if hook.active %}disable{% else %}enable{% endif %}</button>
-            </form>
-            <form method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/webhooks/{{ hook.id }}/delete">
-              <button class="kg-btn kg-btn--danger-ghost" type="submit">delete</button>
-            </form>
-          </span>
-        </li>
-        {% endfor %}
-      </ul>
-    {% endif %}
-    <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/webhooks" style="margin-top:1rem">
-      <label>
-        payload URL
-        <input type="url" name="url" required placeholder="https://example.com/hooks/kitgit" />
-      </label>
-      <label>
-        secret <span class="kg-muted">(optional)</span>
-        <input type="text" name="secret" placeholder="HMAC secret" autocomplete="off" />
-      </label>
-      <fieldset style="border:0;padding:0;margin:0">
-        <legend class="kg-muted" style="margin-bottom:0.35rem">events</legend>
-        <label class="row"><input type="checkbox" name="event_push" checked /> push</label>
-        <label class="row"><input type="checkbox" name="event_issues" checked /> issues</label>
-        <label class="row"><input type="checkbox" name="event_pull_request" checked /> pull_request</label>
-        <label class="row"><input type="checkbox" name="event_release" checked /> release</label>
-      </fieldset>
-      <div class="kg-actions">
-        <button class="kg-btn" type="submit">add webhook</button>
-      </div>
-    </form>
-    {% if !webhook_deliveries.is_empty() %}
-      <div style="margin-top:1.25rem">
-        <span class="kg-kicker">recent deliveries</span>
-        <ul class="kg-list">
-          {% for d in webhook_deliveries %}
-          <li>
-            <span>
-              <strong>{{ d.event }}</strong>{% if !d.action.is_empty() %}.{{ d.action }}{% endif %}
-              <span class="kg-badge">{% if d.success %}ok{% else %}fail{% endif %}</span>
-              <span class="kg-meta">{{ d.status_label }} · {{ d.created_at }}</span>
-              <span class="kg-meta">{{ d.webhook_url }}</span>
-            </span>
-          </li>
-          {% endfor %}
-        </ul>
-      </div>
-    {% endif %}
-  </div>
-
   {% if access.can_owner() || is_site_admin %}
     <div class="kg-danger-zone">
       <span class="kg-kicker">danger zone</span>
@@ -385,8 +211,8 @@
 
       <form class="kg-form" method="post" action="/{{ owner.username }}/{{ repo.name }}/settings/danger/delete">
         <div class="kg-danger-row">
-          <label class="kg-danger-confirm">
-            <span>type <code>{{ repo.name }}</code> to delete</span>
+          <label>
+            type <code>{{ repo.name }}</code> to delete
             <input type="text" name="confirm" required autocomplete="off" />
           </label>
           <button class="kg-btn kg-btn--danger" type="submit">delete repository</button>
templates/repo_tree.html +2 −4
diff --git a/templates/repo_tree.html b/templates/repo_tree.html
index 3124b29..9a27884 100644
--- a/templates/repo_tree.html
+++ b/templates/repo_tree.html
@@ -27,8 +27,7 @@
       {% if e.is_dir %}
         <span class="kg-icon" style="--icon:url('/static/icons/folder.svg')"></span>
         <a href="/{{ owner.username }}/{{ repo.name }}/tree/{{ branch }}/{{ e.path }}">{{ e.name }}</a>
-        <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-        <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+        <span class="kg-meta">{{ e.mode }}</span>
         <a class="kg-tree__dl kg-btn kg-btn--ghost" href="/{{ owner.username }}/{{ repo.name }}/archive.zip?ref={{ branch }}&path={{ e.path }}" title="Download {{ e.name }} as ZIP">
           <span class="kg-icon" style="--icon:url('/static/icons/download.svg')"></span>
           <span class="kg-sr-only">Download</span>
@@ -36,8 +35,7 @@
       {% else %}
         <span class="kg-icon" style="--icon:url('/static/icons/file.svg')"></span>
         <a href="/{{ owner.username }}/{{ repo.name }}/blob/{{ branch }}/{{ e.path }}">{{ e.name }}</a>
-        <span class="kg-tree__commit kg-meta" title="{{ e.commit_message }}">{{ e.commit_message }}</span>
-        <span class="kg-tree__time kg-meta">{{ e.commit_time }}</span>
+        <span class="kg-meta">{{ e.mode }}</span>
       {% endif %}
     </li>
     {% endfor %}
templates/signup.html +14 −8
diff --git a/templates/signup.html b/templates/signup.html
index 9ed43de..5eceaf6 100644
--- a/templates/signup.html
+++ b/templates/signup.html
@@ -5,13 +5,7 @@
 {% block content %}
 <section class="kg-section kg-auth">
   {% if !signups_enabled %}
-    <div class="kg-flash kg-flash--banner">
-      {% if invite.is_empty() %}
-        {{ signup_disabled_message }}
-      {% else %}
-        Invite-only signup — your invite code unlocks registration.
-      {% endif %}
-    </div>
+    <div class="kg-flash kg-flash--banner">{{ signup_disabled_message }}</div>
   {% endif %}
   <p class="kg-landing__brand">
     <span class="kg-mark__glyph" aria-hidden="true">狐</span>
@@ -21,11 +15,12 @@
   {% if let Some(err) = error %}
     <div class="kg-flash">{{ err }}</div>
   {% endif %}
+  {% if signups_enabled || !invite.is_empty() %}
   <form class="kg-form" method="post" action="/auth/signup" autocomplete="on">
     {% if !signups_enabled %}
     <label>
       invite code
-      <input type="text" name="invite" required value="{{ invite }}" autocomplete="off" spellcheck="false" placeholder="Enter invite code" />
+      <input type="text" name="invite" required value="{{ invite }}" autocomplete="off" spellcheck="false" />
     </label>
     {% endif %}
     <label>
@@ -48,6 +43,17 @@
       <button class="kg-btn" type="submit">create account</button>
     </div>
   </form>
+  {% else %}
+  <form class="kg-form" method="get" action="/auth/signup" style="margin-top:1rem">
+    <label>
+      invite code
+      <input type="text" name="invite" required autocomplete="off" spellcheck="false" placeholder="Enter an invite code" />
+    </label>
+    <div class="kg-actions">
+      <button class="kg-btn" type="submit">continue</button>
+    </div>
+  </form>
+  {% endif %}
   <p class="kg-meta" style="margin-top:1.25rem">
     Already have an account? <a href="/auth/login">Log in</a>
   </p>