Repository: vividn/datum
Pull request #912: Feature/add nowview
Author: login: justinshenk
Created: 2025-04-25T12:24:50Z | Merged: 2025-05-08T11:37:46Z

Added nowview for viewing past 15 minutes and current state.

<img width="418" alt="image" src="https://github.com/user-attachments/assets/875a5ccc-35ea-4796-9f66-da2807264b64" />



Diff:
diff --git a/.gitignore b/.gitignore
index 726cdd0e..3c144f49 100644
--- a/.gitignore
+++ b/.gitignore
@@ -111,3 +111,6 @@ build/
 !*.secret
 .vscode/
 .pouch
+
+# Mac
+.DS_Store
diff --git a/src/commands/__test__/__snapshots__/dayviewCmd.test.ts.snap b/src/commands/__test__/__snapshots__/dayviewCmd.test.ts.snap
index 32c29170..c5592295 100644
--- a/src/commands/__test__/__snapshots__/dayviewCmd.test.ts.snap
+++ b/src/commands/__test__/__snapshots__/dayviewCmd.test.ts.snap
@@ -88,13 +88,13 @@ Duration: 30m" data-end-time="2024-11-20T09:10:00.000Z" data-time="2024-11-20T08
                     </svg>
                     <svg height="12" width="916" y="21.72283353842795" x="0" preserveAspectRatio="none" viewBox="0 0 916 12" class="project">
                         <defs>
-                            <pattern patternTransform="rotate(45)" height="12" width="6.361111184735083" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-419.8333381925155">
-                                <rect fill="#af67ca" height="36" width="3.1805555923675417" y="-12" x="0"/>
-                                <rect fill="#2cb1ad" height="36" width="3.1805555923675417" y="-12" x="3.1805555923675417"/>
+                            <pattern patternTransform="rotate(45)" height="12" width="16" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-419.8333381925155">
+                                <rect fill="#af67ca" height="36" width="8" y="-12" x="0"/>
+                                <rect fill="#2cb1ad" height="36" width="8" y="-12" x="8"/>
                             </pattern>
