feat: make Settings a read-only projection over the typed meta refs
commit
013aea2feat: make Settings a read-only projection over the typed meta refs
Settings now reads real values from refs/meta/config (General, topics),
refs/meta/members (Members), and refs/meta/checks (Checks), and shows
derived feature status instead of fake switches. Drop the stub Visibility
radios and Danger-zone buttons, which backed no data, and their dead CSS.
feat: project config/members/checks into the Settings page refactor: show derived feature status instead of stub toggles Assisted-by: Claude:claude-opus-4-8
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-ents-server/src/web/pages.rs
@@ -814,12 +814,13 @@
.map_err(|err| err.to_string())
}
-/// The Settings tab. Persisting changes needs a config store that does not
-/// exist yet, so the controls reflect the repository's current real values and
-/// are presented read-only.
+/// The Settings tab: a read-only projection over the repository's typed meta
+/// refs — `refs/meta/config` (General), `refs/meta/members` (Members), and the
+/// derived feature and check status. Editing is a members-gated write path that
+/// does not exist yet, so the values are presented as the current configuration.
pub(super) async fn settings_page(repo: &Path, meta: &RepoMeta) -> Markup {
- let name = meta.name();
let signers = load_signers(repo).await;
+ let checks = load_checks(repo).await;
repo_shell(
meta,
Tab::Settings,
@@ -827,21 +828,28 @@
html! {
div.settings {
div.page-header { h1.page-title { "Repository settings" } }
- p.shell-note { "These reflect the repository's current configuration." }
+ p.shell-note {
+ "The repository's configuration on " code { "refs/meta/config" }
+ " and " code { "refs/meta/members" } "."
+ }
div.card {
div.card-header { "General" }
- div.field {
- label { "Repository name" }
- input type="text" value=(name) disabled title="Not editable yet";
- }
- div.field {
- label { "Description" }
- textarea rows="2" disabled title="Not editable yet" { (meta.description.as_deref().unwrap_or_default()) }
- }
- div.field {
- label { "Default branch" }
- input type="text" value=(meta.branch.as_deref().unwrap_or("—")) disabled title="Not editable yet";
+ (setting_row("Repository name", meta.name()))
+ (setting_row("Description", meta.description.as_deref().unwrap_or("—")))
+ (setting_row("Homepage", meta.homepage.as_deref().unwrap_or("—")))
+ (setting_row("Default branch", meta.branch.as_deref().unwrap_or("—")))
+ div.card-row {
+ span.setting-label { "Topics" }
+ @if meta.topics.is_empty() {
+ span.muted { "—" }
+ } @else {
+ div.topics {
+ @for topic in &meta.topics {
+ span.topic { (topic) }
+ }
+ }
+ }
}
}
@@ -849,8 +857,7 @@
div.card-header { "Features" }
(feature_row("Bug reports", "Track and triage bugs.", meta.issues > 0))
(feature_row("Releases", "Publish tagged releases.", meta.releases > 0))
- (feature_row("Checks (CI)", "Run signed CI records on push.", false))
- (feature_row("Wiki", "A separate documentation space.", false))
+ (feature_row("Checks (CI)", "Run signed CI records on push.", matches!(&checks, Ok(c) if !c.is_empty())))
}
div.card {
@@ -878,26 +885,24 @@
}
div.card {
- div.card-header { "Visibility" }
- (visibility_row("Public", "Anyone can read this repository.", true))
- (visibility_row("Private", "Only collaborators can read it.", false))
- }
-
- div.card.danger {
- div.card-header { "Danger zone" }
- div.danger-row {
- div {
- strong { "Archive this repository" }
- p.muted { "Make it read-only." }
- }
- button.btn-danger-outline type="button" disabled title="Not available yet" { "Archive" }
+ div.card-header {
+ "Checks"
+ @if let Ok(checks) = &checks { span.count { (checks.len()) } }
}
- div.danger-row {
- div {
- strong { "Delete this repository" }
- p.muted { "This cannot be undone." }
+ p.shell-note {
+ "Commands on " code { "refs/meta/checks" } " run against each push "
+ "(" code { "git ents checks list" } ")."
+ }
+ @match &checks {
+ Err(err) => div.card-row.muted { "Could not read checks: " (err) }
+ Ok(checks) if checks.is_empty() => {
+ div.card-row.muted { "No checks configured." }
+ }
+ Ok(checks) => {
+ @for check in checks {
+ (check.render())
+ }
}
- button.btn-danger type="button" disabled title="Not available yet" { "Delete" }
}
}
}
@@ -915,7 +920,18 @@
.map_err(|err| err.to_string())
}
-/// A Features row with a static toggle reflecting `on`.
+/// A read-only setting row: a label and its current value.
+fn setting_row(label: &str, value: &str) -> Markup {
+ html! {
+ div.card-row {
+ span.setting-label { (label) }
+ span.muted { (value) }
+ }
+ }
+}
+
+/// A Features row showing the derived, read-only status of `title`: active when
+/// the feature has backing data, empty otherwise.
fn feature_row(title: &str, desc: &str, on: bool) -> Markup {
html! {
div.feature-row {
@@ -923,20 +939,7 @@
strong { (title) }
p.muted { (desc) }
}
- span.toggle.on[on].stub title="Not editable yet" { span.knob {} }
- }
- }
-}
-
-/// A Visibility radio row, selected when `on`.
-fn visibility_row(title: &str, desc: &str, on: bool) -> Markup {
- html! {
- div.visibility-row.sel[on] {
- span.radio.on[on].stub title="Not editable yet" {}
- div {
- strong { (title) }
- p.muted { (desc) }
- }
+ span.feature-status.on[on] { @if on { "Active" } @else { "Empty" } }
}
}
}
crates/git-ents-server/src/web/style.css
@@ -323,33 +323,13 @@
.subtab.active .icon { color: var(--s-func); }
.settings { max-width: 46rem; }
-.field { padding: .85rem 1.1rem; }
-.field + .field { border-top: 1px solid var(--color-border); }
-.field label { display: block; font-size: .82rem; font-weight: 600; margin-bottom: .35rem; }
-.field input, .field textarea, .field select { width: 100%; font-family: var(--font-sans); font-size: .9rem; color: var(--color-text); background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 8px; padding: .5rem .7rem; resize: vertical; }
-.field input:focus, .field textarea:focus { outline: none; border-color: var(--color-accent); }
-.field input[readonly], .field textarea[readonly] { color: var(--color-text-muted); }
-.feature-row, .danger-row { display: flex; align-items: center; gap: 1rem; padding: .85rem 1.1rem; }
-.feature-row + .feature-row, .danger-row + .danger-row { border-top: 1px solid var(--color-border); }
-.feature-row > div:first-child, .danger-row > div:first-child { flex: 1; }
-.feature-row p, .danger-row p { font-size: .82rem; margin: .1rem 0 0; }
-.toggle { flex-shrink: 0; position: relative; width: 42px; height: 24px; border-radius: var(--radius-pill); background: var(--color-border); transition: background .18s; }
-.toggle .knob { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left .18s; }
-.toggle.on { background: var(--color-accent); }
-.toggle.on .knob { left: 21px; }
-.visibility-row { display: flex; align-items: flex-start; gap: .7rem; padding: .85rem 1.1rem; }
-.visibility-row + .visibility-row { border-top: 1px solid var(--color-border); }
-.visibility-row.sel { background: var(--color-accent-subtle); }
-.visibility-row p { font-size: .82rem; margin: .1rem 0 0; }
-.radio { flex-shrink: 0; margin-top: .2rem; width: 16px; height: 16px; border-radius: 50%; border: 1px solid var(--color-border); position: relative; }
-.radio.on { border-color: var(--color-accent); }
-.radio.on::after { content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--color-accent); }
-.card.danger { border-color: var(--s-keyword); }
-.card.danger .card-header { color: var(--s-keyword); }
-.btn-danger-outline, .btn-danger { font-family: var(--font-sans); font-size: .82rem; font-weight: 600; border-radius: var(--radius-sm); padding: .4rem .9rem; cursor: pointer; flex-shrink: 0; }
-.btn-danger-outline { color: var(--s-keyword); background: transparent; border: 1px solid var(--s-keyword); }
-.btn-danger-outline:hover { background: var(--diff-del); }
-.btn-danger { color: #fff; background: var(--s-keyword); border: none; }
+.setting-label { flex-shrink: 0; min-width: 11rem; font-weight: 600; font-family: var(--font-sans); }
+.feature-row { display: flex; align-items: center; gap: 1rem; padding: .85rem 1.1rem; }
+.feature-row + .feature-row { border-top: 1px solid var(--color-border); }
+.feature-row > div:first-child { flex: 1; }
+.feature-row p { font-size: .82rem; margin: .1rem 0 0; }
+.feature-status { flex-shrink: 0; font-family: var(--font-mono); font-size: .76rem; color: var(--color-text-muted); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-pill); padding: .2rem .7rem; }
+.feature-status.on { color: var(--color-bg); background: var(--color-accent); border-color: var(--color-accent); }
.site-footer { border-top: 1px solid var(--color-border); color: var(--color-text-muted); font-size: .8rem; margin-top: auto; }
.footer-inner { max-width: var(--max-width); margin: 0 auto; padding: 2rem 1.5rem; text-align: center; }