daily-focus 1.1.1-dev.1 → 1.1.1-dev.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SETUP.md CHANGED
@@ -98,7 +98,7 @@ under **Morning agent** in the settings. In `.env` or the environment that is:
98
98
  DAILY_FOCUS_AGENT=codex
99
99
  ```
100
100
 
101
- Each scheduled morning at 07:00 local time the dashboard starts the agent through
101
+ Each scheduled morning at 07:00 your time the dashboard starts the agent through
102
102
  that CLI, headless, in the store, and the brief appears when it finishes. Set
103
103
  `DAILY_FOCUS_AGENT_AT` to another time, such as `06:30`. The dashboard has to be
104
104
  running at the time; if it wasn't, it runs the agent as soon as it is started that
@@ -110,6 +110,15 @@ otherwise; see [Match your working week](#match-your-working-week).
110
110
  refresh icon still does. A new store's first brief always comes from the setup
111
111
  steps' button, so the clock never runs the agent before you've told it where to look.
112
112
 
113
+ "Your time" is the time zone of the browser you last looked at the dashboard
114
+ in. The page tells the dashboard when it loads, reconnects or comes back into view,
115
+ and when the browser's own zone changes, so a dashboard on a Mac at home follows
116
+ you abroad as soon as you open it there. The dashboard also follows the Mac it runs
117
+ on when that Mac changes time zones, which covers the morning after a flight before
118
+ you have opened it. Whichever changed last wins, and the dashboard remembers it
119
+ across restarts. Every date and time on the page is shown in that zone; a browser
120
+ somewhere else says so under the page title.
121
+
113
122
  Whichever way it starts, every run lands in the same place: the report it gives when
114
123
  it finishes, and a chat where you can ask it why it did what it did. See
115
124
  [The morning agent's runs](#the-morning-agents-runs).
@@ -1,3 +1,7 @@
1
+ import {
2
+ readSystemZone,
3
+ startingZone
4
+ } from "./chunk-AUBJ2IDQ.js";
1
5
  import {
2
6
  linkState
3
7
  } from "./chunk-QKT2FAB5.js";
@@ -8,7 +12,7 @@ import {
8
12
  readArchiveIndex,
9
13
  resolveItems,
10
14
  runContractChecks
11
- } from "./chunk-UM2EPNWR.js";
15
+ } from "./chunk-C6FXWHGT.js";
12
16
  import {
13
17
  BRIEF_REFRESH_GRACE_HOURS,
14
18
  describeSchedule,
@@ -18,7 +22,7 @@ import {
18
22
  resolveSchedule,
19
23
  runsOn,
20
24
  workingMsBetween
21
- } from "./chunk-OPASVYLR.js";
25
+ } from "./chunk-NWYOCDUU.js";
22
26
  import {
23
27
  command
24
28
  } from "./chunk-BC2W7NND.js";
@@ -27,6 +31,7 @@ import {
27
31
  import { readFile, readdir } from "node:fs/promises";
28
32
  import { resolve } from "node:path";
29
33
  var config = loadConfig();
34
+ process.env.TZ = (await startingZone(config.timeZoneFile, await readSystemZone())).timeZone;
30
35
  var store = new Store(config);
31
36
  var failures = 0;
32
37
  var warnings = 0;
@@ -0,0 +1,107 @@
1
+ import {
2
+ writeJsonAtomic
3
+ } from "./chunk-C6FXWHGT.js";
4
+ import {
5
+ localTimeZone
6
+ } from "./chunk-NWYOCDUU.js";
7
+
8
+ // src/zone.ts
9
+ import { readFile, readlink } from "node:fs/promises";
10
+ function validZone(raw) {
11
+ if (typeof raw !== "string" || !/^[A-Za-z][A-Za-z0-9_+\-/]*$/.test(raw)) return null;
12
+ try {
13
+ new Intl.DateTimeFormat("en", { timeZone: raw });
14
+ return raw;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ async function readSystemZone(path = "/etc/localtime") {
20
+ try {
21
+ const target = await readlink(path);
22
+ const name = /zoneinfo\/(?:posix\/|right\/)?(.+)$/.exec(target)?.[1];
23
+ return validZone(name);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ async function startingZone(file, system) {
29
+ const saved = await loadZoneFile(file);
30
+ if (!saved) return { timeZone: localTimeZone(), moved: false };
31
+ if (system && saved.systemTimeZone && system !== saved.systemTimeZone) return { timeZone: system, moved: true };
32
+ return { timeZone: saved.timeZone, moved: false };
33
+ }
34
+ async function loadZoneFile(file) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(await readFile(file, "utf8"));
38
+ } catch {
39
+ return null;
40
+ }
41
+ const record = parsed ?? {};
42
+ const timeZone = validZone(record.timeZone);
43
+ if (!timeZone) return null;
44
+ return { timeZone, systemTimeZone: validZone(record.systemTimeZone) };
45
+ }
46
+ var TimeZoneKeeper = class {
47
+ #file;
48
+ #readSystem;
49
+ #current = localTimeZone();
50
+ /** The Mac's zone as last read, so `checkSystem` acts on a change rather than a difference. */
51
+ #system = null;
52
+ constructor(file, readSystem = readSystemZone) {
53
+ this.#file = file;
54
+ this.#readSystem = readSystem;
55
+ }
56
+ get current() {
57
+ return this.#current;
58
+ }
59
+ /** Decide the zone at startup; see `startingZone`. */
60
+ async start() {
61
+ this.#system = await this.#readSystem();
62
+ const { timeZone, moved } = await startingZone(this.#file, this.#system);
63
+ if (moved) await this.#adopt(timeZone);
64
+ else this.#apply(timeZone);
65
+ }
66
+ /** A browser says it is in `zone`. True if that changed the dashboard's. */
67
+ async report(zone) {
68
+ if (zone === this.#current) return false;
69
+ await this.#adopt(zone);
70
+ return true;
71
+ }
72
+ /** Follow the Mac if its zone has changed since last read. True if that changed the dashboard's. */
73
+ async checkSystem() {
74
+ const system = await this.#readSystem();
75
+ if (!system || system === this.#system) return false;
76
+ this.#system = system;
77
+ if (system === this.#current) {
78
+ await this.#save();
79
+ return false;
80
+ }
81
+ await this.#adopt(system);
82
+ return true;
83
+ }
84
+ async #adopt(zone) {
85
+ this.#apply(zone);
86
+ await this.#save();
87
+ }
88
+ #apply(zone) {
89
+ this.#current = zone;
90
+ process.env.TZ = zone;
91
+ }
92
+ async #save() {
93
+ const file = { timeZone: this.#current, systemTimeZone: this.#system };
94
+ try {
95
+ await writeJsonAtomic(this.#file, file);
96
+ } catch (err) {
97
+ console.warn(`[daily-focus] could not save the time zone: ${err.message}`);
98
+ }
99
+ }
100
+ };
101
+
102
+ export {
103
+ validZone,
104
+ readSystemZone,
105
+ startingZone,
106
+ TimeZoneKeeper
107
+ };
@@ -14,7 +14,7 @@ import {
14
14
  startOfLocalDay,
15
15
  workingDaysSince,
16
16
  workingMsBetween
17
- } from "./chunk-OPASVYLR.js";
17
+ } from "./chunk-NWYOCDUU.js";
18
18
 
19
19
  // src/fs.ts
20
20
  import { mkdir, rename, writeFile } from "node:fs/promises";
@@ -139,7 +139,7 @@ var claudeAdapter = {
139
139
  if (model) args.push("--model", model);
140
140
  if (effort) args.push("--effort", effort);
141
141
  if (sessionId) args.push("--resume", sessionId);
142
- args.push(prompt);
142
+ args.push("--", prompt);
143
143
  return args;
144
144
  },
145
145
  parse(line) {
@@ -444,6 +444,7 @@ function loadConfig(env = loadEnv()) {
444
444
  pullsFile: resolve2(dataDir, "prs.json"),
445
445
  calendarFile: resolve2(dataDir, "calendar.json"),
446
446
  ticketsFile: resolve2(dataDir, "tickets.json"),
447
+ timeZoneFile: resolve2(dataDir, "timezone.json"),
447
448
  // The same 1 to 120 the session endpoint holds a start to.
448
449
  sessionMinutes: envBounded("DAILY_FOCUS_SESSION_MINUTES", 25, 1, 120, env),
449
450
  awayAfterMinutes: envBounded("DAILY_FOCUS_AWAY_AFTER", profile === "personal" ? 0 : 10, 0, 24 * 60, env),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  localDateKey
3
- } from "./chunk-OPASVYLR.js";
3
+ } from "./chunk-NWYOCDUU.js";
4
4
 
5
5
  // src/sample.ts
6
6
  function timeOn(now, hour, minute = 0) {
package/dist/cli.js CHANGED
@@ -49,7 +49,7 @@ function shouldOpen(flag) {
49
49
  return flag && process.stdout.isTTY === true && !process.env.CI;
50
50
  }
51
51
  async function demo(open, profile) {
52
- const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-HTFKCJJ5.js");
52
+ const { demoEnv, demoIntegrations, demoProfile, writeDemoStore } = await import("./demo-AMK3Y4JZ.js");
53
53
  const chosen = demoProfile(profile);
54
54
  const store = await mkdtemp(join(tmpdir(), "daily-focus-demo-"));
55
55
  process.on("exit", () => rmSync(store, { recursive: true, force: true }));
@@ -65,7 +65,7 @@ async function demo(open, profile) {
65
65
  );
66
66
  }
67
67
  async function start(open, onStarted, options = {}) {
68
- const { runServer } = await import("./server-JJVEH4ME.js");
68
+ const { runServer } = await import("./server-5HCA62YY.js");
69
69
  await runServer({
70
70
  ...options,
71
71
  onStarted: (url) => {
@@ -162,20 +162,20 @@ async function main(argv) {
162
162
  }
163
163
  return start(open);
164
164
  case "service": {
165
- const { main: service } = await import("./service-6ZFOHPWN.js");
165
+ const { main: service } = await import("./service-63U4WBUH.js");
166
166
  process.exitCode = await service(values.remove ? ["--remove"] : []);
167
167
  return;
168
168
  }
169
169
  // These three are scripts that do their work as they load, and read their
170
170
  // own flags from the command line.
171
171
  case "init":
172
- await import("./init-DEVCAQZZ.js");
172
+ await import("./init-YGOPPELJ.js");
173
173
  return;
174
174
  case "audit":
175
- await import("./audit-W3O6CLOF.js");
175
+ await import("./audit-B5GKGH2P.js");
176
176
  return;
177
177
  case "seed":
178
- await import("./seed-SMKNFYTC.js");
178
+ await import("./seed-57RJGU4Y.js");
179
179
  return;
180
180
  case "build-calendar":
181
181
  return buildCalendar();
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  QUICK_ACTIONS
3
- } from "./chunk-F3EHZWRS.js";
3
+ } from "./chunk-GFCWPYEI.js";
4
4
  import {
5
5
  dateFromNow,
6
6
  sampleBrief,
7
7
  sampleGeneratedAt
8
- } from "./chunk-Q5OKXJXT.js";
8
+ } from "./chunk-YWHADSKR.js";
9
9
  import {
10
10
  PROFILES,
11
11
  localDateKey
12
- } from "./chunk-OPASVYLR.js";
12
+ } from "./chunk-NWYOCDUU.js";
13
13
  import "./chunk-BC2W7NND.js";
14
14
 
15
15
  // src/demo.ts
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-QKT2FAB5.js";
9
9
  import {
10
10
  loadConfig
11
- } from "./chunk-OPASVYLR.js";
11
+ } from "./chunk-NWYOCDUU.js";
12
12
  import {
13
13
  PACKAGED,
14
14
  command
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  sampleBrief
3
- } from "./chunk-Q5OKXJXT.js";
3
+ } from "./chunk-YWHADSKR.js";
4
4
  import {
5
5
  Store
6
- } from "./chunk-UM2EPNWR.js";
6
+ } from "./chunk-C6FXWHGT.js";
7
7
  import {
8
8
  loadConfig
9
- } from "./chunk-OPASVYLR.js";
9
+ } from "./chunk-NWYOCDUU.js";
10
10
  import {
11
11
  command
12
12
  } from "./chunk-BC2W7NND.js";
@@ -1,11 +1,15 @@
1
1
  import {
2
2
  ADAPTERS,
3
3
  Assistant
4
- } from "./chunk-F3EHZWRS.js";
4
+ } from "./chunk-GFCWPYEI.js";
5
5
  import {
6
6
  FOCUS_TEMPLATE,
7
7
  sourcesTemplate
8
8
  } from "./chunk-V2UN3JYL.js";
9
+ import {
10
+ TimeZoneKeeper,
11
+ validZone
12
+ } from "./chunk-AUBJ2IDQ.js";
9
13
  import {
10
14
  describeLink,
11
15
  linkStore,
@@ -20,7 +24,7 @@ import {
20
24
  stopSession,
21
25
  tempPathFor,
22
26
  writeJsonAtomic
23
- } from "./chunk-UM2EPNWR.js";
27
+ } from "./chunk-C6FXWHGT.js";
24
28
  import {
25
29
  CLI_NAMES,
26
30
  LAUNCH_SETTINGS,
@@ -36,7 +40,7 @@ import {
36
40
  readDotEnv,
37
41
  resolveSchedule,
38
42
  scheduledRunDue
39
- } from "./chunk-OPASVYLR.js";
43
+ } from "./chunk-NWYOCDUU.js";
40
44
  import {
41
45
  PACKAGED,
42
46
  command,
@@ -1488,7 +1492,8 @@ async function runHelper(appPath, selfAddresses) {
1488
1492
  const out = join(tmpdir(), `daily-focus-calendar-${process.pid}-${Date.now()}.json`);
1489
1493
  try {
1490
1494
  try {
1491
- await run2("/usr/bin/open", ["-n", "-a", appPath, "--args", out, selfAddresses.join(",")], {
1495
+ const zone = process.env.TZ ? ["--env", `TZ=${process.env.TZ}`] : [];
1496
+ await run2("/usr/bin/open", ["-n", "-a", appPath, ...zone, "--args", out, selfAddresses.join(",")], {
1492
1497
  timeout: HELPER_TIMEOUT_MS
1493
1498
  });
1494
1499
  } catch (err) {
@@ -3686,6 +3691,8 @@ async function startServer(env, options = {}) {
3686
3691
  const store = new Store(config);
3687
3692
  await store.ensureDataDir();
3688
3693
  const storeId = await store.fingerprint();
3694
+ const zone = new TimeZoneKeeper(config.timeZoneFile, options.readSystemZone);
3695
+ await zone.start();
3689
3696
  for (const outcome of await linkStore(config)) {
3690
3697
  if (outcome.result === "linked") console.log(`[daily-focus] linked ${describeLink(config, outcome)}`);
3691
3698
  if (outcome.result === "failed") console.warn(`[daily-focus] could not link ${outcome.name}: ${outcome.detail}`);
@@ -3781,6 +3788,8 @@ data: ${payload}
3781
3788
  basename(tempPathFor(config.pullsFile)),
3782
3789
  basename(config.ticketsFile),
3783
3790
  basename(tempPathFor(config.ticketsFile)),
3791
+ basename(config.timeZoneFile),
3792
+ basename(tempPathFor(config.timeZoneFile)),
3784
3793
  // Appended by this process, which broadcasts on its own.
3785
3794
  basename(config.assistantLogFile),
3786
3795
  basename(config.agentLogFile)
@@ -3822,10 +3831,23 @@ data: ${payload}
3822
3831
  await checkPresence();
3823
3832
  await assistant.start();
3824
3833
  await agent.start();
3825
- async function clockTick() {
3834
+ let ticks = Promise.resolve();
3835
+ function clockTick() {
3836
+ ticks = ticks.then(tickOnce).catch((err) => {
3837
+ console.error(`[daily-focus] the morning clock failed: ${err.message}`);
3838
+ });
3839
+ return ticks;
3840
+ }
3841
+ async function tickOnce() {
3842
+ if (await zone.checkSystem()) zoneMoved();
3826
3843
  if (restart === "waiting" || await firstBriefPending(config)) return;
3827
3844
  await agent.tick();
3828
3845
  }
3846
+ function zoneMoved() {
3847
+ console.log(`[daily-focus] time zone is now ${zone.current}`);
3848
+ void calendar.refresh();
3849
+ void broadcast();
3850
+ }
3829
3851
  void clockTick();
3830
3852
  const agentClock = setInterval(() => {
3831
3853
  void clockTick();
@@ -3909,6 +3931,26 @@ data: ${JSON.stringify(await buildState())}
3909
3931
  sendState(res, await buildState());
3910
3932
  return;
3911
3933
  }
3934
+ if (path === "/api/timezone" && req.method === "POST") {
3935
+ let body;
3936
+ try {
3937
+ body = JSON.parse(await readBody(req));
3938
+ } catch (err) {
3939
+ sendJSON(res, 400, { error: `invalid JSON body: ${err.message}` });
3940
+ return;
3941
+ }
3942
+ const timeZone = validZone(body?.timeZone);
3943
+ if (!timeZone) {
3944
+ sendJSON(res, 400, { error: '"timeZone" must be an IANA time zone, such as Europe/Copenhagen' });
3945
+ return;
3946
+ }
3947
+ if (await zone.report(timeZone)) {
3948
+ zoneMoved();
3949
+ await clockTick();
3950
+ }
3951
+ sendState(res, await buildState());
3952
+ return;
3953
+ }
3912
3954
  if (path === "/api/calendar/refresh" && req.method === "POST") {
3913
3955
  await calendar.refresh();
3914
3956
  sendState(res, await buildState());
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadConfig
3
- } from "./chunk-OPASVYLR.js";
3
+ } from "./chunk-NWYOCDUU.js";
4
4
  import {
5
5
  IN_NPX_CACHE,
6
6
  PACKAGED,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "daily-focus",
3
- "version": "1.1.1-dev.1",
3
+ "version": "1.1.1-dev.3",
4
4
  "description": "An interactive dashboard of what you should focus on today at work",
5
5
  "keywords": [
6
6
  "dashboard",
package/public/app.js CHANGED
@@ -1010,6 +1010,9 @@ function subscribe(onState, onError) {
1010
1010
  source.addEventListener("open", () => onError(null));
1011
1011
  return () => source.close();
1012
1012
  }
1013
+ function postTimeZone(timeZone) {
1014
+ return post("/api/timezone", { timeZone });
1015
+ }
1013
1016
  function postSession(body) {
1014
1017
  return post("/api/session", body);
1015
1018
  }
@@ -1293,15 +1296,28 @@ function ThemeToggle({ ui: ui2, handlers: handlers2 }) {
1293
1296
  }
1294
1297
 
1295
1298
  // client/format.ts
1296
- var timeFmt = new Intl.DateTimeFormat(void 0, { hour: "2-digit", minute: "2-digit" });
1297
- var dayFmt = new Intl.DateTimeFormat(void 0, {
1299
+ var DAY_FORMAT = {
1298
1300
  weekday: "long",
1299
1301
  day: "numeric",
1300
1302
  month: "long"
1301
- });
1302
- var shortDayFmt = new Intl.DateTimeFormat(void 0, { day: "numeric", month: "short" });
1303
- var shortDayYearFmt = new Intl.DateTimeFormat(void 0, { day: "numeric", month: "short", year: "numeric" });
1304
- var weekdayFmt = new Intl.DateTimeFormat(void 0, { weekday: "long" });
1303
+ };
1304
+ function formatters(timeZone) {
1305
+ return {
1306
+ time: new Intl.DateTimeFormat(void 0, { timeZone, hour: "2-digit", minute: "2-digit" }),
1307
+ day: new Intl.DateTimeFormat(void 0, { timeZone, ...DAY_FORMAT }),
1308
+ shortDay: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short" }),
1309
+ shortDayYear: new Intl.DateTimeFormat(void 0, { timeZone, day: "numeric", month: "short", year: "numeric" }),
1310
+ weekday: new Intl.DateTimeFormat(void 0, { timeZone, weekday: "long" })
1311
+ };
1312
+ }
1313
+ var clockFormats = formatters();
1314
+ var dateFormats = formatters("UTC");
1315
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
1316
+ function formatted(value, kind) {
1317
+ const bare = typeof value === "string" && DATE_ONLY.test(value);
1318
+ const d4 = bare ? /* @__PURE__ */ new Date(`${value}T00:00:00Z`) : parseDate(value);
1319
+ return d4 && !Number.isNaN(d4.getTime()) ? (bare ? dateFormats : clockFormats)[kind].format(d4) : "";
1320
+ }
1305
1321
  function parseDate(value) {
1306
1322
  if (typeof value !== "string" || value === "") return null;
1307
1323
  const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
@@ -1310,20 +1326,16 @@ function parseDate(value) {
1310
1326
  return Number.isNaN(d4.getTime()) ? null : d4;
1311
1327
  }
1312
1328
  function formatTime(value) {
1313
- const d4 = parseDate(value);
1314
- return d4 ? timeFmt.format(d4) : "";
1329
+ return formatted(value, "time");
1315
1330
  }
1316
1331
  function formatDay(value) {
1317
- const d4 = parseDate(value);
1318
- return d4 ? dayFmt.format(d4) : "";
1332
+ return formatted(value, "day");
1319
1333
  }
1320
1334
  function formatShortDay(value) {
1321
- const d4 = parseDate(value);
1322
- return d4 ? shortDayFmt.format(d4) : "";
1335
+ return formatted(value, "shortDay");
1323
1336
  }
1324
1337
  function formatWeekday(value) {
1325
- const d4 = parseDate(value);
1326
- return d4 ? weekdayFmt.format(d4) : "";
1338
+ return formatted(value, "weekday");
1327
1339
  }
1328
1340
  var dayKeyFmt = null;
1329
1341
  var calendarZone;
@@ -1331,9 +1343,11 @@ function setCalendarZone(timeZone) {
1331
1343
  if (timeZone === calendarZone) return;
1332
1344
  calendarZone = timeZone;
1333
1345
  try {
1346
+ clockFormats = formatters(timeZone);
1334
1347
  dayKeyFmt = timeZone ? new Intl.DateTimeFormat("en-US", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }) : null;
1335
1348
  } catch {
1336
1349
  dayKeyFmt = null;
1350
+ clockFormats = formatters();
1337
1351
  }
1338
1352
  }
1339
1353
  function dayKey(d4) {
@@ -1370,8 +1384,8 @@ function daysFromToday(value, now = /* @__PURE__ */ new Date()) {
1370
1384
  }
1371
1385
  const target = parseDate(value);
1372
1386
  if (!target) return null;
1373
- const a4 = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
1374
- const b4 = new Date(target.getFullYear(), target.getMonth(), target.getDate()).getTime();
1387
+ const a4 = Date.UTC(...splitKey(dayKey(now)));
1388
+ const b4 = Date.UTC(...splitKey(dayKey(target)));
1375
1389
  return Math.round((b4 - a4) / 864e5);
1376
1390
  }
1377
1391
  function relativeDay(value, now = /* @__PURE__ */ new Date()) {
@@ -1402,7 +1416,7 @@ function relativeTime(value, now = /* @__PURE__ */ new Date()) {
1402
1416
  if (hours < 48) return `${hours} h ago`;
1403
1417
  const days = Math.round(hours / 24);
1404
1418
  if (days <= 30) return `${days} days ago`;
1405
- return d4.getFullYear() === now.getFullYear() ? formatShortDay(value) : shortDayYearFmt.format(d4);
1419
+ return dayKey(d4).slice(0, 4) === dayKey(now).slice(0, 4) ? formatShortDay(value) : formatted(value, "shortDayYear");
1406
1420
  }
1407
1421
  function firstLine(text) {
1408
1422
  const line = String(text).split("\n").find((l5) => l5.trim() !== "") ?? "";
@@ -3458,6 +3472,46 @@ function parkButton(row, ui2, handlers2, menuKey) {
3458
3472
  );
3459
3473
  }
3460
3474
 
3475
+ // client/zone.ts
3476
+ var ZONE_CHECK_MS = 6e4;
3477
+ function browserZone() {
3478
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
3479
+ }
3480
+ function sameZone(a4, b4) {
3481
+ if (a4 === b4) return true;
3482
+ try {
3483
+ const canonical = (zone2) => new Intl.DateTimeFormat("en", { timeZone: zone2 }).resolvedOptions().timeZone;
3484
+ return canonical(a4) === canonical(b4);
3485
+ } catch {
3486
+ return false;
3487
+ }
3488
+ }
3489
+ function watchZone(deps) {
3490
+ let seen = deps.browserZone();
3491
+ let sending = null;
3492
+ function tellIfElsewhere() {
3493
+ const here = deps.browserZone();
3494
+ const there = deps.dashboardZone();
3495
+ if (there === void 0 || sameZone(here, there) || sending === here) return;
3496
+ sending = here;
3497
+ deps.send(here).catch((err) => console.warn(`[daily-focus] could not tell the dashboard this browser is in ${here}:`, err)).finally(() => {
3498
+ sending = null;
3499
+ });
3500
+ }
3501
+ return {
3502
+ look() {
3503
+ seen = deps.browserZone();
3504
+ if (deps.visible()) tellIfElsewhere();
3505
+ },
3506
+ check() {
3507
+ const here = deps.browserZone();
3508
+ if (here === seen) return;
3509
+ seen = here;
3510
+ tellIfElsewhere();
3511
+ }
3512
+ };
3513
+ }
3514
+
3461
3515
  // client/chrome.ts
3462
3516
  function Header({ state: state2, ui: ui2, handlers: handlers2 }) {
3463
3517
  const error = ui2.connectionError.value;
@@ -3468,7 +3522,8 @@ function Header({ state: state2, ui: ui2, handlers: handlers2 }) {
3468
3522
  "div",
3469
3523
  { class: "page-header__titles" },
3470
3524
  el("h1", null, "Daily Focus"),
3471
- el("p", { class: "page-header__date", id: "header-date" }, state2 ? headerDate(state2) : "")
3525
+ el("p", { class: "page-header__date", id: "header-date" }, state2 ? headerDate(state2) : ""),
3526
+ state2 ? zoneLine(state2) : null
3472
3527
  ),
3473
3528
  el(
3474
3529
  "div",
@@ -3562,6 +3617,11 @@ function headerDate(state2) {
3562
3617
  const today = dayKey(new Date(state2.now));
3563
3618
  return formatDay(state2.brief.date ?? today) || formatDay(today);
3564
3619
  }
3620
+ function zoneLine(state2) {
3621
+ if (sameZone(browserZone(), state2.timeZone)) return null;
3622
+ const at = state2.agentRun?.schedule ? ` \xB7 Morning agent at ${state2.agentRun.schedule.at}` : "";
3623
+ return el("p", { class: "page-header__zone", id: "header-zone" }, `Times in ${state2.timeZone}${at}`);
3624
+ }
3565
3625
  function freshness(state2, handlers2) {
3566
3626
  let text;
3567
3627
  let stale;
@@ -5164,15 +5224,31 @@ j3(() => {
5164
5224
  installKeyboard(handlers);
5165
5225
  await refresh();
5166
5226
  startLocalTick();
5227
+ var zone = watchZone({
5228
+ browserZone,
5229
+ dashboardZone: () => state.value?.timeZone,
5230
+ visible: () => document.visibilityState === "visible",
5231
+ send: async (timeZone) => adoptState(await postTimeZone(timeZone))
5232
+ });
5233
+ zone.look();
5234
+ document.addEventListener("visibilitychange", () => zone.look());
5235
+ window.addEventListener("focus", () => zone.look());
5236
+ setInterval(() => zone.check(), ZONE_CHECK_MS);
5167
5237
  followLocation();
5168
5238
  window.addEventListener("popstate", followLocation);
5169
5239
  window.addEventListener("hashchange", followLocation);
5240
+ var reconnected = false;
5170
5241
  subscribe(
5171
5242
  // A server push is authoritative, but must not yank a row out from under an
5172
5243
  // in-flight click; adoptState keeps anything pending at its optimistic status.
5173
- (next) => adoptState(next),
5244
+ (next) => {
5245
+ adoptState(next);
5246
+ if (reconnected) zone.look();
5247
+ reconnected = false;
5248
+ },
5174
5249
  (err) => {
5175
5250
  ui.connectionError.value = err;
5251
+ if (err === null) reconnected = true;
5176
5252
  }
5177
5253
  );
5178
5254
  //# sourceMappingURL=app.js.map
package/public/style.css CHANGED
@@ -161,6 +161,12 @@ a {
161
161
  gap: 8px;
162
162
  }
163
163
 
164
+ .page-header__zone {
165
+ margin: 3px 0 0;
166
+ color: var(--ink-2);
167
+ font-size: 12px;
168
+ }
169
+
164
170
  .freshness {
165
171
  color: var(--ink-3);
166
172
  font-size: 13px;