forge: auto-set agent-configs org avatar on core start (#424)
Sibling to ensure_core_avatar (#320). Same one-shot marker-guarded upload pattern, this time aimed at the Forgejo per-org avatar endpoint (`POST /api/v1/orgs/{org}/avatar`). ## SVG-to-PNG at build time Mara: *don't check in the png. instead generate png on the fly or in build.* `hive-c0re/build.rs` renders `branding/agent-configs.svg` → `$OUT_DIR/agent-configs.png` via `rsvg-convert` (librsvg) on every compile; `forge.rs` then `include_bytes!`s the OUT_DIR PNG. The raster never gets checked into git — SVG stays source of truth, the PNG is a build artifact. - `hive-c0re/Cargo.toml`: declares `build = "build.rs"` - `flake.nix`: adds `librsvg` to `naersk-lib.buildPackage` `nativeBuildInputs` (covers both the runtime package and the clippy check derivation) and to the dev shell so local `cargo build` finds `rsvg-convert` on PATH. - For dev builds outside Nix, install librsvg (Debian: `librsvg2-bin`, macOS: `brew install librsvg`). ## Icon design Sibling visual to the main hyperhive mark — same dark base + outer ring + corner-bracket frame so the family reads at a glance. Centre swaps the hexagonal hive for a stacked-config-files motif: three offset sheets, folded-corner affordance, curly-brace `{ }` glyph telegraphing "config file." Brace font-size dropped 78→56 + letter-spacing -3 (#424 mara: "braces cross the boundaries of the page") so the glyphs sit comfortably inside the 120-wide front sheet with clear breathing room on the left/right edges. The stack is shifted so the front sheet centres on canvas-(150, 150); brace text anchors there with `dominant-baseline=central` for true vertical centring. ## Validation - `cargo check` clean (only pre-existing warnings). - One-shot marker honoured: re-runs of `ensure_all` skip after the first success; `rm /var/lib/hyperhive/forge-agent-configs-avatar-set` forces re-upload (useful for icon revisions). - Behaviour mirrors the existing `ensure_core_avatar` pattern. Browser smoke test isn't possible from inside iris's container. Worth eyeballing post-deploy: `http://localhost:3000/agent-configs` should show the new avatar where the default identicon used to be.
This commit is contained in:
parent
57a753d80a
commit
a444774ab3
5 changed files with 225 additions and 0 deletions
116
branding/agent-configs.svg
Normal file
116
branding/agent-configs.svg
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<svg width="300" height="300" viewBox="0 0 300 300" role="img" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>HyperHive · agent-configs</title>
|
||||||
|
<desc>HyperHive agent-configs org icon — stacked config files, amber on dark, same frame as the main hyperhive mark</desc>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clipH"><circle cx="150" cy="150" r="140"/></clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<g clip-path="url(#clipH)">
|
||||||
|
<!-- same dark base + faint horizontal scanlines as the parent
|
||||||
|
hyperhive mark, so the agent-configs org reads as a sibling -->
|
||||||
|
<rect x="0" y="0" width="300" height="300" fill="#0a0600"/>
|
||||||
|
<g stroke="#ffb300" stroke-width="0.35" opacity="0.07">
|
||||||
|
<line x1="0" x2="300" y1="10" y2="10"/> <line x1="0" x2="300" y1="20" y2="20"/>
|
||||||
|
<line x1="0" x2="300" y1="30" y2="30"/> <line x1="0" x2="300" y1="40" y2="40"/>
|
||||||
|
<line x1="0" x2="300" y1="50" y2="50"/> <line x1="0" x2="300" y1="60" y2="60"/>
|
||||||
|
<line x1="0" x2="300" y1="70" y2="70"/> <line x1="0" x2="300" y1="80" y2="80"/>
|
||||||
|
<line x1="0" x2="300" y1="90" y2="90"/> <line x1="0" x2="300" y1="100" y2="100"/>
|
||||||
|
<line x1="0" x2="300" y1="110" y2="110"/> <line x1="0" x2="300" y1="120" y2="120"/>
|
||||||
|
<line x1="0" x2="300" y1="130" y2="130"/> <line x1="0" x2="300" y1="140" y2="140"/>
|
||||||
|
<line x1="0" x2="300" y1="150" y2="150"/> <line x1="0" x2="300" y1="160" y2="160"/>
|
||||||
|
<line x1="0" x2="300" y1="170" y2="170"/> <line x1="0" x2="300" y1="180" y2="180"/>
|
||||||
|
<line x1="0" x2="300" y1="190" y2="190"/> <line x1="0" x2="300" y1="200" y2="200"/>
|
||||||
|
<line x1="0" x2="300" y1="210" y2="210"/> <line x1="0" x2="300" y1="220" y2="220"/>
|
||||||
|
<line x1="0" x2="300" y1="230" y2="230"/> <line x1="0" x2="300" y1="240" y2="240"/>
|
||||||
|
<line x1="0" x2="300" y1="250" y2="250"/> <line x1="0" x2="300" y1="260" y2="260"/>
|
||||||
|
<line x1="0" x2="300" y1="270" y2="270"/> <line x1="0" x2="300" y1="280" y2="280"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- containment rings, same as parent (visual family) -->
|
||||||
|
<circle cx="150" cy="150" r="118" fill="none" stroke="#ffb300" stroke-width="1" opacity="0.4"/>
|
||||||
|
<circle cx="150" cy="150" r="104" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.25" stroke-dasharray="4 6"/>
|
||||||
|
|
||||||
|
<!-- stack of three offset config sheets. each sheet is a
|
||||||
|
dark-fill rectangle outlined in amber, with thin "content
|
||||||
|
lines" inside to read as a file. offset diagonally so the
|
||||||
|
stack reads top-down: back / mid / front. front sheet is
|
||||||
|
centred on the canvas (150, 150) so the curly-brace glyph
|
||||||
|
at canvas-centre lands at the visual middle of the front
|
||||||
|
sheet; back and mid peek out top-LEFT so the stack reads
|
||||||
|
as a pile. -->
|
||||||
|
|
||||||
|
<!-- BACK sheet (x=70, y=56 — top-left peek) -->
|
||||||
|
<g opacity="0.55">
|
||||||
|
<rect x="70" y="56" width="120" height="148" rx="6" ry="6"
|
||||||
|
fill="#150c00" stroke="#ffb300" stroke-width="1.4"/>
|
||||||
|
<g stroke="#ffb300" stroke-width="0.7" opacity="0.55">
|
||||||
|
<line x1="82" y1="78" x2="164" y2="78"/>
|
||||||
|
<line x1="82" y1="94" x2="144" y2="94"/>
|
||||||
|
<line x1="82" y1="110" x2="158" y2="110"/>
|
||||||
|
<line x1="82" y1="126" x2="132" y2="126"/>
|
||||||
|
<line x1="82" y1="142" x2="164" y2="142"/>
|
||||||
|
<line x1="82" y1="158" x2="144" y2="158"/>
|
||||||
|
<line x1="82" y1="174" x2="154" y2="174"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- MID sheet (x=80, y=66) -->
|
||||||
|
<g opacity="0.8">
|
||||||
|
<rect x="80" y="66" width="120" height="148" rx="6" ry="6"
|
||||||
|
fill="#1a0f00" stroke="#ffb300" stroke-width="1.6"/>
|
||||||
|
<g stroke="#ffb300" stroke-width="0.8" opacity="0.7">
|
||||||
|
<line x1="92" y1="88" x2="174" y2="88"/>
|
||||||
|
<line x1="92" y1="104" x2="154" y2="104"/>
|
||||||
|
<line x1="92" y1="120" x2="168" y2="120"/>
|
||||||
|
<line x1="92" y1="136" x2="142" y2="136"/>
|
||||||
|
<line x1="92" y1="152" x2="174" y2="152"/>
|
||||||
|
<line x1="92" y1="168" x2="154" y2="168"/>
|
||||||
|
<line x1="92" y1="184" x2="164" y2="184"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- FRONT sheet (x=90, y=76, width=120, height=148 — centred
|
||||||
|
at canvas (150, 150)). Folded top-right corner + curly-
|
||||||
|
brace glyph. The brace text is anchored at canvas-centre
|
||||||
|
which now coincides with the front-sheet centre. -->
|
||||||
|
<g>
|
||||||
|
<!-- main rectangle with folded top-right corner cut: draw as a
|
||||||
|
path so the corner triangle reads as a tab -->
|
||||||
|
<path d="M 90 76
|
||||||
|
L 192 76
|
||||||
|
L 210 94
|
||||||
|
L 210 224
|
||||||
|
L 90 224
|
||||||
|
Z"
|
||||||
|
fill="#1f1200" stroke="#ffb300" stroke-width="1.8"/>
|
||||||
|
<!-- folded-corner triangle (lighter fill) -->
|
||||||
|
<path d="M 192 76 L 210 94 L 192 94 Z"
|
||||||
|
fill="#2a1900" stroke="#ffb300" stroke-width="1.2"/>
|
||||||
|
<!-- curly braces — the universal "config file" glyph.
|
||||||
|
dominant-baseline=central centres the glyph block on the
|
||||||
|
text y-coordinate so it lands at the front-sheet middle.
|
||||||
|
font-size dropped 78→56 + tighter letter-spacing (#424
|
||||||
|
mara: "braces cross the boundaries of the page") so the
|
||||||
|
glyphs sit comfortably inside the 120-wide sheet with
|
||||||
|
clear breathing room on the left/right edges. -->
|
||||||
|
<text x="150" y="150" text-anchor="middle" dominant-baseline="central"
|
||||||
|
font-family="ui-monospace, 'JetBrains Mono', monospace"
|
||||||
|
font-size="56" font-weight="700" letter-spacing="-3"
|
||||||
|
fill="#ffb300" opacity="0.95">{ }</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- thin centerline crosshair, same flourish as the parent -->
|
||||||
|
<rect x="0" y="143" width="300" height="2" fill="#ffb300" opacity="0.07"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- same outer frame + corner brackets as the parent hyperhive
|
||||||
|
mark so the two orgs read as a visual family -->
|
||||||
|
<circle cx="150" cy="150" r="140" fill="none" stroke="#ffb300" stroke-width="2.5"/>
|
||||||
|
<circle cx="150" cy="150" r="145" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.4" stroke-dasharray="8 4"/>
|
||||||
|
<g stroke="#ffb300" stroke-width="1.5" fill="none" opacity="0.8">
|
||||||
|
<path d="M44,44 L20,44 L20,70"/>
|
||||||
|
<path d="M256,44 L280,44 L280,70"/>
|
||||||
|
<path d="M44,256 L20,256 L20,230"/>
|
||||||
|
<path d="M256,256 L280,256 L280,230"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
12
flake.nix
12
flake.nix
|
|
@ -55,6 +55,12 @@
|
||||||
{
|
{
|
||||||
default = naersk-lib.buildPackage {
|
default = naersk-lib.buildPackage {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
# librsvg ships `rsvg-convert`, which hive-c0re/build.rs
|
||||||
|
# invokes to render branding/agent-configs.svg into the
|
||||||
|
# PNG it embeds via `include_bytes!` (#424). Keeps the
|
||||||
|
# raster out of git — SVG stays source-of-truth, PNG is
|
||||||
|
# a build artifact in $OUT_DIR.
|
||||||
|
nativeBuildInputs = [ pkgs.librsvg ];
|
||||||
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)";
|
meta.description = "hyperhive workspace (hive-c0re, hive-ag3nt, hive-m1nd)";
|
||||||
};
|
};
|
||||||
# Bundled browser assets — see ./nix/frontend.nix. Output is
|
# Bundled browser assets — see ./nix/frontend.nix. Output is
|
||||||
|
|
@ -152,6 +158,7 @@
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
cargo
|
cargo
|
||||||
clippy
|
clippy
|
||||||
|
librsvg # rsvg-convert — hive-c0re/build.rs invokes it (#424)
|
||||||
pkg-config
|
pkg-config
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
rustc
|
rustc
|
||||||
|
|
@ -183,6 +190,11 @@
|
||||||
# Skip the actual build; we only care about the clippy lint.
|
# Skip the actual build; we only care about the clippy lint.
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
copyTarget = false;
|
copyTarget = false;
|
||||||
|
# hive-c0re/build.rs needs rsvg-convert on PATH (#424);
|
||||||
|
# mirror the runtime derivation's nativeBuildInputs so
|
||||||
|
# clippy's vendored-deps build phase doesn't break on
|
||||||
|
# the missing tool.
|
||||||
|
nativeBuildInputs = [ pkgs.librsvg ];
|
||||||
}).overrideAttrs
|
}).overrideAttrs
|
||||||
(old: {
|
(old: {
|
||||||
name = "${old.name}-clippy";
|
name = "${old.name}-clippy";
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
name = "hive-c0re"
|
name = "hive-c0re"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
|
# Render branding/agent-configs.svg → $OUT_DIR/agent-configs.png at
|
||||||
|
# compile time (#424). build.rs shells out to `rsvg-convert`
|
||||||
|
# (librsvg, pulled in via flake.nix' naersk nativeBuildInputs); the
|
||||||
|
# baked PNG is included via include_bytes! from forge.rs so no
|
||||||
|
# raster gets checked into git.
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
49
hive-c0re/build.rs
Normal file
49
hive-c0re/build.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
//! Render `branding/agent-configs.svg` → `$OUT_DIR/agent-configs.png`
|
||||||
|
//! at compile time so the daemon can `include_bytes!` the PNG without
|
||||||
|
//! checking the raster into git (#424 mara: "generate png on the fly
|
||||||
|
//! or in build"). The SVG is the source of truth; the PNG is a build
|
||||||
|
//! artifact.
|
||||||
|
//!
|
||||||
|
//! Uses `rsvg-convert` from PATH (librsvg, already available in
|
||||||
|
//! nixpkgs and added to the naersk derivation's `nativeBuildInputs`
|
||||||
|
//! in `flake.nix`). For dev builds outside Nix, install librsvg via
|
||||||
|
//! your system package manager (Debian/Ubuntu: `librsvg2-bin`,
|
||||||
|
//! macOS: `brew install librsvg`).
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const SVG_PATH: &str = "../branding/agent-configs.svg";
|
||||||
|
const PNG_NAME: &str = "agent-configs.png";
|
||||||
|
// 300×300 to match the existing branding/hyperhive.png, which the
|
||||||
|
// Forgejo avatar endpoint accepts without resizing on upload.
|
||||||
|
const PX: &str = "300";
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Re-run the build script when either the SVG itself or this
|
||||||
|
// script change. We deliberately don't watch every file in
|
||||||
|
// `branding/` — only the one PNG we generate.
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
println!("cargo:rerun-if-changed={SVG_PATH}");
|
||||||
|
|
||||||
|
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR set by cargo"));
|
||||||
|
let png_path = out_dir.join(PNG_NAME);
|
||||||
|
|
||||||
|
let status = Command::new("rsvg-convert")
|
||||||
|
.args(["--width", PX, "--height", PX, "-o"])
|
||||||
|
.arg(&png_path)
|
||||||
|
.arg(SVG_PATH)
|
||||||
|
.status();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Ok(s) if s.success() => {}
|
||||||
|
Ok(s) => panic!("rsvg-convert exited with {s} rendering {SVG_PATH}"),
|
||||||
|
Err(e) => panic!(
|
||||||
|
"failed to invoke rsvg-convert: {e}\n\
|
||||||
|
install librsvg (Debian/Ubuntu: librsvg2-bin, macOS: brew install librsvg, \
|
||||||
|
NixOS: pkgs.librsvg). The Nix derivation already pulls it in via \
|
||||||
|
flake.nix → naersk-lib.buildPackage.nativeBuildInputs.",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,11 +38,23 @@ const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token";
|
||||||
/// the upload runs once, the marker is written, subsequent startups skip
|
/// the upload runs once, the marker is written, subsequent startups skip
|
||||||
/// the call. Delete to force re-upload.
|
/// the call. Delete to force re-upload.
|
||||||
const CORE_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-core-avatar-set";
|
const CORE_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-core-avatar-set";
|
||||||
|
/// Sibling marker for the `agent-configs` org avatar (#424). Same one-
|
||||||
|
/// shot semantics — delete to force the upload to re-run.
|
||||||
|
const CONFIG_ORG_AVATAR_MARKER: &str = "/var/lib/hyperhive/forge-agent-configs-avatar-set";
|
||||||
/// Hyperhive logo bytes, baked into the daemon. Uploaded once via the
|
/// Hyperhive logo bytes, baked into the daemon. Uploaded once via the
|
||||||
/// admin avatar API so the `core` Forgejo user shows the project mark
|
/// admin avatar API so the `core` Forgejo user shows the project mark
|
||||||
/// next to commits in `agent-configs/*`, `core/meta`, etc. instead of
|
/// next to commits in `agent-configs/*`, `core/meta`, etc. instead of
|
||||||
/// the default hash identicon.
|
/// the default hash identicon.
|
||||||
const CORE_AVATAR_PNG: &[u8] = include_bytes!("../../branding/hyperhive.png");
|
const CORE_AVATAR_PNG: &[u8] = include_bytes!("../../branding/hyperhive.png");
|
||||||
|
/// `agent-configs` org logo bytes (#424). Sibling visual to the main
|
||||||
|
/// hyperhive mark — same dark base + outer ring + corner brackets,
|
||||||
|
/// with a stacked-config-files glyph in the centre so the operator
|
||||||
|
/// can distinguish the agent-configs namespace from the main
|
||||||
|
/// `hyperhive` org at a glance. Source-of-truth is
|
||||||
|
/// `branding/agent-configs.svg`; `hive-c0re/build.rs` renders it
|
||||||
|
/// into `$OUT_DIR/agent-configs.png` at compile time via
|
||||||
|
/// `rsvg-convert` so the raster never gets checked into git.
|
||||||
|
const CONFIG_ORG_AVATAR_PNG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/agent-configs.png"));
|
||||||
/// Forgejo org grouping every agent's applied config repo. Core is a
|
/// Forgejo org grouping every agent's applied config repo. Core is a
|
||||||
/// site admin and reads + writes every repo here; agents are NOT
|
/// site admin and reads + writes every repo here; agents are NOT
|
||||||
/// members and the repos are private, so no agent — not even the one
|
/// members and the repos are private, so no agent — not even the one
|
||||||
|
|
@ -303,6 +315,33 @@ async fn ensure_core_avatar(token: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the `agent-configs` org's Forgejo avatar to the
|
||||||
|
/// configs-stack glyph once (#424). Sibling to `ensure_core_avatar`:
|
||||||
|
/// one-shot, marker-guarded, best-effort. Forgejo's per-org avatar
|
||||||
|
/// endpoint is `POST /api/v1/orgs/{org}/avatar` with a base64-PNG
|
||||||
|
/// JSON body — same shape as the admin user endpoint above.
|
||||||
|
async fn ensure_config_org_avatar(token: &str) -> Result<()> {
|
||||||
|
let marker = std::path::Path::new(CONFIG_ORG_AVATAR_MARKER);
|
||||||
|
if marker.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let body = format!(
|
||||||
|
r#"{{"image":"{}"}}"#,
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(CONFIG_ORG_AVATAR_PNG),
|
||||||
|
);
|
||||||
|
let url = format!("{FORGE_HTTP}/api/v1/orgs/{CONFIG_ORG}/avatar");
|
||||||
|
let status = forge_http(reqwest::Method::POST, &url, token, &body).await?;
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("set {CONFIG_ORG} avatar: HTTP {status}");
|
||||||
|
}
|
||||||
|
if let Some(parent) = marker.parent() {
|
||||||
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
std::fs::write(marker, "").ok();
|
||||||
|
tracing::info!(org = CONFIG_ORG, "forge: set org avatar to configs-stack logo");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure the bootstrap `core` admin user + a token at
|
/// Ensure the bootstrap `core` admin user + a token at
|
||||||
/// `CORE_TOKEN_PATH`. The token is what hive-c0re uses for forgejo
|
/// `CORE_TOKEN_PATH`. The token is what hive-c0re uses for forgejo
|
||||||
/// API calls (org creation now, meta-repo push later). Returns the
|
/// API calls (org creation now, meta-repo push later). Returns the
|
||||||
|
|
@ -631,6 +670,9 @@ pub async fn ensure_all() {
|
||||||
if let Err(e) = ensure_core_avatar(token).await {
|
if let Err(e) = ensure_core_avatar(token).await {
|
||||||
tracing::warn!(error = ?e, "forge: ensure_core_avatar failed");
|
tracing::warn!(error = ?e, "forge: ensure_core_avatar failed");
|
||||||
}
|
}
|
||||||
|
if let Err(e) = ensure_config_org_avatar(token).await {
|
||||||
|
tracing::warn!(error = ?e, "forge: ensure_config_org_avatar failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let Ok(containers) = crate::lifecycle::list().await else {
|
let Ok(containers) = crate::lifecycle::list().await else {
|
||||||
tracing::warn!("forge: nixos-container list failed; skipping user sweep");
|
tracing::warn!("forge: nixos-container list failed; skipping user sweep");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue