<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Keanred</title>
    <description>The latest articles on DEV Community by Keanred (@keanred).</description>
    <link>https://dev.to/keanred</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4031690%2Fe914d4bb-5027-4a1f-a999-f93a16cc51f2.png</url>
      <title>DEV Community: Keanred</title>
      <link>https://dev.to/keanred</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/keanred"/>
    <language>en</language>
    <item>
      <title>reveille: a morning dashboard that survives the internet</title>
      <dc:creator>Keanred</dc:creator>
      <pubDate>Thu, 16 Jul 2026 08:08:56 +0000</pubDate>
      <link>https://dev.to/keanred/reveille-a-morning-dashboard-that-survives-the-internet-3909</link>
      <guid>https://dev.to/keanred/reveille-a-morning-dashboard-that-survives-the-internet-3909</guid>
      <description>&lt;p&gt;I used to start every day the same way: open a browser, then five tabs. Weather. Calendar. GitHub, to see which PRs needed me. A todo list. A sunrise time, because I live in Finland and in December that number is the difference between "I'll walk" and "I absolutely will not."&lt;/p&gt;

&lt;p&gt;None of that needs a browser. It's a handful of API calls and a bit of formatting. So I built &lt;code&gt;reveille&lt;/code&gt;, one terminal command that reveilles those live sources into a single Ink-rendered view. Type &lt;code&gt;reveille&lt;/code&gt;, get your morning, close the terminal.&lt;/p&gt;

&lt;p&gt;The feature list is almost beside the point. The interesting part, and the reason I kept working on it past the weekend it should have taken, is that building a dashboard that reads a dozen flaky external APIs is really a lesson in what to do when those APIs misbehave. A dashboard that hangs because one weather endpoint is having a bad day is worse than no dashboard at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually shows
&lt;/h2&gt;

