Add release notes and tidy mobile UI

This commit is contained in:
Dorian
2026-05-06 19:58:03 +01:00
parent b715c3f27d
commit 1e74719932
6 changed files with 305 additions and 37 deletions

View File

@@ -4,12 +4,14 @@ import { RouterLink, RouterView, useRoute } from "vue-router";
import { useAuthStore } from "./stores/auth";
import { useStatsStore } from "./stores/stats";
import ChatDrawer from "./components/ChatDrawer.vue";
import ReleaseNotesDrawer from "./components/ReleaseNotesDrawer.vue";
const auth = useAuthStore();
const stats = useStatsStore();
const route = useRoute();
const crt = ref(false);
const chatOpen = ref(false);
const releaseOpen = ref(false);
const LOW_HASHRATE_THS = 10;
const TOP_HASHRATE_THS = 70;
const heatLevel = computed(() => {
@@ -56,16 +58,22 @@ watch(
</nav>
<div class="actions">
<button class="thin" @click="toggleCrt">CRT {{ crt ? "on" : "off" }}</button>
<button class="thin" @click="releaseOpen = !releaseOpen">notes</button>
<template v-if="auth.isLoggedIn">
<button class="thin icon-btn" aria-label="open chat" @click="chatOpen = !chatOpen">
<span class="chat-icon" />
</button>
<button class="thin" @click="chatOpen = !chatOpen">chat</button>
<span class="muted">{{ auth.npub ? shortNpub(auth.npub) : "" }}</span>
<button class="thin" @click="auth.logout()">logout</button>
</template>
</div>
</header>
<nav v-if="auth.isLoggedIn" class="mobile-tabbar" aria-label="mobile navigation">
<RouterLink to="/">status</RouterLink>
<RouterLink to="/graphs">graphs</RouterLink>
<button type="button" @click="releaseOpen = !releaseOpen">notes</button>
<button type="button" @click="chatOpen = !chatOpen">chat</button>
</nav>
<main :class="{ shellLogin: route.name === 'login' }">
<RouterView />
</main>
@@ -75,6 +83,7 @@ watch(
</footer>
<ChatDrawer v-if="auth.isLoggedIn" :open="chatOpen" @close="chatOpen = false" />
<ReleaseNotesDrawer :open="releaseOpen" @close="releaseOpen = false" />
</template>
<style scoped>
@@ -122,6 +131,9 @@ watch(
color: var(--neon-cyan);
box-shadow: var(--shadow-cyan);
}
.mobile-tabbar {
display: none;
}
.actions {
display: flex;
gap: 12px;
@@ -131,30 +143,6 @@ button.thin {
padding: 4px 10px;
font-size: 11px;
}
.icon-btn {
width: 30px;
height: 24px;
display: grid;
place-items: center;
padding: 0;
}
.chat-icon {
width: 14px;
height: 10px;
border: 1px solid currentColor;
position: relative;
}
.chat-icon::after {
content: "";
position: absolute;
right: 2px;
bottom: -5px;
width: 6px;
height: 5px;
border-left: 1px solid currentColor;
border-bottom: 1px solid currentColor;
transform: skewY(-35deg);
}
main {
padding: 24px;
max-width: 1400px;
@@ -174,16 +162,60 @@ main.shellLogin {
}
@media (max-width: 760px) {
.topbar {
align-items: flex-start;
flex-direction: column;
gap: 10px;
align-items: center;
padding: 10px 14px;
}
.brand .muted,
.nav {
margin: 0;
display: none;
}
.actions {
width: 100%;
justify-content: space-between;
width: auto;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
}
.actions button:nth-of-type(n + 2) {
display: none;
}
.actions .muted {
display: none;
}
.mobile-tabbar {
position: sticky;
top: 49px;
z-index: 9;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0;
border-bottom: 1px solid var(--line);
background: rgba(7, 9, 15, 0.94);
backdrop-filter: blur(6px);
}
.mobile-tabbar a,
.mobile-tabbar button {
min-width: 0;
border: 0;
border-right: 1px solid var(--line);
color: var(--fg-1);
padding: 10px 4px;
text-align: center;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
background: transparent;
}
.mobile-tabbar a:last-child,
.mobile-tabbar button:last-child {
border-right: 0;
}
.mobile-tabbar a.router-link-active,
.mobile-tabbar button:hover {
color: var(--neon-cyan);
box-shadow: inset 0 -1px 0 var(--neon-cyan), var(--shadow-cyan);
}
main {
padding: 16px;
}
}
</style>

View File

@@ -171,7 +171,7 @@ async function scrollBottom(): Promise<void> {
width: min(430px, 92vw);
height: 100vh;
display: grid;
grid-template-rows: auto auto 1fr auto;
grid-template-rows: auto auto minmax(0, 1fr) auto;
gap: 12px;
padding: 16px;
background: rgba(7, 9, 15, 0.97);
@@ -280,11 +280,21 @@ p {
}
.composer {
display: grid;
grid-template-columns: 1fr auto;
grid-template-columns: minmax(0, 1fr) 64px;
gap: 8px;
align-items: start;
}
textarea {
height: 58px;
min-height: 58px;
max-height: 92px;
resize: none;
line-height: 1.3;
}
.composer button {
align-self: start;
height: 58px;
padding: 0 8px;
}
@media (max-width: 760px) {
.chat-backdrop {
@@ -295,6 +305,7 @@ textarea {
bottom: 0;
width: 100vw;
height: 55vh;
padding: 14px;
transform: translateY(105%);
border-left: 0;
border-top: 1px solid var(--line-bright);
@@ -302,5 +313,8 @@ textarea {
.chat-drawer.open {
transform: translateY(0);
}
.composer {
grid-template-columns: minmax(0, 1fr) 58px;
}
}
</style>

View File

@@ -65,9 +65,11 @@ const currentLine = computed(() => {
display: flex;
flex-direction: column;
gap: 10px;
min-width: 0;
}
.source {
font-size: 10px;
overflow-wrap: anywhere;
}
.line {
font-size: 13px;
@@ -75,13 +77,20 @@ const currentLine = computed(() => {
gap: 10px;
align-items: baseline;
min-height: 32px;
min-width: 0;
}
.prefix { color: var(--neon-amber); font-weight: 700; }
.text { line-height: 1.5; }
.text {
line-height: 1.5;
min-width: 0;
overflow-wrap: anywhere;
word-break: break-word;
}
.dots {
display: flex;
gap: 5px;
margin-top: 4px;
flex-wrap: wrap;
}
.dot {
width: 5px; height: 5px;

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { RELEASE_NOTES } from "../releaseNotes";
defineProps<{ open: boolean }>();
const emit = defineEmits<{ close: [] }>();
</script>
<template>
<div v-if="open" class="release-backdrop" @click="emit('close')" />
<aside :class="['release-drawer', { open }]">
<header>
<div>
<div class="label">release notes</div>
<h2 class="glow-amber">what changed while the heaters screamed</h2>
</div>
<button class="thin" aria-label="close release notes" @click="emit('close')">×</button>
</header>
<div class="notes">
<article v-for="note in RELEASE_NOTES" :key="note.hash" class="note">
<div class="note-head">
<strong>{{ note.title }}</strong>
<code>{{ note.hash }}</code>
</div>
<ul>
<li v-for="bullet in note.bullets" :key="bullet">{{ bullet }}</li>
</ul>
</article>
</div>
</aside>
</template>
<style scoped>
.release-backdrop {
position: fixed;
inset: 0;
z-index: 18;
background: rgba(0, 0, 0, 0.22);
}
.release-drawer {
position: fixed;
top: 0;
right: 0;
z-index: 21;
width: min(520px, 94vw);
height: 100vh;
display: grid;
grid-template-rows: auto 1fr;
gap: 14px;
padding: 16px;
background: rgba(7, 9, 15, 0.98);
border-left: 1px solid var(--line-bright);
box-shadow: -16px 0 40px rgba(0, 0, 0, 0.48);
transform: translateX(102%);
transition: transform 0.18s ease;
}
.release-drawer.open {
transform: translateX(0);
}
header,
.note-head {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: start;
}
h2 {
margin: 2px 0 0;
font-size: 18px;
letter-spacing: 0;
}
.notes {
overflow: auto;
display: flex;
flex-direction: column;
gap: 12px;
padding-right: 4px;
}
.note {
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.025);
padding: 12px;
}
.note-head strong {
color: var(--neon-cyan);
}
code {
color: var(--fg-2);
font-size: 10px;
}
ul {
margin: 8px 0 0;
padding-left: 18px;
}
li {
margin: 5px 0;
color: var(--fg-1);
font-size: 12px;
line-height: 1.4;
}
@media (max-width: 760px) {
.release-drawer {
top: auto;
bottom: 0;
width: 100vw;
height: 65vh;
transform: translateY(105%);
border-left: 0;
border-top: 1px solid var(--line-bright);
}
.release-drawer.open {
transform: translateY(0);
}
}
</style>

View File

@@ -0,0 +1,96 @@
export type ReleaseNote = {
hash: string;
title: string;
bullets: string[];
};
export const RELEASE_NOTES: ReleaseNote[] = [
{
hash: "b715c3f",
title: "Nostr chat drawer",
bullets: [
"Added a signed Nostr chat panel, because apparently the miners needed a comments section for witnesses.",
"Desktop slides in from the right; mobile rises as a half-screen sheet like a tiny gossip bunker.",
"Loads kind-0 profile names and avatars, so the abuse has proper attribution.",
],
},
{
hash: "b2f81a1",
title: "Tidier hero stats",
bullets: [
"Pinned the hero stat boxes down so rotating jokes stop rearranging the furniture.",
"Aligned block height, network hashrate, and share stats like they have jobs.",
],
},
{
hash: "1f86bf9",
title: "Readable odds",
bullets: [
"Replaced cursed science notation with human-readable odds like '1 in 2.0B', which is still awful but now legible.",
"Slowed rotating labels and lottery jokes to 10 seconds so the dashboard stops acting caffeinated.",
],
},
{
hash: "fa707e2",
title: "Network stats and rotating jokes",
bullets: [
"Added block height, network hashrate, and difficulty from tx1138 mempool, because Datum was keeping the good gossip to itself.",
"Solo odds now use current network hashrate, so the humiliation is freshly calculated.",
"Added rotating labels and a large pile of darker lottery jokes about fiat, Bitcoin, and electrical self-deception.",
"Race view now compares actual miners to a large-pool-ish bully and the total network, on a mercy scale so the children remain visible.",
],
},
{
hash: "7e1f7a1",
title: "Heat-reactive theme",
bullets: [
"Interface now gets redder from 10 TH/s to 70 TH/s, because someone's getting warmed tonight.",
],
},
{
hash: "243cd0d",
title: "Boomer Heater got worse",
bullets: [
"Gave Boomer Heater a dead status, skull marker, and custom zero-stat labels because it contributes exactly fuck all.",
"Updated the heater asset to say 'NO HASHES. JUST GHEI.' Accurate thermodynamics, questionable monetary policy.",
],
},
{
hash: "d6f8fd7",
title: "Boomer Heater on status",
bullets: [
"Added fake Boomer Heater to the status screen, always last, where fiat appliances belong.",
"Race copy now says 'just fiat and a pension.' No sats, no shares, no future.",
],
},
{
hash: "0c8e26f",
title: "PWA and miner race",
bullets: [
"Made gashboard installable as a PWA with icons, service worker, and cache rules that should stop stale bundles haunting the cupboard.",
"Remote signer login can resume after Primal/Amber returns to the PWA callback, because mobile auth should not require ritual sacrifice.",
"Added miner race visuals with local miner assets, including the non-hashing radiator of shame.",
],
},
{
hash: "360b1eb",
title: "BigPapa",
bullets: ["Renamed the unknown Datum miner from ??? to BigPapa. Same mystery, more authority."],
},
{
hash: "c2c376f",
title: "Shameboard calculations",
bullets: [
"Added best observed share/work, expected block time, sats/day fantasy math, share velocity, reject pain, efficiency, BTU/hr, and per-miner insults. A spreadsheet with a personality disorder.",
],
},
{
hash: "c77c746",
title: "Graphs and remote signer login",
bullets: [
"Added graph tab with hashrate telemetry, share flow, miner mix, and browser-local history so the suffering can be trended.",
"Added Nostr Connect 'open signer app' login for Primal/Amber handoff, replacing the bunker URI homework nobody asked for.",
"Fixed Portainer-on-Umbrel deployment compose for host networking and direct Datum URL, because Portainer's fake network was cosplay.",
],
},
];

View File

@@ -120,7 +120,7 @@ function minerKey(m: { nickname: string; authUsername: string; remoteHost: strin
.banner.warn { border-color: var(--neon-amber); color: var(--neon-amber); }
.grid-row {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
margin: 16px 0;
}
@@ -130,6 +130,7 @@ function minerKey(m: { nickname: string; authUsername: string; remoteHost: strin
gap: 16px;
align-items: center;
margin-top: 16px;
min-width: 0;
}
.best strong {
display: block;
@@ -139,6 +140,7 @@ function minerKey(m: { nickname: string; authUsername: string; remoteHost: strin
.best span {
font-size: 11px;
text-align: right;
overflow-wrap: anywhere;
}
.miners {
display: grid;