Add release notes and tidy mobile UI
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
115
apps/web/src/components/ReleaseNotesDrawer.vue
Normal file
115
apps/web/src/components/ReleaseNotesDrawer.vue
Normal 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>
|
||||
96
apps/web/src/releaseNotes.ts
Normal file
96
apps/web/src/releaseNotes.ts
Normal 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.",
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user