&lt;p&gt;Out of the box, and configurable from a TOML file, reveille pulls together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weather and Finnish sunrise/sunset (&lt;code&gt;daylight&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Calendar events, via Google Calendar OAuth&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git&lt;/code&gt; working-tree status and GitHub PR/review state&lt;/li&gt;
&lt;li&gt;Todos&lt;/li&gt;
&lt;li&gt;System health, including running Docker containers and local service checks&lt;/li&gt;
&lt;li&gt;A clock, a Hacker News-style headline feed, and an escape hatch: a generic &lt;code&gt;http-json&lt;/code&gt; source for any JSON endpoint you want on screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Panels lay out as masonry columns using Ink's flexbox (Yoga under the hood), so the dashboard reflows to your terminal width instead of assuming a fixed grid.&lt;/p&gt;

&lt;p&gt;Two commands earn their keep beyond the main view:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reveille --once&lt;/code&gt; fetches everything once, prints a single compact line (the next event and its countdown, today's event count, unchecked todos), then exits. Colors strip automatically when stdout isn't a TTY, so it drops straight into a shell prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next: Standup in 12m  ·  4 events  ·  2 due
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;reveille doctor&lt;/code&gt; is the first thing I run when a panel misbehaves. It validates the config and checks every source's credentials, then tells me &lt;em&gt;which&lt;/em&gt; secret failed to resolve and &lt;em&gt;where&lt;/em&gt; it looked, which turns "the weather panel is blank" from a debugging session into a one-line answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: everything is a &lt;code&gt;Source&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The whole system is built around one small contract. A &lt;code&gt;Source&amp;lt;T&amp;gt;&lt;/code&gt; is a pure, abortable, typed data feed that, deliberately, knows nothing about rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// stable; also the cache key&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// source type; selects the panel renderer&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// panel title&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// refresh interval, ms&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// per-fetch timeout, ms&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SourceContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// must honor ctx.signal&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping rendering out of the source is the decision everything else hangs off. A source is just "here's how to get some typed data, here's how long it's good for, here's when to give up." That makes every source trivially testable in isolation (no terminal, no React, no mocking a renderer), and it means the UI can treat all eleven source kinds identically.&lt;/p&gt;

&lt;p&gt;The state of a source at any moment is a discriminated union, and this is where the resilience story lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SourceState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;fetchedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;stale&lt;/code&gt; case is the one that matters. It carries the &lt;em&gt;last good data&lt;/em&gt; &lt;strong&gt;and&lt;/strong&gt; the error that happened on the most recent refresh. That single shape is what makes graceful degradation fall out of the type system instead of being bolted on with try/catch scattered through the UI. A panel whose latest fetch failed but whose previous fetch succeeded doesn't go blank and it doesn't lie. It shows the last good data with a "stale" badge. The failure is honest and local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resilience by construction
&lt;/h2&gt;

&lt;p&gt;Three decisions turn that contract into something that feels solid to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold start paints from cache.&lt;/strong&gt; Every successful fetch is written to an atomic JSON cache on disk under &lt;code&gt;~/.config/reveille/cache/&lt;/code&gt;. On launch, reveille hydrates every panel from that cache &lt;em&gt;first&lt;/em&gt;, so the dashboard is fully drawn before a single network request completes. Then each source refreshes on its own cadence in the background. You never look at a screen full of spinners.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every fetch is timeout-bounded and abortable.&lt;/strong&gt; Each source declares its own &lt;code&gt;timeout&lt;/code&gt;, and fetches run on native &lt;code&gt;fetch&lt;/code&gt; with &lt;code&gt;AbortSignal.any&lt;/code&gt; / &lt;code&gt;AbortSignal.timeout&lt;/code&gt;. A source that hangs gets cut off at its deadline and degrades to &lt;code&gt;stale&lt;/code&gt; (or &lt;code&gt;error&lt;/code&gt; if there's no cached data), so it can't hold the rest of the dashboard hostage. This is the whole reason the &lt;code&gt;fetch(ctx)&lt;/code&gt; signature forces you to honor &lt;code&gt;ctx.signal&lt;/code&gt;: a source that ignores cancellation is a bug the contract makes hard to write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure is per-panel, never global.&lt;/strong&gt; Because state is per-source and the UI renders each &lt;code&gt;SourceState&lt;/code&gt; independently, one dead endpoint costs you exactly one panel. The other ten keep refreshing. There is no shared failure mode where a single bad API takes down the view.&lt;/p&gt;

&lt;p&gt;The result is a dashboard that behaves well precisely when the network doesn't, which, for a tool you run first thing every morning on whatever connection you woke up to, is the only behavior that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extending it, with the compiler as a guardrail
&lt;/h2&gt;

&lt;p&gt;Adding a new source is four small steps, and the type system walks you through all of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the config variant to the Zod &lt;code&gt;discriminatedUnion&lt;/code&gt; in &lt;code&gt;config/schema.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Write &lt;code&gt;fooSource(cfg): Source&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Wire it into the registry's &lt;code&gt;switch&lt;/code&gt;, which maps a config entry to a live source.&lt;/li&gt;
&lt;li&gt;Add a panel body under &lt;code&gt;ui/panels/&lt;/code&gt; and register it in &lt;code&gt;bodyFor()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The registry &lt;code&gt;switch&lt;/code&gt; ends in a &lt;code&gt;never&lt;/code&gt; exhaustiveness guard, so a config kind you forgot to handle is a &lt;em&gt;compile&lt;/em&gt; error, not a runtime surprise you discover in production at 7am. Between that and the Zod schema validating config at load time, the two failure classes that plague this kind of tool (malformed config and unhandled source types) are both caught before the dashboard ever renders. You extend the union, and TypeScript tells you everything you still have to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets without plaintext in your config
&lt;/h2&gt;

&lt;p&gt;Every config field that holds a credential accepts a &lt;strong&gt;secret reference&lt;/strong&gt; rather than a raw value, resolved by a small reference-resolver in &lt;code&gt;core/secrets.ts&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reference&lt;/th&gt;
&lt;th&gt;Resolves to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keychain:NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OS keychain (service &lt;code&gt;reveille&lt;/code&gt;, account &lt;code&gt;NAME&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;env:NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;environment variable &lt;code&gt;NAME&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cmd:&amp;lt;command&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stdout of a shell command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;used as a literal value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The default advice is &lt;code&gt;keychain:&lt;/code&gt; for anything the dashboard reads unattended, because &lt;code&gt;env:&lt;/code&gt; refs only resolve when the variables are actually exported. An &lt;code&gt;env:&lt;/code&gt;-based source silently fails when you launch reveille from a bare prompt with no &lt;code&gt;.env&lt;/code&gt; loaded, and I'd rather the tool steer you away from that trap than let you rediscover it.&lt;/p&gt;

&lt;p&gt;The keychain support (&lt;code&gt;keytar&lt;/code&gt;) is an &lt;em&gt;optional&lt;/em&gt; native dependency. If it fails to build or load, which native modules love to do, reveille degrades to &lt;code&gt;REVEILLE_SECRET_*&lt;/code&gt; environment variables instead of crashing on startup. Same philosophy as the sources: a missing capability should narrow what works, not break the whole thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, and why
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript, end-to-end, ESM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renderer&lt;/td&gt;
&lt;td&gt;Ink (React for the terminal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Node 20.12+ (native &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;--env-file&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;native &lt;code&gt;fetch&lt;/code&gt; + &lt;code&gt;AbortSignal.any/timeout&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;TOML via &lt;code&gt;@iarna/toml&lt;/code&gt;, validated with Zod&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache&lt;/td&gt;
&lt;td&gt;atomic JSON on disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;OS keychain via &lt;code&gt;keytar&lt;/code&gt;, env-var fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;vitest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ink lets me describe the UI as React components and get flexbox layout in a terminal for free, which is what makes the masonry reflow trivial. Leaning on a modern Node runtime meant no HTTP client dependency and no &lt;code&gt;dotenv&lt;/code&gt;: native &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;AbortSignal&lt;/code&gt;, and &lt;code&gt;--env-file&lt;/code&gt; cover it. And because the sources are pure and rendering-free, the test suite is mostly about the interesting behavior: does a timeout produce &lt;code&gt;stale&lt;/code&gt;, does a cache hit hydrate before the network resolves, does the exhaustiveness guard hold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;     &lt;span class="c"&gt;# keytar is optional; a failed native build is non-fatal&lt;/span&gt;
npm run dev     &lt;span class="c"&gt;# run from source with tsx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first run with no config, reveille offers to scaffold a starter one at &lt;code&gt;~/.config/reveille/config.toml&lt;/code&gt; (or run &lt;code&gt;reveille init&lt;/code&gt;). You get a clock and a headline panel out of the box; edit the config to add sources.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reveille&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Launch the interactive dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reveille init&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scaffold a starter config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reveille doctor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validate config and check every credential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reveille login google&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authorize Google Calendar; stores a refresh token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reveille --once&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Print a one-line summary and exit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Press &lt;code&gt;q&lt;/code&gt; or Ctrl-C to quit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's going
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Source&amp;lt;T&amp;gt;&lt;/code&gt; contract makes the roadmap mostly a question of "what else do I want on screen at 7am." Because adding a feed is a source plus a panel, the answer is cheap to act on: RSS, CI status, a train departure board, whatever. The &lt;code&gt;--once&lt;/code&gt; summary mode also hints at a direction I like: the same sources, rendered as a prompt line, a status bar, a notification. The data layer is decoupled enough that none of those need the dashboard at all.&lt;/p&gt;

&lt;p&gt;It's MIT-licensed and built to be the tool I actually reach for, which remains the only spec I trust.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Keanred/reveille" rel="noopener noreferrer"&gt;https://github.com/Keanred/reveille&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>cli</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
