feat(honeybar): the HoneyOS bar, loaded at runtime
A strip along the top that leads to the other apps of the same system. It is served by HoneyOS; InkNote only says where to find it, through INKNOTE_HONEYBAR_ORIGIN. Empty means no bar and no change at all. The origin is served by the API rather than built into the page, because an image is pulled ready-made and a service worker keeps whole responses: anything frozen into index.html would outlive the setting. Layout follows the bar through --honeybar-h. The body has overflow:hidden, so the surplus height would not have scrolled but been cut off at the bottom; and #root padded itself around the notch that the bar now covers, which would have counted that gap twice. Unit 263/263, all fourteen browser suites green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ea448f091
commit
00b1d4469f
12 files changed
+144
-5
No files matched your search
@@ -24,6 +24,15 @@ INKNOTE_ADMIN_PASSWORD=
|
||||
|
||||
# ── Optional ── unset keeps the default shown
|
||||
# INKNOTE_MAX_IMAGE_MB=25 # pasted images
|
||||
|
||||
# ── HoneyOS bar ───────────────────────────────────────────────────────────────
|
||||
# The strip along the top that ties the apps together. Empty = no bar.
|
||||
# Wirkt beim Neustart, nicht beim Bauen.
|
||||
# INKNOTE_HONEYBAR_ORIGIN=https://honeyos.example.com
|
||||
# Only needed when HoneyOS lives on a different site than InkNote — then the
|
||||
# browser will not send its session and the bar needs this to fetch the app
|
||||
# list. Same domain (notes.example.org + example.org): leave empty.
|
||||
# INKNOTE_HONEYBAR_KEY=
|
||||
# INKNOTE_MAX_PDF_MB=200 # imported PDFs, limited separately
|
||||
# INKNOTE_UID=1000 # must own INKNOTE_DATA_DIR
|
||||
# INKNOTE_GID=1000
|
||||
|
||||
@@ -14,6 +14,32 @@ a rebuild is not bit-identical.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-25 — The bar that ties the apps together
|
||||
|
||||
**Images** — web `sha256:7c802a96…`, server `sha256:a156fb92…`
|
||||
|
||||
**Added**
|
||||
- **A strip along the top that leads to the other apps.** It is served by
|
||||
HoneyOS, so what it looks like and which apps it lists are settled there —
|
||||
InkNote only says where to find it. `INKNOTE_HONEYBAR_ORIGIN` in the `.env`
|
||||
switches it on, empty leaves everything exactly as before.
|
||||
- **`GET /api/public/config`** hands out that origin, and the key that goes with
|
||||
it when HoneyOS lives on a different site. Runtime, not build time: an image
|
||||
is pulled ready-made, so a value baked in at build time could never be changed
|
||||
from the `.env` — and a service worker keeps whole responses, headers
|
||||
included, so anything frozen into `index.html` outlives the setting in the
|
||||
browser. Aether learned that one the hard way first.
|
||||
|
||||
**Changed**
|
||||
- **The app now measures its height against the bar**, in `#root` and in the
|
||||
four places that asked for the full window. With `overflow: hidden` on the
|
||||
body the surplus would not have scrolled — it would have been cut off at the
|
||||
bottom, silently. And `#root` padded itself around the phone's notch, which
|
||||
the bar now covers, so that gap would have been counted twice. Without a bar
|
||||
`--honeybar-h` is `0px` and every one of these is what it was.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-22 — A bigger face, and the theme where the settings are
|
||||
|
||||
**Images** — web `sha256:fe35b0e7…`, server `sha256:a557fb4d…`
|
||||
|
||||
@@ -878,6 +878,26 @@ app.get("/api/auth/methods", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/* Where the HoneyOS bar lives, if it is wanted at all.
|
||||
*
|
||||
* Served at runtime rather than baked into the build for two reasons. The image
|
||||
* is pulled ready-made, so a value fixed at build time could never be changed
|
||||
* from the .env afterwards. And a service worker caches whole responses,
|
||||
* headers included — anything frozen into index.html stays frozen in the
|
||||
* browser long after the server has moved on.
|
||||
*
|
||||
* The key is only handed out together with an origin: without a bar it has
|
||||
* nothing to unlock, and it should not sit in the page for no reason. It buys
|
||||
* the list of apps and nothing else. */
|
||||
app.get("/api/public/config", (req, res) => {
|
||||
const origin = String(process.env.INKNOTE_HONEYBAR_ORIGIN || "").replace(/\/+$/, "");
|
||||
res.json({
|
||||
honeybar: origin
|
||||
? { origin, key: String(process.env.INKNOTE_HONEYBAR_KEY || "") }
|
||||
: null,
|
||||
});
|
||||
});
|
||||
|
||||
/* The browser is sent to the provider from here rather than from the app, so
|
||||
* the app never has to know the issuer, the scopes or the redirect. */
|
||||
app.get("/api/auth/oidc/start", async (req, res) => {
|
||||
|
||||
@@ -32,6 +32,11 @@ services:
|
||||
# Optional tuning; unset means the server's own defaults apply.
|
||||
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-}
|
||||
- INKNOTE_MAX_IMAGE_MB=${INKNOTE_MAX_IMAGE_MB:-}
|
||||
# HoneyOS bar. Empty origin means no bar at all; the key is only needed
|
||||
# when HoneyOS sits on a different site, where the browser withholds its
|
||||
# session. Takes effect on restart — nothing here is baked into the image.
|
||||
- INKNOTE_HONEYBAR_ORIGIN=${INKNOTE_HONEYBAR_ORIGIN:-}
|
||||
- INKNOTE_HONEYBAR_KEY=${INKNOTE_HONEYBAR_KEY:-}
|
||||
- INKNOTE_MAX_PDF_MB=${INKNOTE_MAX_PDF_MB:-}
|
||||
- MAX_VERSIONS_PER_NOTE=${MAX_VERSIONS_PER_NOTE:-}
|
||||
- VERSION_RETENTION_DAYS=${VERSION_RETENTION_DAYS:-}
|
||||
|
||||
@@ -30,6 +30,11 @@ services:
|
||||
# Optional tuning; unset means the server's own defaults apply.
|
||||
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-}
|
||||
- INKNOTE_MAX_IMAGE_MB=${INKNOTE_MAX_IMAGE_MB:-}
|
||||
# HoneyOS bar. Empty origin means no bar at all; the key is only needed
|
||||
# when HoneyOS sits on a different site, where the browser withholds its
|
||||
# session. Takes effect on restart — nothing here is baked into the image.
|
||||
- INKNOTE_HONEYBAR_ORIGIN=${INKNOTE_HONEYBAR_ORIGIN:-}
|
||||
- INKNOTE_HONEYBAR_KEY=${INKNOTE_HONEYBAR_KEY:-}
|
||||
- INKNOTE_MAX_PDF_MB=${INKNOTE_MAX_PDF_MB:-}
|
||||
- MAX_VERSIONS_PER_NOTE=${MAX_VERSIONS_PER_NOTE:-}
|
||||
- VERSION_RETENTION_DAYS=${VERSION_RETENTION_DAYS:-}
|
||||
|
||||
+14
-1
@@ -38,7 +38,20 @@
|
||||
</script>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; overflow: hidden; overscroll-behavior: none; }
|
||||
#root { padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); height: 100vh; }
|
||||
/* --honeybar-h is set by the HoneyOS bar while it is on the page, and is
|
||||
0 when it is not. The bar sits above the app and pushes it down, so the
|
||||
app has that much less room; body has overflow:hidden, which means the
|
||||
difference would not scroll but simply be cut off at the bottom.
|
||||
|
||||
It also covers the notch itself, so the padding above must give way —
|
||||
otherwise the gap is counted twice. max() keeps that from going
|
||||
negative when there is no bar. */
|
||||
#root {
|
||||
box-sizing: border-box;
|
||||
padding: max(0px, calc(env(safe-area-inset-top) - var(--honeybar-h, 0px)))
|
||||
env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||
height: calc(100vh - var(--honeybar-h, 0px));
|
||||
}
|
||||
#splash { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; background: #0d1117; z-index: 9999; transition: opacity 0.3s; }
|
||||
#splash.hide { opacity: 0; pointer-events: none; }
|
||||
#splash .logo { width: 64px; height: 64px; border-radius: 18px; background: linear-gradient(135deg, #388bfd, #a855f7); display: flex; align-items: center; justify-content: center; font-size: 32px; animation: pulse 1.5s infinite; }
|
||||
|
||||
@@ -933,7 +933,7 @@ export default function App() {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
height: "calc(100vh - var(--honeybar-h, 0px))",
|
||||
background: T.bg,
|
||||
color: T.text,
|
||||
fontFamily: FONT_UI,
|
||||
@@ -1000,7 +1000,7 @@ export default function App() {
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
height: "calc(100vh - var(--honeybar-h, 0px))",
|
||||
width: "100vw",
|
||||
overflow: "hidden",
|
||||
background: T.bg,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Load the HoneyOS bar, if this instance is configured to have one.
|
||||
*
|
||||
* The bar is a strip along the top of every app that belongs to the same
|
||||
* system: it says where you are and takes you to the others. It lives on the
|
||||
* HoneyOS server, so it stays in step everywhere without InkNote being rebuilt.
|
||||
*
|
||||
* Deliberately fire-and-forget. If HoneyOS is slow, down, or simply not
|
||||
* configured, nothing here throws and nothing here waits — InkNote is not the
|
||||
* kind of app that may fail to start because a decoration is missing. Offline
|
||||
* there is no bar at all, which is the right trade for an offline-first app.
|
||||
*/
|
||||
export async function mountHoneybar() {
|
||||
// The bar mounts itself on load; a second script tag would mean two bars.
|
||||
if (document.querySelector("script[data-honeybar]")) return;
|
||||
|
||||
let origin, key;
|
||||
try {
|
||||
const r = await fetch("/api/public/config");
|
||||
if (!r.ok) return;
|
||||
const cfg = (await r.json()).honeybar;
|
||||
if (!cfg?.origin) return;
|
||||
({ origin, key } = cfg);
|
||||
} catch {
|
||||
return; // offline, or no server — either way, no bar
|
||||
}
|
||||
|
||||
const el = document.createElement("script");
|
||||
el.dataset.honeybar = "";
|
||||
el.defer = true;
|
||||
el.src = origin.replace(/\/+$/, "") + "/honeybar/honeybar.js";
|
||||
// honeybar.js reads its settings from document.currentScript, which is set
|
||||
// for a script inserted this way just as it is for one written in the HTML.
|
||||
el.dataset.app = "inknote";
|
||||
if (key) el.dataset.key = key;
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import NoteApp from "./App.jsx";
|
||||
import { mountHoneybar } from "./honeybar.js";
|
||||
|
||||
// Hide splash screen
|
||||
const hideSplash = () => {
|
||||
@@ -19,6 +20,9 @@ ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
|
||||
setTimeout(hideSplash, 200);
|
||||
|
||||
// Not awaited: the bar is a decoration and must never hold up the app.
|
||||
mountHoneybar();
|
||||
|
||||
// Register service worker
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", async () => {
|
||||
|
||||
@@ -81,7 +81,7 @@ export function AuthScreen({ onLogin, dark, initialError = "" }) {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minHeight: "100vh",
|
||||
minHeight: "calc(100vh - var(--honeybar-h, 0px))",
|
||||
background: T.bg,
|
||||
fontFamily: "'DM Sans',sans-serif",
|
||||
}}
|
||||
|
||||
@@ -157,7 +157,7 @@ export function Sidebar({
|
||||
zIndex: isMobile ? 900 : 1,
|
||||
width: isMobile ? (open ? "100vw" : 0) : open ? 260 : 0,
|
||||
minWidth: isMobile ? 0 : open ? 260 : 0,
|
||||
height: isMobile ? "100vh" : "auto",
|
||||
height: isMobile ? "calc(100vh - var(--honeybar-h, 0px))" : "auto",
|
||||
background: T.sidebar,
|
||||
borderRight: isMobile ? "none" : `1px solid ${T.border}`,
|
||||
display: "flex",
|
||||
|
||||
@@ -169,6 +169,26 @@ The web UI listens on port `3000`, the API only inside the compose network. If a
|
||||
reverse proxy sits in front, bind the port to localhost (`"127.0.0.1:3000:80"`),
|
||||
otherwise the app is reachable on the host's address without TLS.
|
||||
|
||||
#### The HoneyOS bar
|
||||
|
||||
`INKNOTE_HONEYBAR_ORIGIN` puts a strip along the top of every page that leads to
|
||||
the other apps of the same system — it is served by HoneyOS, so what it looks
|
||||
like and which apps it lists are settled there, not here. Empty means no bar at
|
||||
all, and nothing about the app changes.
|
||||
|
||||
The value takes effect on restart: the page asks the server for it
|
||||
(`GET /api/public/config`) and loads the script itself. It is deliberately not
|
||||
built into the image — an image is pulled ready-made, and a service worker keeps
|
||||
whole responses, so anything frozen into `index.html` would outlive the setting.
|
||||
|
||||
`INKNOTE_HONEYBAR_KEY` is only needed when HoneyOS sits on a *different site*
|
||||
than InkNote (`honeyos.example.com` vs `notes.example.org`). The browser then
|
||||
withholds the HoneyOS session, and the bar has no other way to ask for the list
|
||||
of apps. Under a shared domain it can stay empty.
|
||||
|
||||
Offline there is no bar — it comes from another server. The app itself is
|
||||
unaffected, which is the right way round for one that is built to work offline.
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
Reference in new issue
Block a user