daily-focus 1.1.1-dev.2 → 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 +10 -1
- package/dist/{audit-W3O6CLOF.js → audit-B5GKGH2P.js} +7 -2
- package/dist/chunk-AUBJ2IDQ.js +107 -0
- package/dist/{chunk-UM2EPNWR.js → chunk-C6FXWHGT.js} +1 -1
- package/dist/{chunk-OPASVYLR.js → chunk-NWYOCDUU.js} +1 -0
- package/dist/{chunk-Q5OKXJXT.js → chunk-YWHADSKR.js} +1 -1
- package/dist/cli.js +6 -6
- package/dist/{demo-OQLT3QWP.js → demo-AMK3Y4JZ.js} +2 -2
- package/dist/{init-DEVCAQZZ.js → init-YGOPPELJ.js} +1 -1
- package/dist/{seed-SMKNFYTC.js → seed-57RJGU4Y.js} +3 -3
- package/dist/{server-5O3LQVEY.js → server-5HCA62YY.js} +46 -4
- package/dist/{service-6ZFOHPWN.js → service-63U4WBUH.js} +1 -1
- package/package.json +1 -1
- package/public/app.js +95 -19
- package/public/style.css +6 -0
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
|
|
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-
|
|
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-
|
|
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
|
+
};
|
|
@@ -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),
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
172
|
+
await import("./init-YGOPPELJ.js");
|
|
173
173
|
return;
|
|
174
174
|
case "audit":
|
|
175
|
-
await import("./audit-
|
|
175
|
+
await import("./audit-B5GKGH2P.js");
|
|
176
176
|
return;
|
|
177
177
|
case "seed":
|
|
178
|
-
await import("./seed-
|
|
178
|
+
await import("./seed-57RJGU4Y.js");
|
|
179
179
|
return;
|
|
180
180
|
case "build-calendar":
|
|
181
181
|
return buildCalendar();
|
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
dateFromNow,
|
|
6
6
|
sampleBrief,
|
|
7
7
|
sampleGeneratedAt
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-YWHADSKR.js";
|
|
9
9
|
import {
|
|
10
10
|
PROFILES,
|
|
11
11
|
localDateKey
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-NWYOCDUU.js";
|
|
13
13
|
import "./chunk-BC2W7NND.js";
|
|
14
14
|
|
|
15
15
|
// src/demo.ts
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sampleBrief
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YWHADSKR.js";
|
|
4
4
|
import {
|
|
5
5
|
Store
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-C6FXWHGT.js";
|
|
7
7
|
import {
|
|
8
8
|
loadConfig
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-NWYOCDUU.js";
|
|
10
10
|
import {
|
|
11
11
|
command
|
|
12
12
|
} from "./chunk-BC2W7NND.js";
|
|
@@ -6,6 +6,10 @@ 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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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());
|
package/package.json
CHANGED
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
|
|
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
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1314
|
-
return d4 ? timeFmt.format(d4) : "";
|
|
1329
|
+
return formatted(value, "time");
|
|
1315
1330
|
}
|
|
1316
1331
|
function formatDay(value) {
|
|
1317
|
-
|
|
1318
|
-
return d4 ? dayFmt.format(d4) : "";
|
|
1332
|
+
return formatted(value, "day");
|
|
1319
1333
|
}
|
|
1320
1334
|
function formatShortDay(value) {
|
|
1321
|
-
|
|
1322
|
-
return d4 ? shortDayFmt.format(d4) : "";
|
|
1335
|
+
return formatted(value, "shortDay");
|
|
1323
1336
|
}
|
|
1324
1337
|
function formatWeekday(value) {
|
|
1325
|
-
|
|
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 =
|
|
1374
|
-
const b4 =
|
|
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.
|
|
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) =>
|
|
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
|