OXYGENHUB

The logo's arms are drawn and slide while a page loads; the Serves column lines up; the job re-run button has room (closes #306, closes #309, closes #310) 03bfb945f5c1

The mark's gradient was in bounding-box units, and a horizontal line's
box has no height, so the arms were never painted. It is in user space
now in the header, footer, favicon and mark.svg, and the wait animation
slides the arms opposite ways beside the turning rings.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 10ff523123bf
When
· 2026-10-25 16:31 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
03bfb945f5c1802fccb5e2b81155f569b5ea76039bf4f747cf3be08a8e27d458
Depends on
16626c0d69c0 19355e2283ab 19bb31ff1f5c 5c6eb1feaa71 74be97962ed8 a7e6e8e6094c b5668108487f b6b7e4cf6b24 d23464c0ac60 de4467cb1eca de505df5cf8d e9b9619cd857
Depended on by
5d818ec7d5fc
In bookmarks
api-unknown-fields diffusion-pin-bookmark draft-ready main proposal-list-fast
In tags
v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
16 atoms, 0 tree operations, 6 files

Browse the tree at this change

modified CHANGELOG.md +4 −0
…
3 unchanged lines
4 4
5 5
## Unreleased
6 6
7 +
- **One logo, with its arms (#306):** the Oxygen mark's two arms were never drawn: their gradient was in bounding-box units, and a horizontal line's box has no height, so SVG paints nothing. The gradient is now in user space everywhere (header, footer, favicon, `mark.svg`; the VS Code extensions and the Android app too), and while a page loads the arms slide back and forth in turn beside the turning rings (still with reduced motion).
8 +
- **Admin › Runners: the Serves column lines up (#309):** the scope sits above its input, so the inputs line up whatever the scope's length.
9 +
- **A job's RE-RUN THIS JOB button no longer touches its log (#310).**
10 +
7 11
## 0.9.1 (2026-10-25)
8 12
9 13
- **`POST /api/v1/user/runner-token` (repo:write):** a one-time registration token for a runner that serves the caller's own repositories and no one else's, as the Android app offers the phone as a runner (oxygenhub-android#8); it lasts a day, like the settings pages' tokens, and is audited.
…
304 unchanged lines
modified assets/icons/mark.svg +1 −1
1 1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-24 -12 48 24" width="48" height="24" aria-hidden="true">
2 2
  <defs>
3 -
    <linearGradient id="oxyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
3 +
    <linearGradient id="oxyGrad" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11">
4 4
      <stop offset="0%" stop-color="#00F0FF"/>
5 5
      <stop offset="100%" stop-color="#0072FF"/>
6 6
    </linearGradient>
…
8 unchanged lines
modified assets/icons/oxygenhub.svg +1 −1
1 1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="100%" height="100%">
2 2
  <defs>
3 -
    <linearGradient id="oxyGrad" x1="0%" y1="0%" x2="100%" y2="100%">
3 +
    <linearGradient id="oxyGrad" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11">
4 4
      <stop offset="0%" stop-color="#00F0FF" />
5 5
      <stop offset="100%" stop-color="#0072FF" />
6 6
    </linearGradient>
…
14 unchanged lines
modified templates/admin_runners.html +1 −1
…
27 unchanged lines
28 28
    <td><a href="/admin/runners/{{ r.id }}"><strong>{{ r.name }}</strong></a>{% if r.source == "config" %} <span class="tag" title="Configured with a hub.toml token">hub.toml</span>{% endif %}</td>
29 29
    <td><span class="runner-status st-{{ r.connectivity() }}">{{ r.connectivity() }}</span></td>
30 30
    <td class="small"><a href="/admin/runners?runner={{ r.name|urlencode }}#queue" title="{% if row.jobs.is_empty() %}Its jobs in the queue{% else %}{{ row.jobs_line() }}{% endif %}">{{ row.doing() }}</a></td>
31 -
    <td class="small">{{ row.scope }}
31 +
    <td class="small runner-serves"><div>{{ row.scope }}</div>
32 32
      <form class="tiny" method="post" action="/admin/runners/{{ r.id }}/scope" title="Empty for the whole site, an owner, or owner/repository"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="text" name="scope" size="14" placeholder="owner or owner/repo"> <button>set</button></form></td>
33 33
    <td>{% if r.key.is_empty() %}<span class="mute" title="Bound on its first signed receipt">not yet</span>{% else %}<code title="{{ r.key }} · {{ r.key_name }}">{{ r.key|short }}</code>{% endif %}</td>
34 34
    <td class="small">{% if r.arch.is_empty() %}<span class="mute">unknown</span>{% else %}{{ r.arch }} · {{ r.os }} · flow {{ r.version }}{% endif %}{% let f = r.facts_line() %}{% if !f.is_empty() %}<br><span class="mute" title="Measured by the runner at its last claim{% if !r.kernel.is_empty() %}; kernel {{ r.kernel }}{% endif %}">{{ f }}</span>{% endif %}{% if !row.swarm.is_empty() %}<br><span class="mute" title="Where other runners fetch image chunks from it">swarm {{ row.swarm }}</span>{% endif %}</td>
…
41 unchanged lines
modified templates/base.html +2 −2
…
11 unchanged lines
12 12
<body{% block bodyattrs %}{% endblock %}>
13 13
<a class="skip" href="#main">Skip to content</a>
14 14
<header>
15 -
  <a class="brand" href="/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="-24 -12 48 24" width="48" height="24" aria-hidden="true"><defs><linearGradient id="oxyGradH" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0FF"/><stop offset="100%" stop-color="#0072FF"/></linearGradient></defs><path d="M -11 -6 L 11 -6" stroke="url(#oxyGradH)" stroke-width="3" stroke-linecap="round"/><path d="M -11 6 L 11 6" stroke="url(#oxyGradH)" stroke-width="3" stroke-linecap="round"/><circle cx="-11" cy="0" r="10" fill="none" stroke="url(#oxyGradH)" stroke-width="3"/><circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/><circle cx="11" cy="0" r="10" fill="none" stroke="url(#oxyGradH)" stroke-width="3"/><circle cx="11" cy="0" r="3.5" fill="#FFFFFF"/></svg><span class="px">OXYGEN<em>HUB</em></span></a>
15 +
  <a class="brand" href="/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="-24 -12 48 24" width="48" height="24" aria-hidden="true"><defs><linearGradient id="oxyGradH" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11"><stop offset="0%" stop-color="#00F0FF"/><stop offset="100%" stop-color="#0072FF"/></linearGradient></defs><path class="arm" d="M -11 -6 L 11 -6" stroke="url(#oxyGradH)" stroke-width="3" stroke-linecap="round"/><path class="arm" d="M -11 6 L 11 6" stroke="url(#oxyGradH)" stroke-width="3" stroke-linecap="round"/><circle cx="-11" cy="0" r="10" fill="none" stroke="url(#oxyGradH)" stroke-width="3"/><circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/><circle cx="11" cy="0" r="10" fill="none" stroke="url(#oxyGradH)" stroke-width="3"/><circle cx="11" cy="0" r="3.5" fill="#FFFFFF"/></svg><span class="px">OXYGEN<em>HUB</em></span></a>
16 16
  <form class="find" action="/search" method="get" role="search"><input type="search" name="q" placeholder="Search…" aria-label="Search" autocomplete="off"></form>
17 17
  <nav aria-label="Account">
18 18
    <button type="button" class="theme-toggle" data-theme-toggle aria-label="Theme" title="Theme"><span aria-hidden="true">◐</span></button>
…
24 unchanged lines
43 43
<footer>
44 44
  <div class="foot">
45 45
    <div>
46 -
      <a class="brand" href="/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="-24 -12 48 24" width="48" height="24" aria-hidden="true"><defs><linearGradient id="oxyGradF" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00F0FF"/><stop offset="100%" stop-color="#0072FF"/></linearGradient></defs><path d="M -11 -6 L 11 -6" stroke="url(#oxyGradF)" stroke-width="3" stroke-linecap="round"/><path d="M -11 6 L 11 6" stroke="url(#oxyGradF)" stroke-width="3" stroke-linecap="round"/><circle cx="-11" cy="0" r="10" fill="none" stroke="url(#oxyGradF)" stroke-width="3"/><circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/><circle cx="11" cy="0" r="10" fill="none" stroke="url(#oxyGradF)" stroke-width="3"/><circle cx="11" cy="0" r="3.5" fill="#FFFFFF"/></svg><span class="px">OXYGEN<em>HUB</em></span></a>
46 +
      <a class="brand" href="/"><svg xmlns="http://www.w3.org/2000/svg" viewBox="-24 -12 48 24" width="48" height="24" aria-hidden="true"><defs><linearGradient id="oxyGradF" gradientUnits="userSpaceOnUse" x1="-22" y1="-11" x2="22" y2="11"><stop offset="0%" stop-color="#00F0FF"/><stop offset="100%" stop-color="#0072FF"/></linearGradient></defs><path class="arm" d="M -11 -6 L 11 -6" stroke="url(#oxyGradF)" stroke-width="3" stroke-linecap="round"/><path class="arm" d="M -11 6 L 11 6" stroke="url(#oxyGradF)" stroke-width="3" stroke-linecap="round"/><circle cx="-11" cy="0" r="10" fill="none" stroke="url(#oxyGradF)" stroke-width="3"/><circle cx="-11" cy="0" r="3.5" fill="#00F0FF"/><circle cx="11" cy="0" r="10" fill="none" stroke="url(#oxyGradF)" stroke-width="3"/><circle cx="11" cy="0" r="3.5" fill="#FFFFFF"/></svg><span class="px">OXYGEN<em>HUB</em></span></a>
47 47
      <p class="small">{{ shell.site }} · a forge for Oxygen repositories. Nothing here is inferred: what a page says, the object store says.</p>
48 48
    </div>
49 49
    <div class="cols">
…
13 unchanged lines
modified templates/style.css +10 −2
…
356 unchanged lines
357 357
table.small td, table.small th { padding: 5px 8px; font-size: 13px; }
358 358
form.tiny { display: inline; margin: 0; }
359 359
form.tiny button { padding: 1px 6px; font-size: 10px; }
360 +
.flow-job > form.job-rerun { display: block; margin: 0 0 8px; }
361 +
/* The runners table's Serves cell (#309): the scope above its input, so the inputs line up whatever the scope's length. */
362 +
.runner-serves form.tiny { display: flex; gap: 4px; margin-top: 4px; white-space: nowrap; }
360 363
361 364
/* proposals */
362 365
.comment { background: var(--panel); border: 1px solid var(--line); padding: 10px 14px; margin: 0 0 10px; }
…
755 unchanged lines
1118 1121
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
1119 1122
1120 1123
/* Working (#142): a pressed button's spinner, and the Oxygen mark's rings
1121 -
   spinning while a slow page loads (hub.js adds .oxy-wait after 300 ms). The
1122 -
   pointer keeps its own shape: the mark beside it is the only sign. */
1124 +
   spinning and its arms sliding back and forth in turn while a slow page
1125 +
   loads (hub.js adds .oxy-wait after 300 ms; #306). The pointer keeps its own
1126 +
   shape: the mark beside it is the only sign. */
1123 1127
button[aria-busy=true] { opacity: 1; }
1124 1128
.oxy-spin { display: inline-block; width: 1em; text-align: center; }
1125 1129
.oxy-wait-mark { display: none; position: fixed; z-index: 1000; pointer-events: none; padding: 4px 6px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, .18); line-height: 0; }
…
2 unchanged lines
1128 1132
.oxy-wait .oxy-wait-mark { display: block; }
1129 1133
.oxy-wait header .brand svg circle[r="10"], .oxy-wait-mark svg circle[r="10"] { stroke-dasharray: 44 18.8; animation: oxy-ring .9s linear infinite; }
1130 1134
.oxy-wait header .brand svg circle[cx="11"][r="10"], .oxy-wait-mark svg circle[cx="11"][r="10"] { animation-direction: reverse; }
1135 +
.oxy-wait header .brand svg .arm, .oxy-wait-mark svg .arm { animation: oxy-arm .9s ease-in-out infinite alternate; }
1136 +
.oxy-wait header .brand svg .arm + .arm, .oxy-wait-mark svg .arm + .arm { animation-direction: alternate-reverse; }
1131 1137
@keyframes oxy-ring { to { stroke-dashoffset: -62.8; } }
1138 +
@keyframes oxy-arm { from { transform: translateX(-3px); } to { transform: translateX(3px); } }
1132 1139
@media (prefers-reduced-motion: reduce) {
1133 1140
  .oxy-wait header .brand svg circle[r="10"], .oxy-wait-mark svg circle[r="10"] { animation: none; stroke-dasharray: none; }
1141 +
  .oxy-wait header .brand svg .arm, .oxy-wait-mark svg .arm { animation: none; }
1134 1142
}
1135 1143
1136 1144
/* Admin › Runners › Images (C0, docs/cylinder.md): where each image is. */
…
316 unchanged lines