-                            <pattern patternTransform="rotate(45)" height="12" width="6.361111184735083" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-438.91667174672074">
-                                <rect fill="#af67ca" height="36" width="3.1805555923675417" y="-12" x="0"/>
-                                <rect fill="#2cb1ad" height="36" width="3.1805555923675417" y="-12" x="3.1805555923675417"/>
+                            <pattern patternTransform="rotate(45)" height="12" width="16" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-438.91667174672074">
+                                <rect fill="#af67ca" height="36" width="8" y="-12" x="0"/>
+                                <rect fill="#2cb1ad" height="36" width="8" y="-12" x="8"/>
                             </pattern>
                             <style>
                                 .popover {
diff --git a/src/commands/__test__/__snapshots__/sample_morning.svg b/src/commands/__test__/__snapshots__/sample_morning.svg
index 60dddf3a..3d26bc53 100644
--- a/src/commands/__test__/__snapshots__/sample_morning.svg
+++ b/src/commands/__test__/__snapshots__/sample_morning.svg
@@ -85,13 +85,13 @@ Duration: 30m" data-end-time="2024-11-20T09:10:00.000Z" data-time="2024-11-20T08
                     </svg>
                     <svg height="12" width="916" y="21.72283353842795" x="0" preserveAspectRatio="none" viewBox="0 0 916 12" class="project">
                         <defs>
-                            <pattern patternTransform="rotate(45)" height="12" width="6.361111184735083" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-419.8333381925155">
-                                <rect fill="#af67ca" height="36" width="3.1805555923675417" y="-12" x="0"/>
-                                <rect fill="#2cb1ad" height="36" width="3.1805555923675417" y="-12" x="3.1805555923675417"/>
+                            <pattern patternTransform="rotate(45)" height="12" width="16" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-419.8333381925155">
+                                <rect fill="#af67ca" height="36" width="8" y="-12" x="0"/>
+                                <rect fill="#2cb1ad" height="36" width="8" y="-12" x="8"/>
                             </pattern>
-                            <pattern patternTransform="rotate(45)" height="12" width="6.361111184735083" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-438.91667174672074">
-                                <rect fill="#af67ca" height="36" width="3.1805555923675417" y="-12" x="0"/>
-                                <rect fill="#2cb1ad" height="36" width="3.1805555923675417" y="-12" x="3.1805555923675417"/>
+                            <pattern patternTransform="rotate(45)" height="12" width="16" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-438.91667174672074">
+                                <rect fill="#af67ca" height="36" width="8" y="-12" x="0"/>
+                                <rect fill="#2cb1ad" height="36" width="8" y="-12" x="8"/>
                             </pattern>
                             <style>
                                 .popover {
diff --git a/src/commands/nowviewCmd.ts b/src/commands/nowviewCmd.ts
new file mode 100644
index 00000000..c59679c9
--- /dev/null
+++ b/src/commands/nowviewCmd.ts
@@ -0,0 +1,80 @@
+import { ArgumentParser } from "argparse";
+import { dbArgs } from "../input/dbArgs";
+import { MainDatumArgs } from "../input/mainArgs";
+import { nowview } from "../dayview/nowview";
+import { parseIfNeeded } from "../utils/parseIfNeeded";
+import { connectDb } from "../auth/connectDb";
+
+export const nowviewArgs = new ArgumentParser({
+  add_help: false,
+  parents: [],
+});
+
+nowviewArgs.add_argument("--width", {
+  help: "width of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--height", {
+  help: "height of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--output-file", "-o", {
+  help: "output file, should have a .html or .svg extension",
+  type: "str",
+  dest: "outputFile",
+});
+
+nowviewArgs.add_argument("--time-axis-height", {
+  help: "height of the time axis",
+  type: "int",
+  dest: "timeAxisHeight",
+});
+
+nowviewArgs.add_argument("--watch", "-w", {
+  help: "watch for changes and update every minute",
+  action: "store_true",
+  dest: "watch",
+});
+
+export const nowviewCmdArgs = new ArgumentParser({
+  description: "View the current state and last 15 minutes",
+  prog: "datum nowview",
+  usage: `%(prog)s`,
+  parents: [nowviewArgs, dbArgs],
+});
+
+export type NowviewCmdArgs = MainDatumArgs & {
+  width?: number;
+  height?: number;
+  outputFile?: string;
+  timeAxisHeight?: number;
+  watch?: boolean;
+};
+
+export async function nowviewCmd(
+  args: NowviewCmdArgs | string | string[],
+  preparsed?: Partial<NowviewCmdArgs>,
+): Promise<string> {
+  args = parseIfNeeded(nowviewCmdArgs, args, preparsed);
+  const db = connectDb(args);
+
+  if (args.watch) {
+    console.log("watching");
+    const emitter = db.changes({ since: "now", live: true });
+    while (true) {
+      console.log("redrawing");
+      await Promise.all([
+        nowview(args),
+        // redraw every minute or when data changes
+        new Promise((resolve) => {
+          setTimeout(resolve, 1000 * 60);
+          emitter.once("change", resolve);
+        }),
+      ]);
+    }
+  }
+
+  return await nowview(args);
+}
diff --git a/src/dayview/dayview.ts b/src/dayview/dayview.ts
index 4cf63cc6..92b54ffd 100644
--- a/src/dayview/dayview.ts
+++ b/src/dayview/dayview.ts
@@ -72,7 +72,7 @@ export async function dayview(args: DayviewCmdArgs): Promise<string> {
     }
   }
 
-  const document = domdoc();
+  const document = domdoc("dayview");
 
   const svg = d3
     .select(document.body)
@@ -245,7 +245,6 @@ export async function dayview(args: DayviewCmdArgs): Promise<string> {
     return prettySvg;
   }
   if (outputFile.endsWith(".svg")) {
-    // Add XML declaration, SVG namespace, and ensure all required namespaces are included
     const xmlDeclaration =
       '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
     fs.writeFileSync(outputFile, xmlDeclaration + prettySvg);
diff --git a/src/dayview/domdoc.ts b/src/dayview/domdoc.ts
index 59454c5e..b4fa9692 100644
--- a/src/dayview/domdoc.ts
+++ b/src/dayview/domdoc.ts
@@ -1,12 +1,16 @@
 import { parseHTML } from "linkedom";
 
-export function domdoc() {
+export function domdoc(title: string = "") {
   const { document } = parseHTML(`
 <!DOCTYPE html>
 <html>
   <head>
-    <title>dayview</title>
-    <script type="text/javascript" src="https://livejs.com/live.js"></script>
+    <meta charset="UTF-8">
+    <title>${title}</title>
+    <style>
+      body { margin: 0; padding: 0; background: #000; }
+      svg { display: block; margin: 0 auto; }
+    </style>
   </head>
   <body style="background-color: black">
   </body>
diff --git a/src/dayview/fieldSvgBlocks.ts b/src/dayview/fieldSvgBlocks.ts
index 74e88abb..4c067d45 100644
--- a/src/dayview/fieldSvgBlocks.ts
+++ b/src/dayview/fieldSvgBlocks.ts
@@ -92,10 +92,16 @@ export async function fieldSvgBlocks(args: FieldSvgBlocksType) {
     .range([0, width]);
 
   // When there are multiple states, create stripes
-  const stripeWidthMinutes = 5;
-  const stripeWidth =
-    timeScale(new Date(startUtc).valueOf() + stripeWidthMinutes * 60 * 1000) -
-    timeScale(new Date(startUtc).valueOf());
+  // const stripeWidthMinutesonFullDay = 5;
+  // const windowWidth = new Date(endUtc).valueOf() - new Date(startUtc).valueOf();
+  // const fullDayMs = 24 * 60 * 60 * 1000;
+  // const stripeWidthMinutes =
+  //   (windowWidth / fullDayMs) * stripeWidthMinutesonFullDay;
+  // const stripeWidthMinutes = 5;
+  // const stripeWidth =
+  //   timeScale(new Date(startUtc).valueOf() + stripeWidthMinutes * 60 * 1000) -
+  //   timeScale(new Date(startUtc).valueOf());
+  const stripeWidth = 8;
 
   // Add SVG definitions for patterns
   const defs = svg.append("defs");
diff --git a/src/dayview/nowview.ts b/src/dayview/nowview.ts
new file mode 100644
index 00000000..fcc9aa24
--- /dev/null
+++ b/src/dayview/nowview.ts
@@ -0,0 +1,203 @@
+import fs from "fs";
+import * as d3 from "d3";
+import { connectDb } from "../auth/connectDb";
+import { NowviewCmdArgs } from "../commands/nowviewCmd";
+import { domdoc } from "./domdoc";
+import { allFieldsSvg } from "../dayview/allFieldsSvg";
+import xmlFormatter from "xml-formatter";
+import { now } from "../time/timeUtils";
+import { DateTime } from "luxon";
+
+const DEFAULT_WIDTH = 400;
+const DEFAULT_HEIGHT = 300;
+const DEFAULT_TIME_AXIS_HEIGHT = 15;
+const TIMELINE_WIDTH_RATIO = 0.6;
+const STATE_PANEL_WIDTH_RATIO = 0.4;
+
+export async function nowview(args: NowviewCmdArgs): Promise<string> {
+  const db = connectDb(args);
+
+  // Setup dimensions
+  const width = args.width ?? DEFAULT_WIDTH;
+  const height = args.height ?? DEFAULT_HEIGHT;
+  const margin = { top: 30, right: 2, bottom: 15, left: 2 };
+  const timeAxisHeight = args.timeAxisHeight ?? DEFAULT_TIME_AXIS_HEIGHT;
+
+  const plotWidth = width - margin.left - margin.right;
+  const plotHeight = height - margin.top - margin.bottom;
+  const dataHeight = plotHeight - timeAxisHeight;
+
+  const endTime = now() as DateTime<true>;
+  const startTime = endTime.minus({ minutes: 15 });
+  const startUtc = startTime.toUTC().toISO();
+  const endUtc = endTime.toUTC().toISO();
+
+  const document = domdoc("nowview");
+  const svg = d3
+    .select(document.body)
+    .append("svg")
+    .attr("xmlns", "http://www.w3.org/2000/svg")
+    .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
+    .attr("width", width)
+    .attr("height", height);
+
+  // Add defs and styles like in dayview
+  const defs = svg.append("defs");
+  defs.append("style").text(`svg { overflow: visible; }`);
+
+  // Add background
+  svg
+    .append("rect")
+    .attr("width", "100%")
+    .attr("height", "100%")
+    .attr("fill", "black");
+
+  // Create main plot area similar to dayview
+  const plot = svg
+    .append("svg")
+    .attr("class", "plot")
+    .attr("x", margin.left)
+    .attr("y", margin.top)
+    .attr("width", plotWidth)
+    .attr("height", plotHeight);
+
+  // Add data area
+  const dataWidth = plotWidth * TIMELINE_WIDTH_RATIO;
+
+  const dataArea = plot
+    .append("svg")
+    .attr("class", "dataArea")
+    .attr("width", dataWidth)
+    .attr("height", dataHeight)
+    .attr("x", 0);
+
+  // Get data using allFieldsSvg for timeline (past 15 minutes)
+  const timelineFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc,
+    endUtc,
+    width: dataWidth,
+    height: dataHeight,
+  });
+
+  if (timelineFieldsSvg) {
+    dataArea.append(() => timelineFieldsSvg);
+  }
+
+  // Create selected state panel
+  const stateWidth = plotWidth * STATE_PANEL_WIDTH_RATIO;
+  const stateArea = plot
+    .append("svg")
+    .attr("class", "stateArea")
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("x", dataWidth);
+
+  // Get current state data using allFieldsSvg with a tiny time window
+  const sliverBeforeEndUtc = endTime.minus({ milliseconds: 1 }).toUTC().toISO(); // Small window to get current state
+
+  const stateFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc: sliverBeforeEndUtc,
+    endUtc: endUtc,
+    width: stateWidth,
+    height: dataHeight,
+  });
+
+  if (stateFieldsSvg) {
+    stateArea.append(() => stateFieldsSvg);
+  }
+
+  // Add border around state panel
+  stateArea
+    .append("rect")
+    .attr("x", 0)
+    .attr("y", 0)
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("fill", "none")
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-opacity", 0.7);
+
+  // Create time scale with current time at right edge of timeline
+  const timeScale = d3
+    .scaleTime()
+    .domain([startTime.toJSDate(), endTime.toJSDate()])
+    .range([0, dataWidth]);
+
+  // Add a vertical line indicating the current time
+  plot
+    .append("line")
+    .attr("class", "current-time-line")
+    .attr("x1", timeScale(endTime.toJSDate()))
+    .attr("x2", timeScale(endTime.toJSDate()))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-dasharray", "4,2");
+
+  const tickValues = [
+    endTime.toJSDate(),
+    endTime.minus({ minutes: 5 }).toJSDate(),
+    endTime.minus({ minutes: 10 }).toJSDate(),
+    endTime.minus({ minutes: 15 }).toJSDate(),
+  ];
+
+  const timeAxis = plot
+    .append("g")
+    .attr("transform", `translate(0, ${dataHeight})`)
+    .call(
+      d3
+        .axisBottom(timeScale)
+        .tickValues(tickValues)
+        .tickFormat((d) => {
+          const date = d as Date;
+          const diffMinutes = Math.round(
+            (date.getTime() - endTime.toJSDate().getTime()) / 60000,
+          );
+          return diffMinutes === 0
+            ? `${endTime.hour}:${endTime.minute.toString().padStart(2, "0")}`
+            : `${diffMinutes}m`;
+        }),
+    );
+
+  timeAxis.selectAll("text").attr("fill", "white");
+  timeAxis.selectAll("line").attr("stroke", "white");
+  timeAxis.selectAll("path").attr("stroke", "white");
+
+  // Add vertical grid lines at the tick positions
+  plot
+    .append("g")
+    .selectAll(".gridline")
+    .data(tickValues)
+    .enter()
+    .append("line")
+    .attr("class", "gridline")
+    .attr("x1", (d) => timeScale(d))
+    .attr("x2", (d) => timeScale(d))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "white")
+    .attr("stroke-opacity", 0.4);
+
+  const outputFile = args.outputFile;
+  const prettySvg = xmlFormatter(svg.node()!.outerHTML);
+
+  if (outputFile === undefined) {
+    return prettySvg;
+  }
+  if (outputFile.endsWith(".svg")) {
+    const xmlDeclaration =
+      '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
+    fs.writeFileSync(outputFile, xmlDeclaration + prettySvg);
+    return prettySvg;
+  } else if (outputFile.endsWith(".html")) {
+    const prettyHtml = xmlFormatter(document.documentElement.outerHTML);
+    fs.writeFileSync(outputFile, prettyHtml);
+    return prettySvg;
+  } else {
+    throw new Error("output file must have a .html or .svg extension");
+  }
+}
diff --git a/src/input/mainArgs.ts b/src/input/mainArgs.ts
index 16bad948..86a840a5 100644
--- a/src/input/mainArgs.ts
+++ b/src/input/mainArgs.ts
@@ -23,6 +23,7 @@ import { restoreCmd } from "../commands/restoreCmd";
 import { migrateCmd } from "../commands/migrateCmd";
 import { checkCmd } from "../commands/checkCmd";
 import { dayviewCmd } from "../commands/dayviewCmd";
+import { nowviewCmd } from "../commands/nowviewCmd";
 import { syncCmd } from "../commands/syncCmd";
 import { retimeCmd } from "../commands/retimeCmd";
 
@@ -159,6 +160,9 @@ export async function datum(cliInput: string | string[]): Promise<void> {
       await dayviewCmd(args, namespace);
       break;
 
+    case "nowview":
+      await nowviewCmd(args, namespace);
+      break;
     // case "test": {
     //   const parser = new ArgumentParser({});
     //   parser.add_argument("--test", {


Reviews:
user: login: vividn
body: The nowview looks nice and is a good starting point for me to continue building it out in the future. Thanks @JustinShenk . There seemed to be some problems with the svg generation though. I'll fix up the issues and comments myself though to save some time and money, though please look through my changes to better understand what I mean
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-05-07T15:58:38Z
user: login: vividn
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-07T16:00:51Z
user: login: justinshenk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:02:17Z
user: login: justinshenk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:03:02Z
user: login: justinshenk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:31:39Z
user: login: justinshenk
body: Thanks for pointing out the existing functionality which can be reused, and other helpful feedback.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:04:36Z
user: login: vividn
body: LGTM
state: APPROVED
author_association: OWNER
submitted_at: 2025-05-08T11:36:49Z

Review comments:
diff_hunk: @@ -173,11 +176,6 @@ export async function datum(cliInput: string | string[]): Promise<void> {
       await syncCmd(args, namespace);
       break;
 
-    case "retime":
-    case "rt":
-      await retimeCmd(args, namespace);
-      break;
-
path: src/input/mainArgs.ts
user: login: vividn
body: Why did you remove the retime command?
created_at: 2025-05-05T14:59:00Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,156 @@
+import { ArgumentParser } from "argparse";
+import { dbArgs } from "../input/dbArgs";
+import { MainDatumArgs } from "../input/mainArgs";
+import { nowview } from "../dayview/nowview";
+import { parseIfNeeded } from "../utils/parseIfNeeded";
+import { connectDb } from "../auth/connectDb";
+import fs from "fs";
+import xmlFormatter from "xml-formatter";
+
+export const nowviewArgs = new ArgumentParser({
+  add_help: false,
+  parents: [],
+});
+
+nowviewArgs.add_argument("--width", {
+  help: "width of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--height", {
+  help: "height of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--output-file", "-o", {
+  help: "output file, should have a .html or .svg extension",
+  type: "str",
+  dest: "outputFile",
+});
+
+nowviewArgs.add_argument("--watch", "-w", {
+  help: "watch for changes and update every minute",
+  action: "store_true",
+  dest: "watch",
+});
+
+export const nowviewCmdArgs = new ArgumentParser({
+  description: "View the current state and last 15 minutes",
+  prog: "datum nowview",
+  usage: `%(prog)s`,
+  parents: [nowviewArgs, dbArgs],
+});
+
+export type NowviewCmdArgs = MainDatumArgs & {
+  width?: number;
+  height?: number;
+  outputFile?: string;
+  timeAxisHeight?: number;
+  timeshift?: string;
+  watch?: boolean;
+};
+
+export async function nowviewCmd(
+  args: NowviewCmdArgs | string | string[],
+  preparsed?: Partial<NowviewCmdArgs>,
+): Promise<string> {
+  args = parseIfNeeded(nowviewCmdArgs, args, preparsed);
+  const db = connectDb(args);
+
+  if (args.watch) {
+    console.log("watching for changes");
+    const emitter = db.changes({ since: "now", live: true });
+
+    // Function to redraw the view
+    const redraw = async () => {
+      console.log("redrawing nowview");
+      const svgContent = await nowview(args);
+
+      if (args.outputFile) {
+        if (args.outputFile.endsWith(".svg")) {
+          const xmlDeclaration =
+            '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
+          const prettySvg = xmlFormatter(svgContent);
+          fs.writeFileSync(args.outputFile, xmlDeclaration + prettySvg);
+        } else if (args.outputFile.endsWith(".html")) {
+          const htmlContent = `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Nowview - Last 15 Minutes</title>
+  <style>
+    body { margin: 0; padding: 0; background: #000; }
+    svg { display: block; margin: 0 auto; }
+  </style>
+</head>
+<body>
+  ${svgContent}
+</body>
+</html>`;
+          const prettyHtml = xmlFormatter(htmlContent);
+          fs.writeFileSync(args.outputFile, prettyHtml);
+        } else {
+          throw new Error("output file must have a .html or .svg extension");
+        }
+      }
+
+      return svgContent;
+    };
+
+    // Initial draw
+    await redraw();
+
+    // Set up redraw on changes or every minute
+    while (true) {
+      await Promise.all([
+        // Wait for either a database change or 1 minute to pass
+        new Promise((resolve) => {
+          const timeout = setTimeout(resolve, 60 * 1000); // 1 minute
+          emitter.once("change", () => {
+            clearTimeout(timeout);
+            resolve(undefined);
+          });
+        }),
+        redraw(),
+      ]);
+    }
+  }
+
+  // Single run mode
+  const svgContent = await nowview(args);
+
+  if (args.outputFile === undefined) {
+    return svgContent;
+  }
+
+  if (args.outputFile.endsWith(".svg")) {
+    // Add XML declaration, SVG namespace, and ensure all required namespaces are included
+    const xmlDeclaration =
+      '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
+    const prettySvg = xmlFormatter(svgContent);
+    fs.writeFileSync(args.outputFile, xmlDeclaration + prettySvg);
+    return prettySvg;
+  } else if (args.outputFile.endsWith(".html")) {
+    const htmlContent = `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Nowview - Last 15 Minutes</title>
+  <style>
+    body { margin: 0; padding: 0; background: #000; }
+    svg { display: block; margin: 0 auto; }
+  </style>
+</head>
+<body>
path: src/commands/nowviewCmd.ts
user: login: vividn
body: This isn't very DRY since all this code exists up above
created_at: 2025-05-07T15:15:27Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,202 @@
+import * as d3 from "d3";
+import { DateTime } from "luxon";
+import { connectDb } from "../auth/connectDb";
+import { NowviewCmdArgs } from "../commands/nowviewCmd";
+import { domdoc } from "./domdoc";
+import { getNowviewScript } from "./nowviewScript"; // TODO: Complete this
+import { allFieldsSvg } from "../dayview/allFieldsSvg";
+
+const DEFAULT_WIDTH = 400;
+const DEFAULT_HEIGHT = 300;
+const DEFAULT_TIME_AXIS_HEIGHT = 15;
+const TIMELINE_WIDTH_RATIO = 0.6;
+const STATE_PANEL_WIDTH_RATIO = 0.4;
+
+// Extract time-related calculations into a helper function
+function calculateTimeRange(timeShiftMinutes: number) {
+  const viewportEnd = DateTime.now().minus({ minutes: timeShiftMinutes });
+  const viewportStart = viewportEnd.minus({ minutes: 15 });
+  return {
+    viewportEnd,
+    viewportStart,
+    startUtc: viewportStart.toUTC().toISO(),
+    endUtc: viewportEnd.toUTC().toISO(),
+  };
+}
+
+export async function nowview(args: NowviewCmdArgs): Promise<string> {
+  const db = connectDb(args);
+
+  // Setup dimensions
+  const width = args.width ?? DEFAULT_WIDTH;
+  const height = args.height ?? DEFAULT_HEIGHT;
+  const margin = { top: 30, right: 2, bottom: 15, left: 2 };
+  const timeAxisHeight = args.timeAxisHeight ?? DEFAULT_TIME_AXIS_HEIGHT;
+
+  const plotWidth = width - margin.left - margin.right;
+  const plotHeight = height - margin.top - margin.bottom;
+  const dataHeight = plotHeight - timeAxisHeight;
+
+  const document = domdoc();
+  const svg = d3
+    .select(document.body)
+    .append("svg")
+    .attr("xmlns", "http://www.w3.org/2000/svg")
+    .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
+    .attr("width", width)
+    .attr("height", height);
+
+  // Add defs and styles like in dayview
+  const defs = svg.append("defs");
+  defs.append("style").text(`svg { overflow: visible; }`);
+
+  // Add background
+  svg
+    .append("rect")
+    .attr("width", "100%")
+    .attr("height", "100%")
+    .attr("fill", "black");
+
+  // Get timeshift and calculate time range
+  const timeShiftMinutes = args.timeshift ? parseInt(args.timeshift, 10) : 0;
+  const { viewportEnd, viewportStart, startUtc, endUtc } =
+    calculateTimeRange(timeShiftMinutes);
+
+  // Create main plot area similar to dayview
+  const plot = svg
+    .append("svg")
+    .attr("class", "plot")
+    .attr("x", margin.left)
+    .attr("y", margin.top)
+    .attr("width", plotWidth)
+    .attr("height", plotHeight);
+
+  // Add data area
+  const dataWidth = plotWidth * TIMELINE_WIDTH_RATIO;
+
+  const dataArea = plot
+    .append("svg")
+    .attr("class", "dataArea")
+    .attr("width", dataWidth)
+    .attr("height", dataHeight)
+    .attr("x", 0);
+
+  // Get data using allFieldsSvg for timeline (past 15 minutes)
+  const timelineFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc,
+    endUtc,
+    width: dataWidth,
+    height: dataHeight,
+  });
+
+  if (timelineFieldsSvg) {
+    dataArea.append(() => timelineFieldsSvg);
+  }
+
+  // Create selected state panel
+  const stateWidth = plotWidth * STATE_PANEL_WIDTH_RATIO;
+  const stateArea = plot
+    .append("svg")
+    .attr("class", "stateArea")
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("x", dataWidth);
+
+  // Get current state data using allFieldsSvg with a tiny time window
+  const stateEndUtc = viewportEnd.toUTC().toISO();
+  const stateStartUtc = viewportEnd.minus({ milliseconds: 1 }).toUTC().toISO(); // Small window to get current state
+
+  const stateFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc: stateStartUtc,
+    endUtc: stateEndUtc,
+    width: stateWidth,
+    height: dataHeight,
+  });
+
+  if (stateFieldsSvg) {
+    stateArea.append(() => stateFieldsSvg);
+  }
+
+  // Add border around state panel
+  stateArea
+    .append("rect")
+    .attr("x", 0)
+    .attr("y", 0)
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("fill", "none")
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-opacity", 0.7);
+
+  // Create time scale with current time at right edge of timeline
+  const timeScale = d3
+    .scaleTime()
+    .domain([viewportStart.toJSDate(), viewportEnd.toJSDate()])
+    .range([0, dataWidth]);
+
+  // Add a vertical line indicating the current time
+  plot
+    .append("line")
+    .attr("class", "current-time-line")
+    .attr("x1", timeScale(viewportEnd.toJSDate()))
+    .attr("x2", timeScale(viewportEnd.toJSDate()))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-dasharray", "4,2");
+
+  const tickValues = [
+    viewportEnd.toJSDate(),
+    viewportEnd.minus({ minutes: 5 }).toJSDate(),
+    viewportEnd.minus({ minutes: 10 }).toJSDate(),
+    viewportEnd.minus({ minutes: 15 }).toJSDate(),
+  ];
+
+  const timeAxis = plot
+    .append("g")
+    .attr("transform", `translate(0, ${dataHeight})`)
+    .call(
+      d3
+        .axisBottom(timeScale)
+        .tickValues(tickValues)
+        .tickFormat((d) => {
+          const date = d as Date;
+          const diffMinutes = Math.round(
+            (date.getTime() - viewportEnd.toJSDate().getTime()) / 60000,
+          );
+          return diffMinutes === 0
+            ? `${viewportEnd.hour}:${viewportEnd.minute.toString().padStart(2, "0")}`
+            : `${diffMinutes}m`;
+        }),
+    );
+
+  timeAxis.selectAll("text").attr("fill", "white");
+  timeAxis.selectAll("line").attr("stroke", "white");
+  timeAxis.selectAll("path").attr("stroke", "white");
+
+  // Add vertical grid lines at the tick positions
+  plot
+    .append("g")
+    .selectAll(".gridline")
+    .data(tickValues)
+    .enter()
+    .append("line")
+    .attr("class", "gridline")
+    .attr("x1", (d) => timeScale(d))
+    .attr("x2", (d) => timeScale(d))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "white")
+    .attr("stroke-opacity", 0.4);
+
+  // Add keyboard navigation script
+  const script = document.createElement("script");
+  script.textContent = getNowviewScript();
path: src/dayview/nowview.ts
user: login: vividn
body: Adding the script seems to break the .svg files (because it is for html). I'm going to remove the script for now anyway--but this is an important thing to consider for later
created_at: 2025-05-07T15:19:00Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,53 @@
+export const getNowviewScript = () => `
path: src/dayview/nowviewScript.ts
user: login: vividn
body: I'll move the nowview script to a separate branch for future study/use
feature/nowview-script
created_at: 2025-05-07T15:19:33Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,202 @@
+import * as d3 from "d3";
+import { DateTime } from "luxon";
+import { connectDb } from "../auth/connectDb";
+import { NowviewCmdArgs } from "../commands/nowviewCmd";
+import { domdoc } from "./domdoc";
+import { getNowviewScript } from "./nowviewScript"; // TODO: Complete this
+import { allFieldsSvg } from "../dayview/allFieldsSvg";
+
+const DEFAULT_WIDTH = 400;
+const DEFAULT_HEIGHT = 300;
+const DEFAULT_TIME_AXIS_HEIGHT = 15;
+const TIMELINE_WIDTH_RATIO = 0.6;
+const STATE_PANEL_WIDTH_RATIO = 0.4;
+
+// Extract time-related calculations into a helper function
+function calculateTimeRange(timeShiftMinutes: number) {
+  const viewportEnd = DateTime.now().minus({ minutes: timeShiftMinutes });
+  const viewportStart = viewportEnd.minus({ minutes: 15 });
+  return {
+    viewportEnd,
+    viewportStart,
+    startUtc: viewportStart.toUTC().toISO(),
+    endUtc: viewportEnd.toUTC().toISO(),
+  };
+}
path: src/dayview/nowview.ts
user: login: vividn
body: I'll remove this, no need to cache earlier times for now
created_at: 2025-05-07T15:38:03Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,202 @@
+import * as d3 from "d3";
+import { DateTime } from "luxon";
+import { connectDb } from "../auth/connectDb";
+import { NowviewCmdArgs } from "../commands/nowviewCmd";
+import { domdoc } from "./domdoc";
+import { getNowviewScript } from "./nowviewScript"; // TODO: Complete this
+import { allFieldsSvg } from "../dayview/allFieldsSvg";
+
+const DEFAULT_WIDTH = 400;
+const DEFAULT_HEIGHT = 300;
+const DEFAULT_TIME_AXIS_HEIGHT = 15;
+const TIMELINE_WIDTH_RATIO = 0.6;
+const STATE_PANEL_WIDTH_RATIO = 0.4;
+
+// Extract time-related calculations into a helper function
+function calculateTimeRange(timeShiftMinutes: number) {
+  const viewportEnd = DateTime.now().minus({ minutes: timeShiftMinutes });
+  const viewportStart = viewportEnd.minus({ minutes: 15 });
+  return {
+    viewportEnd,
+    viewportStart,
+    startUtc: viewportStart.toUTC().toISO(),
+    endUtc: viewportEnd.toUTC().toISO(),
+  };
+}
+
+export async function nowview(args: NowviewCmdArgs): Promise<string> {
+  const db = connectDb(args);
+
+  // Setup dimensions
+  const width = args.width ?? DEFAULT_WIDTH;
+  const height = args.height ?? DEFAULT_HEIGHT;
+  const margin = { top: 30, right: 2, bottom: 15, left: 2 };
+  const timeAxisHeight = args.timeAxisHeight ?? DEFAULT_TIME_AXIS_HEIGHT;
+
+  const plotWidth = width - margin.left - margin.right;
+  const plotHeight = height - margin.top - margin.bottom;
+  const dataHeight = plotHeight - timeAxisHeight;
+
+  const document = domdoc();
+  const svg = d3
+    .select(document.body)
+    .append("svg")
+    .attr("xmlns", "http://www.w3.org/2000/svg")
+    .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
+    .attr("width", width)
+    .attr("height", height);
+
+  // Add defs and styles like in dayview
+  const defs = svg.append("defs");
+  defs.append("style").text(`svg { overflow: visible; }`);
+
+  // Add background
+  svg
+    .append("rect")
+    .attr("width", "100%")
+    .attr("height", "100%")
+    .attr("fill", "black");
+
+  // Get timeshift and calculate time range
+  const timeShiftMinutes = args.timeshift ? parseInt(args.timeshift, 10) : 0;
+  const { viewportEnd, viewportStart, startUtc, endUtc } =
+    calculateTimeRange(timeShiftMinutes);
+
+  // Create main plot area similar to dayview
+  const plot = svg
+    .append("svg")
+    .attr("class", "plot")
+    .attr("x", margin.left)
+    .attr("y", margin.top)
+    .attr("width", plotWidth)
+    .attr("height", plotHeight);
+
+  // Add data area
+  const dataWidth = plotWidth * TIMELINE_WIDTH_RATIO;
+
+  const dataArea = plot
+    .append("svg")
+    .attr("class", "dataArea")
+    .attr("width", dataWidth)
+    .attr("height", dataHeight)
+    .attr("x", 0);
+
+  // Get data using allFieldsSvg for timeline (past 15 minutes)
+  const timelineFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc,
+    endUtc,
+    width: dataWidth,
+    height: dataHeight,
+  });
+
+  if (timelineFieldsSvg) {
+    dataArea.append(() => timelineFieldsSvg);
+  }
+
+  // Create selected state panel
+  const stateWidth = plotWidth * STATE_PANEL_WIDTH_RATIO;
+  const stateArea = plot
+    .append("svg")
+    .attr("class", "stateArea")
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("x", dataWidth);
+
+  // Get current state data using allFieldsSvg with a tiny time window
+  const stateEndUtc = viewportEnd.toUTC().toISO();
+  const stateStartUtc = viewportEnd.minus({ milliseconds: 1 }).toUTC().toISO(); // Small window to get current state
+
+  const stateFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc: stateStartUtc,
+    endUtc: stateEndUtc,
+    width: stateWidth,
+    height: dataHeight,
+  });
+
+  if (stateFieldsSvg) {
+    stateArea.append(() => stateFieldsSvg);
+  }
+
+  // Add border around state panel
+  stateArea
+    .append("rect")
+    .attr("x", 0)
+    .attr("y", 0)
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("fill", "none")
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-opacity", 0.7);
+
+  // Create time scale with current time at right edge of timeline
+  const timeScale = d3
+    .scaleTime()
+    .domain([viewportStart.toJSDate(), viewportEnd.toJSDate()])
+    .range([0, dataWidth]);
+
+  // Add a vertical line indicating the current time
+  plot
+    .append("line")
+    .attr("class", "current-time-line")
+    .attr("x1", timeScale(viewportEnd.toJSDate()))
+    .attr("x2", timeScale(viewportEnd.toJSDate()))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-dasharray", "4,2");
+
+  const tickValues = [
+    viewportEnd.toJSDate(),
+    viewportEnd.minus({ minutes: 5 }).toJSDate(),
+    viewportEnd.minus({ minutes: 10 }).toJSDate(),
+    viewportEnd.minus({ minutes: 15 }).toJSDate(),
+  ];
+
+  const timeAxis = plot
+    .append("g")
+    .attr("transform", `translate(0, ${dataHeight})`)
+    .call(
+      d3
+        .axisBottom(timeScale)
+        .tickValues(tickValues)
+        .tickFormat((d) => {
+          const date = d as Date;
+          const diffMinutes = Math.round(
+            (date.getTime() - viewportEnd.toJSDate().getTime()) / 60000,
+          );
+          return diffMinutes === 0
+            ? `${viewportEnd.hour}:${viewportEnd.minute.toString().padStart(2, "0")}`
+            : `${diffMinutes}m`;
+        }),
+    );
+
+  timeAxis.selectAll("text").attr("fill", "white");
+  timeAxis.selectAll("line").attr("stroke", "white");
+  timeAxis.selectAll("path").attr("stroke", "white");
+
+  // Add vertical grid lines at the tick positions
+  plot
+    .append("g")
+    .selectAll(".gridline")
+    .data(tickValues)
+    .enter()
+    .append("line")
+    .attr("class", "gridline")
+    .attr("x1", (d) => timeScale(d))
+    .attr("x2", (d) => timeScale(d))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "white")
+    .attr("stroke-opacity", 0.4);
+
+  // Add keyboard navigation script
+  const script = document.createElement("script");
+  script.textContent = getNowviewScript();
+  document.body.appendChild(script);
+
+  return document.documentElement.outerHTML;
path: src/dayview/nowview.ts
user: login: vividn
body: This should be more parallel to how dayview.ts is setup. The output file writing should be here and return just the prettySvg. Right now this mismatch causes the svg file outputs of nowview to have html elements in them and the svgs do not work.
created_at: 2025-05-07T15:42:00Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,156 @@
+import { ArgumentParser } from "argparse";
+import { dbArgs } from "../input/dbArgs";
+import { MainDatumArgs } from "../input/mainArgs";
+import { nowview } from "../dayview/nowview";
+import { parseIfNeeded } from "../utils/parseIfNeeded";
+import { connectDb } from "../auth/connectDb";
+import fs from "fs";
+import xmlFormatter from "xml-formatter";
+
+export const nowviewArgs = new ArgumentParser({
+  add_help: false,
+  parents: [],
+});
+
+nowviewArgs.add_argument("--width", {
+  help: "width of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--height", {
+  help: "height of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--output-file", "-o", {
+  help: "output file, should have a .html or .svg extension",
+  type: "str",
+  dest: "outputFile",
+});
+
+nowviewArgs.add_argument("--watch", "-w", {
+  help: "watch for changes and update every minute",
+  action: "store_true",
+  dest: "watch",
+});
+
+export const nowviewCmdArgs = new ArgumentParser({
+  description: "View the current state and last 15 minutes",
+  prog: "datum nowview",
+  usage: `%(prog)s`,
+  parents: [nowviewArgs, dbArgs],
+});
+
+export type NowviewCmdArgs = MainDatumArgs & {
+  width?: number;
+  height?: number;
+  outputFile?: string;
+  timeAxisHeight?: number;
+  timeshift?: string;
+  watch?: boolean;
+};
+
+export async function nowviewCmd(
+  args: NowviewCmdArgs | string | string[],
+  preparsed?: Partial<NowviewCmdArgs>,
+): Promise<string> {
+  args = parseIfNeeded(nowviewCmdArgs, args, preparsed);
+  const db = connectDb(args);
+
+  if (args.watch) {
+    console.log("watching for changes");
+    const emitter = db.changes({ since: "now", live: true });
+
+    // Function to redraw the view
+    const redraw = async () => {
+      console.log("redrawing nowview");
+      const svgContent = await nowview(args);
+
+      if (args.outputFile) {
+        if (args.outputFile.endsWith(".svg")) {
+          const xmlDeclaration =
+            '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
+          const prettySvg = xmlFormatter(svgContent);
+          fs.writeFileSync(args.outputFile, xmlDeclaration + prettySvg);
+        } else if (args.outputFile.endsWith(".html")) {
+          const htmlContent = `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Nowview - Last 15 Minutes</title>
+  <style>
+    body { margin: 0; padding: 0; background: #000; }
+    svg { display: block; margin: 0 auto; }
+  </style>
+</head>
+<body>
+  ${svgContent}
+</body>
+</html>`;
+          const prettyHtml = xmlFormatter(htmlContent);
+          fs.writeFileSync(args.outputFile, prettyHtml);
+        } else {
+          throw new Error("output file must have a .html or .svg extension");
+        }
+      }
+
+      return svgContent;
+    };
+
+    // Initial draw
+    await redraw();
+
+    // Set up redraw on changes or every minute
+    while (true) {
+      await Promise.all([
+        // Wait for either a database change or 1 minute to pass
+        new Promise((resolve) => {
+          const timeout = setTimeout(resolve, 60 * 1000); // 1 minute
+          emitter.once("change", () => {
+            clearTimeout(timeout);
+            resolve(undefined);
+          });
+        }),
+        redraw(),
+      ]);
+    }
+  }
+
+  // Single run mode
+  const svgContent = await nowview(args);
+
+  if (args.outputFile === undefined) {
+    return svgContent;
+  }
+
+  if (args.outputFile.endsWith(".svg")) {
+    // Add XML declaration, SVG namespace, and ensure all required namespaces are included
+    const xmlDeclaration =
+      '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
+    const prettySvg = xmlFormatter(svgContent);
+    fs.writeFileSync(args.outputFile, xmlDeclaration + prettySvg);
+    return prettySvg;
+  } else if (args.outputFile.endsWith(".html")) {
+    const htmlContent = `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Nowview - Last 15 Minutes</title>
+  <style>
+    body { margin: 0; padding: 0; background: #000; }
+    svg { display: block; margin: 0 auto; }
+  </style>
+</head>
+<body>
path: src/commands/nowviewCmd.ts
user: login: vividn
body: Furthermore, the domdoc() function already sets up all the head and style elements, so I've moved your additions here into that
created_at: 2025-05-07T15:55:23Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,53 @@
+export const getNowviewScript = () => `
path: src/dayview/nowviewScript.ts
user: login: vividn
body: Okay moved
created_at: 2025-05-07T15:57:31Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,77 @@
+import { ArgumentParser } from "argparse";
+import { dbArgs } from "../input/dbArgs";
+import { MainDatumArgs } from "../input/mainArgs";
+import { nowview } from "../dayview/nowview";
+import { parseIfNeeded } from "../utils/parseIfNeeded";
+import { connectDb } from "../auth/connectDb";
+import fs from "fs";
+import xmlFormatter from "xml-formatter";
+
+export const nowviewArgs = new ArgumentParser({
+  add_help: false,
+  parents: [],
+});
+
+nowviewArgs.add_argument("--width", {
+  help: "width of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--height", {
+  help: "height of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--output-file", "-o", {
+  help: "output file, should have a .html or .svg extension",
+  type: "str",
+  dest: "outputFile",
+});
+
+nowviewArgs.add_argument("--watch", "-w", {
+  help: "watch for changes and update every minute",
+  action: "store_true",
+  dest: "watch",
+});
+
+export const nowviewCmdArgs = new ArgumentParser({
+  description: "View the current state and last 15 minutes",
+  prog: "datum nowview",
+  usage: `%(prog)s`,
+  parents: [nowviewArgs, dbArgs],
+});
+
+export type NowviewCmdArgs = MainDatumArgs & {
+  width?: number;
+  height?: number;
+  outputFile?: string;
+  timeAxisHeight?: number;
+  timeshift?: string;
path: src/commands/nowviewCmd.ts
user: login: vividn
body: You added these two to the type, but forgot to actually arguments for them to the argparser
created_at: 2025-05-07T16:00:51Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,156 @@
+import { ArgumentParser } from "argparse";
+import { dbArgs } from "../input/dbArgs";
+import { MainDatumArgs } from "../input/mainArgs";
+import { nowview } from "../dayview/nowview";
+import { parseIfNeeded } from "../utils/parseIfNeeded";
+import { connectDb } from "../auth/connectDb";
+import fs from "fs";
+import xmlFormatter from "xml-formatter";
+
+export const nowviewArgs = new ArgumentParser({
+  add_help: false,
+  parents: [],
+});
+
+nowviewArgs.add_argument("--width", {
+  help: "width of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--height", {
+  help: "height of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--output-file", "-o", {
+  help: "output file, should have a .html or .svg extension",
+  type: "str",
+  dest: "outputFile",
+});
+
+nowviewArgs.add_argument("--watch", "-w", {
+  help: "watch for changes and update every minute",
+  action: "store_true",
+  dest: "watch",
+});
+
+export const nowviewCmdArgs = new ArgumentParser({
+  description: "View the current state and last 15 minutes",
+  prog: "datum nowview",
+  usage: `%(prog)s`,
+  parents: [nowviewArgs, dbArgs],
+});
+
+export type NowviewCmdArgs = MainDatumArgs & {
+  width?: number;
+  height?: number;
+  outputFile?: string;
+  timeAxisHeight?: number;
+  timeshift?: string;
+  watch?: boolean;
+};
+
+export async function nowviewCmd(
+  args: NowviewCmdArgs | string | string[],
+  preparsed?: Partial<NowviewCmdArgs>,
+): Promise<string> {
+  args = parseIfNeeded(nowviewCmdArgs, args, preparsed);
+  const db = connectDb(args);
+
+  if (args.watch) {
+    console.log("watching for changes");
+    const emitter = db.changes({ since: "now", live: true });
+
+    // Function to redraw the view
+    const redraw = async () => {
+      console.log("redrawing nowview");
+      const svgContent = await nowview(args);
+
+      if (args.outputFile) {
+        if (args.outputFile.endsWith(".svg")) {
+          const xmlDeclaration =
+            '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
+          const prettySvg = xmlFormatter(svgContent);
+          fs.writeFileSync(args.outputFile, xmlDeclaration + prettySvg);
+        } else if (args.outputFile.endsWith(".html")) {
+          const htmlContent = `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Nowview - Last 15 Minutes</title>
+  <style>
+    body { margin: 0; padding: 0; background: #000; }
+    svg { display: block; margin: 0 auto; }
+  </style>
+</head>
+<body>
+  ${svgContent}
+</body>
+</html>`;
+          const prettyHtml = xmlFormatter(htmlContent);
+          fs.writeFileSync(args.outputFile, prettyHtml);
+        } else {
+          throw new Error("output file must have a .html or .svg extension");
+        }
+      }
+
+      return svgContent;
+    };
+
+    // Initial draw
+    await redraw();
+
+    // Set up redraw on changes or every minute
+    while (true) {
+      await Promise.all([
+        // Wait for either a database change or 1 minute to pass
+        new Promise((resolve) => {
+          const timeout = setTimeout(resolve, 60 * 1000); // 1 minute
+          emitter.once("change", () => {
+            clearTimeout(timeout);
+            resolve(undefined);
+          });
+        }),
+        redraw(),
+      ]);
+    }
+  }
+
+  // Single run mode
+  const svgContent = await nowview(args);
+
+  if (args.outputFile === undefined) {
+    return svgContent;
+  }
+
+  if (args.outputFile.endsWith(".svg")) {
+    // Add XML declaration, SVG namespace, and ensure all required namespaces are included
+    const xmlDeclaration =
+      '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n';
+    const prettySvg = xmlFormatter(svgContent);
+    fs.writeFileSync(args.outputFile, xmlDeclaration + prettySvg);
+    return prettySvg;
+  } else if (args.outputFile.endsWith(".html")) {
+    const htmlContent = `
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Nowview - Last 15 Minutes</title>
+  <style>
+    body { margin: 0; padding: 0; background: #000; }
+    svg { display: block; margin: 0 auto; }
+  </style>
+</head>
+<body>
path: src/commands/nowviewCmd.ts
user: login: justinshenk
body: Good catch!
created_at: 2025-05-08T10:02:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,202 @@
+import * as d3 from "d3";
+import { DateTime } from "luxon";
+import { connectDb } from "../auth/connectDb";
+import { NowviewCmdArgs } from "../commands/nowviewCmd";
+import { domdoc } from "./domdoc";
+import { getNowviewScript } from "./nowviewScript"; // TODO: Complete this
+import { allFieldsSvg } from "../dayview/allFieldsSvg";
+
+const DEFAULT_WIDTH = 400;
+const DEFAULT_HEIGHT = 300;
+const DEFAULT_TIME_AXIS_HEIGHT = 15;
+const TIMELINE_WIDTH_RATIO = 0.6;
+const STATE_PANEL_WIDTH_RATIO = 0.4;
+
+// Extract time-related calculations into a helper function
+function calculateTimeRange(timeShiftMinutes: number) {
+  const viewportEnd = DateTime.now().minus({ minutes: timeShiftMinutes });
+  const viewportStart = viewportEnd.minus({ minutes: 15 });
+  return {
+    viewportEnd,
+    viewportStart,
+    startUtc: viewportStart.toUTC().toISO(),
+    endUtc: viewportEnd.toUTC().toISO(),
+  };
+}
+
+export async function nowview(args: NowviewCmdArgs): Promise<string> {
+  const db = connectDb(args);
+
+  // Setup dimensions
+  const width = args.width ?? DEFAULT_WIDTH;
+  const height = args.height ?? DEFAULT_HEIGHT;
+  const margin = { top: 30, right: 2, bottom: 15, left: 2 };
+  const timeAxisHeight = args.timeAxisHeight ?? DEFAULT_TIME_AXIS_HEIGHT;
+
+  const plotWidth = width - margin.left - margin.right;
+  const plotHeight = height - margin.top - margin.bottom;
+  const dataHeight = plotHeight - timeAxisHeight;
+
+  const document = domdoc();
+  const svg = d3
+    .select(document.body)
+    .append("svg")
+    .attr("xmlns", "http://www.w3.org/2000/svg")
+    .attr("xmlns:xlink", "http://www.w3.org/1999/xlink")
+    .attr("width", width)
+    .attr("height", height);
+
+  // Add defs and styles like in dayview
+  const defs = svg.append("defs");
+  defs.append("style").text(`svg { overflow: visible; }`);
+
+  // Add background
+  svg
+    .append("rect")
+    .attr("width", "100%")
+    .attr("height", "100%")
+    .attr("fill", "black");
+
+  // Get timeshift and calculate time range
+  const timeShiftMinutes = args.timeshift ? parseInt(args.timeshift, 10) : 0;
+  const { viewportEnd, viewportStart, startUtc, endUtc } =
+    calculateTimeRange(timeShiftMinutes);
+
+  // Create main plot area similar to dayview
+  const plot = svg
+    .append("svg")
+    .attr("class", "plot")
+    .attr("x", margin.left)
+    .attr("y", margin.top)
+    .attr("width", plotWidth)
+    .attr("height", plotHeight);
+
+  // Add data area
+  const dataWidth = plotWidth * TIMELINE_WIDTH_RATIO;
+
+  const dataArea = plot
+    .append("svg")
+    .attr("class", "dataArea")
+    .attr("width", dataWidth)
+    .attr("height", dataHeight)
+    .attr("x", 0);
+
+  // Get data using allFieldsSvg for timeline (past 15 minutes)
+  const timelineFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc,
+    endUtc,
+    width: dataWidth,
+    height: dataHeight,
+  });
+
+  if (timelineFieldsSvg) {
+    dataArea.append(() => timelineFieldsSvg);
+  }
+
+  // Create selected state panel
+  const stateWidth = plotWidth * STATE_PANEL_WIDTH_RATIO;
+  const stateArea = plot
+    .append("svg")
+    .attr("class", "stateArea")
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("x", dataWidth);
+
+  // Get current state data using allFieldsSvg with a tiny time window
+  const stateEndUtc = viewportEnd.toUTC().toISO();
+  const stateStartUtc = viewportEnd.minus({ milliseconds: 1 }).toUTC().toISO(); // Small window to get current state
+
+  const stateFieldsSvg = await allFieldsSvg({
+    db,
+    startUtc: stateStartUtc,
+    endUtc: stateEndUtc,
+    width: stateWidth,
+    height: dataHeight,
+  });
+
+  if (stateFieldsSvg) {
+    stateArea.append(() => stateFieldsSvg);
+  }
+
+  // Add border around state panel
+  stateArea
+    .append("rect")
+    .attr("x", 0)
+    .attr("y", 0)
+    .attr("width", stateWidth)
+    .attr("height", dataHeight)
+    .attr("fill", "none")
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-opacity", 0.7);
+
+  // Create time scale with current time at right edge of timeline
+  const timeScale = d3
+    .scaleTime()
+    .domain([viewportStart.toJSDate(), viewportEnd.toJSDate()])
+    .range([0, dataWidth]);
+
+  // Add a vertical line indicating the current time
+  plot
+    .append("line")
+    .attr("class", "current-time-line")
+    .attr("x1", timeScale(viewportEnd.toJSDate()))
+    .attr("x2", timeScale(viewportEnd.toJSDate()))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "#ffcc00")
+    .attr("stroke-width", 2)
+    .attr("stroke-dasharray", "4,2");
+
+  const tickValues = [
+    viewportEnd.toJSDate(),
+    viewportEnd.minus({ minutes: 5 }).toJSDate(),
+    viewportEnd.minus({ minutes: 10 }).toJSDate(),
+    viewportEnd.minus({ minutes: 15 }).toJSDate(),
+  ];
+
+  const timeAxis = plot
+    .append("g")
+    .attr("transform", `translate(0, ${dataHeight})`)
+    .call(
+      d3
+        .axisBottom(timeScale)
+        .tickValues(tickValues)
+        .tickFormat((d) => {
+          const date = d as Date;
+          const diffMinutes = Math.round(
+            (date.getTime() - viewportEnd.toJSDate().getTime()) / 60000,
+          );
+          return diffMinutes === 0
+            ? `${viewportEnd.hour}:${viewportEnd.minute.toString().padStart(2, "0")}`
+            : `${diffMinutes}m`;
+        }),
+    );
+
+  timeAxis.selectAll("text").attr("fill", "white");
+  timeAxis.selectAll("line").attr("stroke", "white");
+  timeAxis.selectAll("path").attr("stroke", "white");
+
+  // Add vertical grid lines at the tick positions
+  plot
+    .append("g")
+    .selectAll(".gridline")
+    .data(tickValues)
+    .enter()
+    .append("line")
+    .attr("class", "gridline")
+    .attr("x1", (d) => timeScale(d))
+    .attr("x2", (d) => timeScale(d))
+    .attr("y1", 0)
+    .attr("y2", dataHeight)
+    .attr("stroke", "white")
+    .attr("stroke-opacity", 0.4);
+
+  // Add keyboard navigation script
+  const script = document.createElement("script");
+  script.textContent = getNowviewScript();
path: src/dayview/nowview.ts
user: login: justinshenk
body: Noted
created_at: 2025-05-08T10:03:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,77 @@
+import { ArgumentParser } from "argparse";
+import { dbArgs } from "../input/dbArgs";
+import { MainDatumArgs } from "../input/mainArgs";
+import { nowview } from "../dayview/nowview";
+import { parseIfNeeded } from "../utils/parseIfNeeded";
+import { connectDb } from "../auth/connectDb";
+import fs from "fs";
+import xmlFormatter from "xml-formatter";
+
+export const nowviewArgs = new ArgumentParser({
+  add_help: false,
+  parents: [],
+});
+
+nowviewArgs.add_argument("--width", {
+  help: "width of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--height", {
+  help: "height of the svg",
+  type: "int",
+});
+
+nowviewArgs.add_argument("--output-file", "-o", {
+  help: "output file, should have a .html or .svg extension",
+  type: "str",
+  dest: "outputFile",
+});
+
+nowviewArgs.add_argument("--watch", "-w", {
+  help: "watch for changes and update every minute",
+  action: "store_true",
+  dest: "watch",
+});
+
+export const nowviewCmdArgs = new ArgumentParser({
+  description: "View the current state and last 15 minutes",
+  prog: "datum nowview",
+  usage: `%(prog)s`,
+  parents: [nowviewArgs, dbArgs],
+});
+
+export type NowviewCmdArgs = MainDatumArgs & {
+  width?: number;
+  height?: number;
+  outputFile?: string;
+  timeAxisHeight?: number;
+  timeshift?: string;
path: src/commands/nowviewCmd.ts
user: login: justinshenk
body: Good catch
created_at: 2025-05-08T10:31:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,11 +176,6 @@ export async function datum(cliInput: string | string[]): Promise<void> {
       await syncCmd(args, namespace);
       break;
 
-    case "retime":
-    case "rt":
-      await retimeCmd(args, namespace);
-      break;
-
path: src/input/mainArgs.ts
user: login: justinshenk
body: That was unintended, looks like we added it back at the same time
created_at: 2025-05-08T10:55:30Z
author_association: CONTRIBUTOR

PR comments:
user: login: justinshenk
created_at: 2025-05-02T13:38:21Z
body: Refactored, removing unused code, fixing lint errors, and adding default constants to top. 
Future possible improvements include:
- make the viewport time window configurable
- clean up time calculation logic
- add tests
- convert inline script in `nowviewScript.ts` to proper TypeScript by setting up a web server
- complete time navigation feature
author_association: CONTRIBUTOR
user: login: justinshenk
created_at: 2025-05-02T14:00:17Z
body: I'd like to add `nowviewCmd.test.ts`, but I'm getting an error on dayview test when testing locally with `pnpm run test`. Can @vividn confirm this is expected usage?

```
Snapshot Summary
 › 1 snapshot written from 1 test suite.
 › 1 snapshot failed from 1 test suite. Inspect your code changes or run `npm test -- -u` to update them.

Test Suites: 1 failed, 1 skipped, 81 passed, 82 of 83 total
Tests:       1 failed, 4 skipped, 73 todo, 1036 passed, 1114 total
Snapshots:   1 failed, 1 written, 64 passed, 66 total
Time:        20.878 s
Ran all test suites.
```
author_association: CONTRIBUTOR
user: login: vividn
created_at: 2025-05-02T14:24:15Z
body: Go ahead and update the dayview snapshot, something probably changed
`pnpm run test dayview -- -u`
author_association: OWNER
user: login: justinshenk
created_at: 2025-05-08T11:30:39Z
body: Still getting a test error, I'm not sure if this is expected behavior or something in my config/snapshot files:

```
pnpm run test dayview -- -u

> @vividn/datum@2.1.0-alpha.0 test /Users/justinshenk/projects/datum
> jest "dayview" "--" "-u"

 FAIL  src/commands/__test__/dayviewCmd.test.ts
  dayview
    ✕ consistently generates a sample morning svg (1097 ms)

  ● dayview › consistently generates a sample morning svg

    expect(received).toMatchSnapshot()

    Snapshot name: `dayview consistently generates a sample morning svg 1`

    - Snapshot  - 6
    + Received  + 6

    @@ -83,17 +83,17 @@
          })();
                              </script>
                          </svg>
                          <svg height="12" width="916" y="21.72283353842795" x="0" preserveAspectRatio="none" viewBox="0 0 916 12" class="project">
                              <defs>
    -                             <pattern patternTransform="rotate(45)" height="12" width="6.361111184735083" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-419.8333381925155">
    -                                 <rect fill="#af67ca" height="36" width="3.1805555923675417" y="-12" x="0"/>
    -                                 <rect fill="#2cb1ad" height="36" width="3.1805555923675417" y="-12" x="3.1805555923675417"/>
    +                             <pattern patternTransform="rotate(45)" height="12" width="12.722222224522971" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-419.8333381925155">
    +                                 <rect fill="#af67ca" height="36" width="6.361111112261486" y="-12" x="0"/>
    +                                 <rect fill="#2cb1ad" height="36" width="6.361111112261486" y="-12" x="6.361111112261486"/>
                                  </pattern>
    -                             <pattern patternTransform="rotate(45)" height="12" width="6.361111184735083" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-438.91667174672074">
    -                                 <rect fill="#af67ca" height="36" width="3.1805555923675417" y="-12" x="0"/>
    -                                 <rect fill="#2cb1ad" height="36" width="3.1805555923675417" y="-12" x="3.1805555923675417"/>
    +                             <pattern patternTransform="rotate(45)" height="12" width="12.722222224522971" patternUnits="userSpaceOnUse" id="stripe-pattern-emails-tasks-438.91667174672074">
    +                                 <rect fill="#af67ca" height="36" width="6.361111112261486" y="-12" x="0"/>
    +                                 <rect fill="#2cb1ad" height="36" width="6.361111112261486" y="-12" x="6.361111112261486"/>
                                  </pattern>
                                  <style>
                                      .popover {
            position: absolute;
            background: rgba(0, 0, 0, 0.8);

      29 |     });
      30 |     const svg = fs.readFileSync("/tmp/sample_morning.svg", "utf8");
    > 31 |     expect(svg).toMatchSnapshot();
         |                 ^
      32 |     // write svg to snapshot folder too for github viewing
      33 |     fs.writeFileSync(__dirname + "/__snapshots__/sample_morning.svg", svg);
      34 |   });

      at Object.toMatchSnapshot (src/commands/__test__/dayviewCmd.test.ts:31:17)

 › 1 snapshot failed.
Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or run `npm test -- -u` to update them.

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 failed, 1 total
Time:        2.134 s, estimated 4 s
Ran all test suites matching /dayview|-u/i.
 ELIFECYCLE  Test failed. See above for more details.
 
 ```
author_association: CONTRIBUTOR
user: login: justinshenk
created_at: 2025-05-08T11:33:09Z
body: Fixed it with `pnpm test -- -u`.
author_association: CONTRIBUTOR
user: login: vividn
created_at: 2025-05-08T11:36:29Z
body: Yeah, I changed the stripe width again so the dayview snapshot broke. If this goes green, I'll merge it
author_association: OWNER