@harborppm/cli 0.57.0 → 0.59.0
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/README.md +10 -4
- package/dist/app.mjs +955 -51
- package/lib/cli.mjs +166 -49
- package/lib/format.mjs +34 -5
- package/package.json +1 -1
package/dist/app.mjs
CHANGED
|
@@ -34,12 +34,17 @@ var __exportAll = (all, no_symbols) => {
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region src/lib/clock.ts
|
|
37
|
+
/** A date's local wall-clock time, with no zone: "2026-10-05T23:10:00". */
|
|
38
|
+
function wallClock(d = /* @__PURE__ */ new Date()) {
|
|
39
|
+
return `${d.getFullYear()}-${pad$1(d.getMonth() + 1)}-${pad$1(d.getDate())}T${pad$1(d.getHours())}:${pad$1(d.getMinutes())}:${pad$1(d.getSeconds())}`;
|
|
40
|
+
}
|
|
37
41
|
function clockNow() {
|
|
38
42
|
return frozen ? new Date(frozen) : /* @__PURE__ */ new Date();
|
|
39
43
|
}
|
|
40
|
-
var frozen;
|
|
44
|
+
var frozen, pad$1;
|
|
41
45
|
var init_clock = __esmMin((() => {
|
|
42
46
|
frozen = null;
|
|
47
|
+
pad$1 = (n) => String(n).padStart(2, "0");
|
|
43
48
|
}));
|
|
44
49
|
|
|
45
50
|
//#endregion
|
|
@@ -2969,7 +2974,7 @@ const scale = (labels) => [
|
|
|
2969
2974
|
label: `${n} ${labels[n]}`,
|
|
2970
2975
|
value: n
|
|
2971
2976
|
}));
|
|
2972
|
-
const plus = (today, days) => format(addDays(parseIso(today), days), "yyyy-MM-dd");
|
|
2977
|
+
const plus$1 = (today, days) => format(addDays(parseIso(today), days), "yyyy-MM-dd");
|
|
2973
2978
|
/** The colour a new project or program takes: the first one its neighbours don't use. */
|
|
2974
2979
|
function nextSwatch(used) {
|
|
2975
2980
|
return SWATCHES.find((s) => !used.includes(s)) ?? SWATCHES[0];
|
|
@@ -3231,7 +3236,7 @@ function commitCreate(store, kind, v, ctx, today) {
|
|
|
3231
3236
|
ownerId: str("ownerId"),
|
|
3232
3237
|
programId,
|
|
3233
3238
|
startDate: today,
|
|
3234
|
-
endDate: str("endDate") ? clampEndToParent(str("endDate"), program.endDate, today) : reckoning ? reckonedEnd(reckoning, today) : clampEndToParent(plus(today, 60), program.endDate, today),
|
|
3239
|
+
endDate: str("endDate") ? clampEndToParent(str("endDate"), program.endDate, today) : reckoning ? reckonedEnd(reckoning, today) : clampEndToParent(plus$1(today, 60), program.endDate, today),
|
|
3235
3240
|
budget: typeof v.budget === "number" ? v.budget : 0
|
|
3236
3241
|
});
|
|
3237
3242
|
if (reckoning) applyReckoning(store.getState(), id, reckoning, today);
|
|
@@ -3247,7 +3252,7 @@ function commitCreate(store, kind, v, ctx, today) {
|
|
|
3247
3252
|
ownerId: str("ownerId"),
|
|
3248
3253
|
portfolioId,
|
|
3249
3254
|
startDate: today,
|
|
3250
|
-
endDate: str("endDate") || plus(today, 90)
|
|
3255
|
+
endDate: str("endDate") || plus$1(today, 90)
|
|
3251
3256
|
});
|
|
3252
3257
|
}
|
|
3253
3258
|
case "milestone":
|
|
@@ -3255,7 +3260,7 @@ function commitCreate(store, kind, v, ctx, today) {
|
|
|
3255
3260
|
return s.addMilestone({
|
|
3256
3261
|
programId: ctx.programId,
|
|
3257
3262
|
name: str("name"),
|
|
3258
|
-
dueDate: str("dueDate") || plus(today, 14)
|
|
3263
|
+
dueDate: str("dueDate") || plus$1(today, 14)
|
|
3259
3264
|
});
|
|
3260
3265
|
case "raid": {
|
|
3261
3266
|
const projectId = ctx.projectId ?? (v.projectId || null);
|
|
@@ -3702,14 +3707,19 @@ function soundingDraws(task, reflections) {
|
|
|
3702
3707
|
if (s.settledDays != null) return [s.settledDays];
|
|
3703
3708
|
return answersFor(reflections, task).map((a) => a.days);
|
|
3704
3709
|
}
|
|
3705
|
-
/**
|
|
3706
|
-
|
|
3710
|
+
/**
|
|
3711
|
+
* Calendar days from `from` until `days` working days have passed: Monday
|
|
3712
|
+
* to Friday, or the days `works` says (a person's holidays and time away,
|
|
3713
|
+
* src/lib/holidays.ts). Gives up after two years.
|
|
3714
|
+
*/
|
|
3715
|
+
function workingDaysAhead(days, from, works) {
|
|
3707
3716
|
let left = Math.ceil(days);
|
|
3708
3717
|
let d = 0;
|
|
3709
|
-
while (left > 0) {
|
|
3718
|
+
while (left > 0 && d < 730) {
|
|
3710
3719
|
d += 1;
|
|
3711
|
-
const
|
|
3712
|
-
|
|
3720
|
+
const at = addDays(from, d);
|
|
3721
|
+
const wd = at.getDay();
|
|
3722
|
+
if (works ? works(at) : wd !== 0 && wd !== 6) left -= 1;
|
|
3713
3723
|
}
|
|
3714
3724
|
return d;
|
|
3715
3725
|
}
|
|
@@ -3753,11 +3763,508 @@ var init_soundings = __esmMin((() => {
|
|
|
3753
3763
|
SOUNDING_MAX_DAYS = 250;
|
|
3754
3764
|
}));
|
|
3755
3765
|
|
|
3766
|
+
//#endregion
|
|
3767
|
+
//#region src/lib/countries.ts
|
|
3768
|
+
/** A country code Harbor has holidays for, upper-cased; null otherwise. */
|
|
3769
|
+
function countryCode(text) {
|
|
3770
|
+
if (!text) return null;
|
|
3771
|
+
const t = text.trim();
|
|
3772
|
+
const up = t.toUpperCase();
|
|
3773
|
+
if (KNOWN.has(up)) return up;
|
|
3774
|
+
if (up === "UK") return "GB";
|
|
3775
|
+
if (up === "USA") return "US";
|
|
3776
|
+
return COUNTRIES.find((c) => c.name.toLowerCase() === t.toLowerCase())?.code ?? null;
|
|
3777
|
+
}
|
|
3778
|
+
var COUNTRIES, KNOWN, countryName;
|
|
3779
|
+
var init_countries = __esmMin((() => {
|
|
3780
|
+
COUNTRIES = [
|
|
3781
|
+
{
|
|
3782
|
+
code: "AU",
|
|
3783
|
+
name: "Australia"
|
|
3784
|
+
},
|
|
3785
|
+
{
|
|
3786
|
+
code: "BR",
|
|
3787
|
+
name: "Brazil"
|
|
3788
|
+
},
|
|
3789
|
+
{
|
|
3790
|
+
code: "CA",
|
|
3791
|
+
name: "Canada"
|
|
3792
|
+
},
|
|
3793
|
+
{
|
|
3794
|
+
code: "FR",
|
|
3795
|
+
name: "France"
|
|
3796
|
+
},
|
|
3797
|
+
{
|
|
3798
|
+
code: "DE",
|
|
3799
|
+
name: "Germany"
|
|
3800
|
+
},
|
|
3801
|
+
{
|
|
3802
|
+
code: "IE",
|
|
3803
|
+
name: "Ireland"
|
|
3804
|
+
},
|
|
3805
|
+
{
|
|
3806
|
+
code: "IT",
|
|
3807
|
+
name: "Italy"
|
|
3808
|
+
},
|
|
3809
|
+
{
|
|
3810
|
+
code: "MX",
|
|
3811
|
+
name: "Mexico"
|
|
3812
|
+
},
|
|
3813
|
+
{
|
|
3814
|
+
code: "NL",
|
|
3815
|
+
name: "Netherlands"
|
|
3816
|
+
},
|
|
3817
|
+
{
|
|
3818
|
+
code: "NZ",
|
|
3819
|
+
name: "New Zealand"
|
|
3820
|
+
},
|
|
3821
|
+
{
|
|
3822
|
+
code: "ES",
|
|
3823
|
+
name: "Spain"
|
|
3824
|
+
},
|
|
3825
|
+
{
|
|
3826
|
+
code: "GB",
|
|
3827
|
+
name: "United Kingdom"
|
|
3828
|
+
},
|
|
3829
|
+
{
|
|
3830
|
+
code: "US",
|
|
3831
|
+
name: "United States"
|
|
3832
|
+
}
|
|
3833
|
+
];
|
|
3834
|
+
KNOWN = new Set(COUNTRIES.map((c) => c.code));
|
|
3835
|
+
countryName = (code) => COUNTRIES.find((c) => c.code === code)?.name ?? code;
|
|
3836
|
+
}));
|
|
3837
|
+
|
|
3838
|
+
//#endregion
|
|
3839
|
+
//#region src/lib/holidays.ts
|
|
3840
|
+
init_soundings();
|
|
3841
|
+
init_date_fns();
|
|
3842
|
+
init_format();
|
|
3843
|
+
init_countries();
|
|
3844
|
+
const iso$4 = (y, m, d) => todayISO(new Date(y, m - 1, d, 12));
|
|
3845
|
+
const weekday = (day) => (/* @__PURE__ */ new Date(`${day}T12:00:00`)).getDay();
|
|
3846
|
+
const plus = (day, n) => todayISO(addDays(/* @__PURE__ */ new Date(`${day}T12:00:00`), n));
|
|
3847
|
+
const isWeekend = (day) => {
|
|
3848
|
+
const wd = weekday(day);
|
|
3849
|
+
return wd === 0 || wd === 6;
|
|
3850
|
+
};
|
|
3851
|
+
/** Easter Sunday (the anonymous Gregorian algorithm). */
|
|
3852
|
+
function easter(year) {
|
|
3853
|
+
const a = year % 19;
|
|
3854
|
+
const b = Math.floor(year / 100);
|
|
3855
|
+
const c = year % 100;
|
|
3856
|
+
const d = Math.floor(b / 4);
|
|
3857
|
+
const e = b % 4;
|
|
3858
|
+
const f = Math.floor((b + 8) / 25);
|
|
3859
|
+
const g = Math.floor((b - f + 1) / 3);
|
|
3860
|
+
const h = (19 * a + b - d - g + 15) % 30;
|
|
3861
|
+
const i = Math.floor(c / 4);
|
|
3862
|
+
const k = c % 4;
|
|
3863
|
+
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
3864
|
+
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
3865
|
+
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
3866
|
+
const day = (h + l - 7 * m + 114) % 31 + 1;
|
|
3867
|
+
return iso$4(year, month, day);
|
|
3868
|
+
}
|
|
3869
|
+
/** The nth given weekday (0 Sunday) of a month; n = -1 for the last. */
|
|
3870
|
+
function nth(year, month, dow, n) {
|
|
3871
|
+
if (n > 0) {
|
|
3872
|
+
const first = iso$4(year, month, 1);
|
|
3873
|
+
const shift = (dow - weekday(first) + 7) % 7;
|
|
3874
|
+
return plus(first, shift + (n - 1) * 7);
|
|
3875
|
+
}
|
|
3876
|
+
const last = iso$4(year, month + 1, 0);
|
|
3877
|
+
return plus(last, -((weekday(last) - dow + 7) % 7));
|
|
3878
|
+
}
|
|
3879
|
+
/** The given weekday on or before a day. */
|
|
3880
|
+
const onOrBefore = (day, dow) => plus(day, -((weekday(day) - dow + 7) % 7));
|
|
3881
|
+
const fixed = (m, d) => (y) => iso$4(y, m, d);
|
|
3882
|
+
const fromEaster = (n) => (y) => plus(easter(y), n);
|
|
3883
|
+
/** New Zealand's Matariki, set by law for each year. */
|
|
3884
|
+
const MATARIKI = {
|
|
3885
|
+
2025: "2025-06-20",
|
|
3886
|
+
2026: "2026-07-10",
|
|
3887
|
+
2027: "2027-06-25",
|
|
3888
|
+
2028: "2028-07-14",
|
|
3889
|
+
2029: "2029-07-06",
|
|
3890
|
+
2030: "2030-06-21",
|
|
3891
|
+
2031: "2031-07-11",
|
|
3892
|
+
2032: "2032-07-02",
|
|
3893
|
+
2033: "2033-06-24",
|
|
3894
|
+
2034: "2034-07-07",
|
|
3895
|
+
2035: "2035-06-29"
|
|
3896
|
+
};
|
|
3897
|
+
const RULES = {
|
|
3898
|
+
US: [
|
|
3899
|
+
[
|
|
3900
|
+
"New Year's Day",
|
|
3901
|
+
fixed(1, 1),
|
|
3902
|
+
"us"
|
|
3903
|
+
],
|
|
3904
|
+
["Martin Luther King Jr. Day", (y) => nth(y, 1, 1, 3)],
|
|
3905
|
+
["Presidents' Day", (y) => nth(y, 2, 1, 3)],
|
|
3906
|
+
["Memorial Day", (y) => nth(y, 5, 1, -1)],
|
|
3907
|
+
[
|
|
3908
|
+
"Juneteenth",
|
|
3909
|
+
fixed(6, 19),
|
|
3910
|
+
"us"
|
|
3911
|
+
],
|
|
3912
|
+
[
|
|
3913
|
+
"Independence Day",
|
|
3914
|
+
fixed(7, 4),
|
|
3915
|
+
"us"
|
|
3916
|
+
],
|
|
3917
|
+
["Labor Day", (y) => nth(y, 9, 1, 1)],
|
|
3918
|
+
["Thanksgiving", (y) => nth(y, 11, 4, 4)],
|
|
3919
|
+
[
|
|
3920
|
+
"Christmas Day",
|
|
3921
|
+
fixed(12, 25),
|
|
3922
|
+
"us"
|
|
3923
|
+
]
|
|
3924
|
+
],
|
|
3925
|
+
CA: [
|
|
3926
|
+
[
|
|
3927
|
+
"New Year's Day",
|
|
3928
|
+
fixed(1, 1),
|
|
3929
|
+
"next"
|
|
3930
|
+
],
|
|
3931
|
+
["Good Friday", fromEaster(-2)],
|
|
3932
|
+
["Victoria Day", (y) => onOrBefore(iso$4(y, 5, 24), 1)],
|
|
3933
|
+
[
|
|
3934
|
+
"Canada Day",
|
|
3935
|
+
fixed(7, 1),
|
|
3936
|
+
"next"
|
|
3937
|
+
],
|
|
3938
|
+
["Labour Day", (y) => nth(y, 9, 1, 1)],
|
|
3939
|
+
["Thanksgiving", (y) => nth(y, 10, 1, 2)],
|
|
3940
|
+
[
|
|
3941
|
+
"Christmas Day",
|
|
3942
|
+
fixed(12, 25),
|
|
3943
|
+
"next"
|
|
3944
|
+
],
|
|
3945
|
+
[
|
|
3946
|
+
"Boxing Day",
|
|
3947
|
+
fixed(12, 26),
|
|
3948
|
+
"next"
|
|
3949
|
+
]
|
|
3950
|
+
],
|
|
3951
|
+
GB: [
|
|
3952
|
+
[
|
|
3953
|
+
"New Year's Day",
|
|
3954
|
+
fixed(1, 1),
|
|
3955
|
+
"next"
|
|
3956
|
+
],
|
|
3957
|
+
["Good Friday", fromEaster(-2)],
|
|
3958
|
+
["Easter Monday", fromEaster(1)],
|
|
3959
|
+
["Early May bank holiday", (y) => nth(y, 5, 1, 1)],
|
|
3960
|
+
["Spring bank holiday", (y) => nth(y, 5, 1, -1)],
|
|
3961
|
+
["Summer bank holiday", (y) => nth(y, 8, 1, -1)],
|
|
3962
|
+
[
|
|
3963
|
+
"Christmas Day",
|
|
3964
|
+
fixed(12, 25),
|
|
3965
|
+
"next"
|
|
3966
|
+
],
|
|
3967
|
+
[
|
|
3968
|
+
"Boxing Day",
|
|
3969
|
+
fixed(12, 26),
|
|
3970
|
+
"next"
|
|
3971
|
+
]
|
|
3972
|
+
],
|
|
3973
|
+
IE: [
|
|
3974
|
+
[
|
|
3975
|
+
"New Year's Day",
|
|
3976
|
+
fixed(1, 1),
|
|
3977
|
+
"next"
|
|
3978
|
+
],
|
|
3979
|
+
["St Brigid's Day", (y) => weekday(iso$4(y, 2, 1)) === 5 ? iso$4(y, 2, 1) : nth(y, 2, 1, 1)],
|
|
3980
|
+
[
|
|
3981
|
+
"St Patrick's Day",
|
|
3982
|
+
fixed(3, 17),
|
|
3983
|
+
"next"
|
|
3984
|
+
],
|
|
3985
|
+
["Easter Monday", fromEaster(1)],
|
|
3986
|
+
["May bank holiday", (y) => nth(y, 5, 1, 1)],
|
|
3987
|
+
["June bank holiday", (y) => nth(y, 6, 1, 1)],
|
|
3988
|
+
["August bank holiday", (y) => nth(y, 8, 1, 1)],
|
|
3989
|
+
["October bank holiday", (y) => nth(y, 10, 1, -1)],
|
|
3990
|
+
[
|
|
3991
|
+
"Christmas Day",
|
|
3992
|
+
fixed(12, 25),
|
|
3993
|
+
"next"
|
|
3994
|
+
],
|
|
3995
|
+
[
|
|
3996
|
+
"St Stephen's Day",
|
|
3997
|
+
fixed(12, 26),
|
|
3998
|
+
"next"
|
|
3999
|
+
]
|
|
4000
|
+
],
|
|
4001
|
+
AU: [
|
|
4002
|
+
[
|
|
4003
|
+
"New Year's Day",
|
|
4004
|
+
fixed(1, 1),
|
|
4005
|
+
"next"
|
|
4006
|
+
],
|
|
4007
|
+
[
|
|
4008
|
+
"Australia Day",
|
|
4009
|
+
fixed(1, 26),
|
|
4010
|
+
"next"
|
|
4011
|
+
],
|
|
4012
|
+
["Good Friday", fromEaster(-2)],
|
|
4013
|
+
["Easter Monday", fromEaster(1)],
|
|
4014
|
+
["Anzac Day", fixed(4, 25)],
|
|
4015
|
+
["King's Birthday", (y) => nth(y, 6, 1, 2)],
|
|
4016
|
+
[
|
|
4017
|
+
"Christmas Day",
|
|
4018
|
+
fixed(12, 25),
|
|
4019
|
+
"next"
|
|
4020
|
+
],
|
|
4021
|
+
[
|
|
4022
|
+
"Boxing Day",
|
|
4023
|
+
fixed(12, 26),
|
|
4024
|
+
"next"
|
|
4025
|
+
]
|
|
4026
|
+
],
|
|
4027
|
+
NZ: [
|
|
4028
|
+
[
|
|
4029
|
+
"New Year's Day",
|
|
4030
|
+
fixed(1, 1),
|
|
4031
|
+
"next"
|
|
4032
|
+
],
|
|
4033
|
+
[
|
|
4034
|
+
"Day after New Year's Day",
|
|
4035
|
+
fixed(1, 2),
|
|
4036
|
+
"next"
|
|
4037
|
+
],
|
|
4038
|
+
[
|
|
4039
|
+
"Waitangi Day",
|
|
4040
|
+
fixed(2, 6),
|
|
4041
|
+
"next"
|
|
4042
|
+
],
|
|
4043
|
+
["Good Friday", fromEaster(-2)],
|
|
4044
|
+
["Easter Monday", fromEaster(1)],
|
|
4045
|
+
[
|
|
4046
|
+
"Anzac Day",
|
|
4047
|
+
fixed(4, 25),
|
|
4048
|
+
"next"
|
|
4049
|
+
],
|
|
4050
|
+
["King's Birthday", (y) => nth(y, 6, 1, 1)],
|
|
4051
|
+
["Matariki", (y) => MATARIKI[y] ?? ""],
|
|
4052
|
+
["Labour Day", (y) => nth(y, 10, 1, 4)],
|
|
4053
|
+
[
|
|
4054
|
+
"Christmas Day",
|
|
4055
|
+
fixed(12, 25),
|
|
4056
|
+
"next"
|
|
4057
|
+
],
|
|
4058
|
+
[
|
|
4059
|
+
"Boxing Day",
|
|
4060
|
+
fixed(12, 26),
|
|
4061
|
+
"next"
|
|
4062
|
+
]
|
|
4063
|
+
],
|
|
4064
|
+
DE: [
|
|
4065
|
+
["Neujahr", fixed(1, 1)],
|
|
4066
|
+
["Karfreitag", fromEaster(-2)],
|
|
4067
|
+
["Ostermontag", fromEaster(1)],
|
|
4068
|
+
["Tag der Arbeit", fixed(5, 1)],
|
|
4069
|
+
["Christi Himmelfahrt", fromEaster(39)],
|
|
4070
|
+
["Pfingstmontag", fromEaster(50)],
|
|
4071
|
+
["Tag der Deutschen Einheit", fixed(10, 3)],
|
|
4072
|
+
["1. Weihnachtstag", fixed(12, 25)],
|
|
4073
|
+
["2. Weihnachtstag", fixed(12, 26)]
|
|
4074
|
+
],
|
|
4075
|
+
FR: [
|
|
4076
|
+
["Jour de l'an", fixed(1, 1)],
|
|
4077
|
+
["Lundi de Pâques", fromEaster(1)],
|
|
4078
|
+
["Fête du Travail", fixed(5, 1)],
|
|
4079
|
+
["Victoire 1945", fixed(5, 8)],
|
|
4080
|
+
["Ascension", fromEaster(39)],
|
|
4081
|
+
["Lundi de Pentecôte", fromEaster(50)],
|
|
4082
|
+
["Fête nationale", fixed(7, 14)],
|
|
4083
|
+
["Assomption", fixed(8, 15)],
|
|
4084
|
+
["Toussaint", fixed(11, 1)],
|
|
4085
|
+
["Armistice", fixed(11, 11)],
|
|
4086
|
+
["Noël", fixed(12, 25)]
|
|
4087
|
+
],
|
|
4088
|
+
NL: [
|
|
4089
|
+
["Nieuwjaarsdag", fixed(1, 1)],
|
|
4090
|
+
["Tweede paasdag", fromEaster(1)],
|
|
4091
|
+
["Koningsdag", (y) => weekday(iso$4(y, 4, 27)) === 0 ? iso$4(y, 4, 26) : iso$4(y, 4, 27)],
|
|
4092
|
+
["Hemelvaartsdag", fromEaster(39)],
|
|
4093
|
+
["Tweede pinksterdag", fromEaster(50)],
|
|
4094
|
+
["Eerste kerstdag", fixed(12, 25)],
|
|
4095
|
+
["Tweede kerstdag", fixed(12, 26)]
|
|
4096
|
+
],
|
|
4097
|
+
ES: [
|
|
4098
|
+
["Año Nuevo", fixed(1, 1)],
|
|
4099
|
+
["Epifanía", fixed(1, 6)],
|
|
4100
|
+
["Viernes Santo", fromEaster(-2)],
|
|
4101
|
+
["Fiesta del Trabajo", fixed(5, 1)],
|
|
4102
|
+
["Asunción", fixed(8, 15)],
|
|
4103
|
+
["Fiesta Nacional", fixed(10, 12)],
|
|
4104
|
+
["Todos los Santos", fixed(11, 1)],
|
|
4105
|
+
["Día de la Constitución", fixed(12, 6)],
|
|
4106
|
+
["Inmaculada Concepción", fixed(12, 8)],
|
|
4107
|
+
["Navidad", fixed(12, 25)]
|
|
4108
|
+
],
|
|
4109
|
+
IT: [
|
|
4110
|
+
["Capodanno", fixed(1, 1)],
|
|
4111
|
+
["Epifania", fixed(1, 6)],
|
|
4112
|
+
["Lunedì dell'Angelo", fromEaster(1)],
|
|
4113
|
+
["Festa della Liberazione", fixed(4, 25)],
|
|
4114
|
+
["Festa del Lavoro", fixed(5, 1)],
|
|
4115
|
+
["Festa della Repubblica", fixed(6, 2)],
|
|
4116
|
+
["Ferragosto", fixed(8, 15)],
|
|
4117
|
+
["Ognissanti", fixed(11, 1)],
|
|
4118
|
+
["Immacolata Concezione", fixed(12, 8)],
|
|
4119
|
+
["Natale", fixed(12, 25)],
|
|
4120
|
+
["Santo Stefano", fixed(12, 26)]
|
|
4121
|
+
],
|
|
4122
|
+
MX: [
|
|
4123
|
+
["Año Nuevo", fixed(1, 1)],
|
|
4124
|
+
["Día de la Constitución", (y) => nth(y, 2, 1, 1)],
|
|
4125
|
+
["Natalicio de Benito Juárez", (y) => nth(y, 3, 1, 3)],
|
|
4126
|
+
["Día del Trabajo", fixed(5, 1)],
|
|
4127
|
+
["Día de la Independencia", fixed(9, 16)],
|
|
4128
|
+
["Día de la Revolución", (y) => nth(y, 11, 1, 3)],
|
|
4129
|
+
["Navidad", fixed(12, 25)]
|
|
4130
|
+
],
|
|
4131
|
+
BR: [
|
|
4132
|
+
["Confraternização Universal", fixed(1, 1)],
|
|
4133
|
+
["Carnaval", fromEaster(-48)],
|
|
4134
|
+
["Carnaval", fromEaster(-47)],
|
|
4135
|
+
["Sexta-feira Santa", fromEaster(-2)],
|
|
4136
|
+
["Tiradentes", fixed(4, 21)],
|
|
4137
|
+
["Dia do Trabalho", fixed(5, 1)],
|
|
4138
|
+
["Corpus Christi", fromEaster(60)],
|
|
4139
|
+
["Independência", fixed(9, 7)],
|
|
4140
|
+
["Nossa Senhora Aparecida", fixed(10, 12)],
|
|
4141
|
+
["Finados", fixed(11, 2)],
|
|
4142
|
+
["Proclamação da República", fixed(11, 15)],
|
|
4143
|
+
["Consciência Negra", fixed(11, 20)],
|
|
4144
|
+
["Natal", fixed(12, 25)]
|
|
4145
|
+
]
|
|
4146
|
+
};
|
|
4147
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4148
|
+
/** A country's public holidays in a year, by the day they're taken, oldest first. */
|
|
4149
|
+
function holidaysIn(country, year) {
|
|
4150
|
+
const key = `${country}|${year}`;
|
|
4151
|
+
const had = cache.get(key);
|
|
4152
|
+
if (had) return had;
|
|
4153
|
+
const days = (RULES[country] ?? []).map(([name, day, move = "none"]) => ({
|
|
4154
|
+
name,
|
|
4155
|
+
date: day(year),
|
|
4156
|
+
move
|
|
4157
|
+
})).filter((h) => h.date);
|
|
4158
|
+
const taken = new Set(days.filter((h) => !isWeekend(h.date)).map((h) => h.date));
|
|
4159
|
+
const out = [];
|
|
4160
|
+
for (const h of days) {
|
|
4161
|
+
let date = h.date;
|
|
4162
|
+
const wd = weekday(date);
|
|
4163
|
+
if (h.move === "us" && (wd === 0 || wd === 6)) date = plus(date, wd === 6 ? -1 : 1);
|
|
4164
|
+
else if (h.move === "next" && (wd === 0 || wd === 6)) {
|
|
4165
|
+
date = plus(date, 1);
|
|
4166
|
+
while (taken.has(date) || isWeekend(date)) date = plus(date, 1);
|
|
4167
|
+
taken.add(date);
|
|
4168
|
+
}
|
|
4169
|
+
out.push({
|
|
4170
|
+
date,
|
|
4171
|
+
name: h.name,
|
|
4172
|
+
country
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
out.sort((a, b) => a.date.localeCompare(b.date));
|
|
4176
|
+
cache.set(key, out);
|
|
4177
|
+
return out;
|
|
4178
|
+
}
|
|
4179
|
+
/** Holidays in the given countries between two days, both included. */
|
|
4180
|
+
function holidaysBetween(countries, from, to) {
|
|
4181
|
+
const out = [];
|
|
4182
|
+
const y0 = Number(from.slice(0, 4));
|
|
4183
|
+
const y1 = Number(to.slice(0, 4));
|
|
4184
|
+
for (const c of countries) for (let y = y0; y <= y1; y++) for (const h of holidaysIn(c, y)) if (h.date >= from && h.date <= to) out.push(h);
|
|
4185
|
+
return out.sort((a, b) => a.date.localeCompare(b.date) || a.country.localeCompare(b.country));
|
|
4186
|
+
}
|
|
4187
|
+
/** Days someone is away: from the handover day up to the day before they're back. */
|
|
4188
|
+
function awayDays(person) {
|
|
4189
|
+
const a = person.away;
|
|
4190
|
+
if (!a?.until || !a.since) return null;
|
|
4191
|
+
return [a.since.slice(0, 10), plus(a.until, -1)];
|
|
4192
|
+
}
|
|
4193
|
+
/** The harbor's calendar of who works when; see the top of this file. */
|
|
4194
|
+
function workCalendar(data) {
|
|
4195
|
+
const harbor = (data.workCalendar?.countries ?? []).filter((c) => countryCode(c) === c);
|
|
4196
|
+
const people = new Map(data.people.map((p) => [p.id, p]));
|
|
4197
|
+
const own = (id) => {
|
|
4198
|
+
const c = id ? countryCode(people.get(id)?.country) : null;
|
|
4199
|
+
return c ? [c] : harbor;
|
|
4200
|
+
};
|
|
4201
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
4202
|
+
const holidayOn = (countries, day) => {
|
|
4203
|
+
const year = Number(day.slice(0, 4));
|
|
4204
|
+
for (const c of countries) {
|
|
4205
|
+
const k = `${c}|${year}`;
|
|
4206
|
+
let days = byDay.get(k);
|
|
4207
|
+
if (!days) {
|
|
4208
|
+
days = new Map(holidaysIn(c, year).map((h) => [h.date, h]));
|
|
4209
|
+
byDay.set(k, days);
|
|
4210
|
+
}
|
|
4211
|
+
const h = days.get(day);
|
|
4212
|
+
if (h) return h;
|
|
4213
|
+
}
|
|
4214
|
+
return null;
|
|
4215
|
+
};
|
|
4216
|
+
const offOn = (personId, day) => {
|
|
4217
|
+
if (isWeekend(day)) return null;
|
|
4218
|
+
const person = personId ? people.get(personId) : void 0;
|
|
4219
|
+
const away = person ? awayDays(person) : null;
|
|
4220
|
+
if (away && day >= away[0] && day <= away[1]) return { kind: "away" };
|
|
4221
|
+
const h = holidayOn(own(personId), day);
|
|
4222
|
+
return h ? {
|
|
4223
|
+
kind: "holiday",
|
|
4224
|
+
name: h.name,
|
|
4225
|
+
country: h.country
|
|
4226
|
+
} : null;
|
|
4227
|
+
};
|
|
4228
|
+
return {
|
|
4229
|
+
countries: harbor,
|
|
4230
|
+
countriesOf: own,
|
|
4231
|
+
offOn,
|
|
4232
|
+
works: (personId, day) => !isWeekend(day) && offOn(personId, day) === null,
|
|
4233
|
+
active: harbor.length > 0 || data.people.some((p) => countryCode(p.country) || awayDays(p) !== null)
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
/** A calendar where nobody has holidays or time away: Monday to Friday. */
|
|
4237
|
+
const PLAIN_CALENDAR = workCalendar({ people: [] });
|
|
4238
|
+
/**
|
|
4239
|
+
* The public holidays anyone in the harbor takes between two days, one
|
|
4240
|
+
* entry a day: "Christmas Day (GB, US)". For views that aren't one person's,
|
|
4241
|
+
* like the timeline.
|
|
4242
|
+
*/
|
|
4243
|
+
function harborHolidays(data, from, to) {
|
|
4244
|
+
const countries = new Set(workCalendar(data).countries);
|
|
4245
|
+
for (const p of data.people) {
|
|
4246
|
+
const c = countryCode(p.country);
|
|
4247
|
+
if (c) countries.add(c);
|
|
4248
|
+
}
|
|
4249
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
4250
|
+
for (const h of holidaysBetween([...countries], from, to)) {
|
|
4251
|
+
if (isWeekend(h.date)) continue;
|
|
4252
|
+
byDay.set(h.date, [...byDay.get(h.date) ?? [], h]);
|
|
4253
|
+
}
|
|
4254
|
+
return [...byDay].map(([date, hs]) => {
|
|
4255
|
+
const names = [...new Set(hs.map((h) => h.name))];
|
|
4256
|
+
const codes = hs.map((h) => h.country).join(", ");
|
|
4257
|
+
return {
|
|
4258
|
+
date,
|
|
4259
|
+
names: `${names.join(" / ")} (${codes})`
|
|
4260
|
+
};
|
|
4261
|
+
});
|
|
4262
|
+
}
|
|
4263
|
+
|
|
3756
4264
|
//#endregion
|
|
3757
4265
|
//#region src/lib/tide.ts
|
|
3758
4266
|
init_date_fns();
|
|
3759
4267
|
init_format();
|
|
3760
|
-
init_soundings();
|
|
3761
4268
|
/**
|
|
3762
4269
|
* The tide forecast: when a project will likely finish, from how fast its
|
|
3763
4270
|
* crew has actually closed tasks. Each of a thousand runs replays the coming
|
|
@@ -3810,7 +4317,7 @@ function random(seed) {
|
|
|
3810
4317
|
};
|
|
3811
4318
|
}
|
|
3812
4319
|
/** Days from today to finishing, one per run; Infinity for a run that never does. */
|
|
3813
|
-
function simulate(remaining, weekly, seed, pace = 1) {
|
|
4320
|
+
function simulate(remaining, weekly, seed, pace = 1, shares = null) {
|
|
3814
4321
|
const next = random(seed);
|
|
3815
4322
|
const out = [];
|
|
3816
4323
|
for (let run = 0; run < TRIALS; run++) {
|
|
@@ -3818,7 +4325,12 @@ function simulate(remaining, weekly, seed, pace = 1) {
|
|
|
3818
4325
|
let days = Infinity;
|
|
3819
4326
|
for (let week = 0; week < MAX_WEEKS; week++) {
|
|
3820
4327
|
const drawn = weekly[Math.floor(next() * weekly.length)];
|
|
3821
|
-
|
|
4328
|
+
let closed = pace === 1 ? drawn : Math.floor(drawn * pace) + (next() < drawn * pace % 1 ? 1 : 0);
|
|
4329
|
+
const share = shares ? shares(week) : 1;
|
|
4330
|
+
if (share < 1 && closed > 0) {
|
|
4331
|
+
const scaled = closed * share;
|
|
4332
|
+
closed = Math.floor(scaled) + (next() < scaled - Math.floor(scaled) ? 1 : 0);
|
|
4333
|
+
}
|
|
3822
4334
|
if (closed >= left) {
|
|
3823
4335
|
const landed = Array.from({ length: closed }, () => 1 + Math.floor(next() * 7));
|
|
3824
4336
|
landed.sort((a, b) => a - b);
|
|
@@ -3836,7 +4348,7 @@ function simulate(remaining, weekly, seed, pace = 1) {
|
|
|
3836
4348
|
* answers at random, so a wide spread widens the forecast. A task already
|
|
3837
4349
|
* in play counts half its days; it's partly done.
|
|
3838
4350
|
*/
|
|
3839
|
-
function soundingFloors(tasks, reflections, base, seed) {
|
|
4351
|
+
function soundingFloors(tasks, reflections, base, seed, calendar) {
|
|
3840
4352
|
const draws = [];
|
|
3841
4353
|
for (const t of tasks) {
|
|
3842
4354
|
if (t.status === "done") continue;
|
|
@@ -3844,7 +4356,8 @@ function soundingFloors(tasks, reflections, base, seed) {
|
|
|
3844
4356
|
if (days.length === 0) continue;
|
|
3845
4357
|
draws.push({
|
|
3846
4358
|
days,
|
|
3847
|
-
share: t.status === "doing" || t.status === "review" ? .5 : 1
|
|
4359
|
+
share: t.status === "doing" || t.status === "review" ? .5 : 1,
|
|
4360
|
+
who: t.assigneeId
|
|
3848
4361
|
});
|
|
3849
4362
|
}
|
|
3850
4363
|
if (draws.length === 0) return {
|
|
@@ -3853,9 +4366,13 @@ function soundingFloors(tasks, reflections, base, seed) {
|
|
|
3853
4366
|
};
|
|
3854
4367
|
const next = random(seed);
|
|
3855
4368
|
const memo = /* @__PURE__ */ new Map();
|
|
3856
|
-
const ahead = (d) => {
|
|
3857
|
-
|
|
3858
|
-
|
|
4369
|
+
const ahead = (d, who) => {
|
|
4370
|
+
const k = `${who}|${d}`;
|
|
4371
|
+
let v = memo.get(k);
|
|
4372
|
+
if (v === void 0) {
|
|
4373
|
+
const works = calendar?.active ? (at) => calendar.works(who, todayISO(at)) : void 0;
|
|
4374
|
+
memo.set(k, v = workingDaysAhead(d, base, works));
|
|
4375
|
+
}
|
|
3859
4376
|
return v;
|
|
3860
4377
|
};
|
|
3861
4378
|
const floors = [];
|
|
@@ -3863,7 +4380,7 @@ function soundingFloors(tasks, reflections, base, seed) {
|
|
|
3863
4380
|
let most = 0;
|
|
3864
4381
|
for (const x of draws) {
|
|
3865
4382
|
const d = x.days[Math.floor(next() * x.days.length)] * x.share;
|
|
3866
|
-
most = Math.max(most, ahead(d));
|
|
4383
|
+
most = Math.max(most, ahead(d, x.who));
|
|
3867
4384
|
}
|
|
3868
4385
|
floors.push(most);
|
|
3869
4386
|
}
|
|
@@ -3873,6 +4390,79 @@ function soundingFloors(tasks, reflections, base, seed) {
|
|
|
3873
4390
|
};
|
|
3874
4391
|
}
|
|
3875
4392
|
/**
|
|
4393
|
+
* Each coming week's share of the crew's usual working days, from day 1
|
|
4394
|
+
* (tomorrow): who owns the open tasks, less their holidays and time away.
|
|
4395
|
+
* Null when the calendar has nothing in it, so the forecast reads as before.
|
|
4396
|
+
*/
|
|
4397
|
+
function weekShares(tasks, base, calendar) {
|
|
4398
|
+
const crew = [...new Set(tasks.filter((t) => t.status !== "done").map((t) => t.assigneeId))].filter((id) => id !== null);
|
|
4399
|
+
const people = crew.length ? crew : [null];
|
|
4400
|
+
if (!calendar?.active) return {
|
|
4401
|
+
shares: null,
|
|
4402
|
+
crew: people
|
|
4403
|
+
};
|
|
4404
|
+
const known = [];
|
|
4405
|
+
const share = (week) => {
|
|
4406
|
+
const had = known[week];
|
|
4407
|
+
if (had !== void 0) return had;
|
|
4408
|
+
let usual = 0;
|
|
4409
|
+
let working = 0;
|
|
4410
|
+
for (let d = week * 7 + 1; d <= week * 7 + 7; d++) {
|
|
4411
|
+
const day = todayISO(addDays(base, d));
|
|
4412
|
+
const wd = addDays(base, d).getDay();
|
|
4413
|
+
if (wd === 0 || wd === 6) continue;
|
|
4414
|
+
for (const p of people) {
|
|
4415
|
+
usual++;
|
|
4416
|
+
if (calendar.works(p, day)) working++;
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
return known[week] = usual ? working / usual : 1;
|
|
4420
|
+
};
|
|
4421
|
+
return {
|
|
4422
|
+
shares: share,
|
|
4423
|
+
crew: people
|
|
4424
|
+
};
|
|
4425
|
+
}
|
|
4426
|
+
/** The holidays and away days the crew has between tomorrow and `until`. */
|
|
4427
|
+
function ashoreUntil(calendar, crew, base, until) {
|
|
4428
|
+
const holidays = [];
|
|
4429
|
+
let awayDays = 0;
|
|
4430
|
+
if (!calendar?.active || !Number.isFinite(until)) return {
|
|
4431
|
+
holidays,
|
|
4432
|
+
awayDays
|
|
4433
|
+
};
|
|
4434
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4435
|
+
for (let d = 1; d <= until; d++) {
|
|
4436
|
+
const day = todayISO(addDays(base, d));
|
|
4437
|
+
for (const p of crew) {
|
|
4438
|
+
const off = calendar.offOn(p, day);
|
|
4439
|
+
if (off?.kind === "away") awayDays++;
|
|
4440
|
+
else if (off?.kind === "holiday" && !seen.has(`${day}|${off.country}`)) {
|
|
4441
|
+
seen.add(`${day}|${off.country}`);
|
|
4442
|
+
holidays.push({
|
|
4443
|
+
date: day,
|
|
4444
|
+
name: off.name,
|
|
4445
|
+
country: off.country
|
|
4446
|
+
});
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
return {
|
|
4451
|
+
holidays,
|
|
4452
|
+
awayDays
|
|
4453
|
+
};
|
|
4454
|
+
}
|
|
4455
|
+
/** What the forecast reads beyond the tasks: soundings and the work calendar. */
|
|
4456
|
+
function tideOptions(data) {
|
|
4457
|
+
return {
|
|
4458
|
+
reflections: data.reflections,
|
|
4459
|
+
calendar: workCalendar({
|
|
4460
|
+
people: data.people ?? [],
|
|
4461
|
+
workCalendar: data.workCalendar
|
|
4462
|
+
})
|
|
4463
|
+
};
|
|
4464
|
+
}
|
|
4465
|
+
/**
|
|
3876
4466
|
* `pace` scales the crew's weekly counts (1 is as they are), for dry dock's
|
|
3877
4467
|
* "what if Sam joined" (src/lib/dry-dock.ts).
|
|
3878
4468
|
*/
|
|
@@ -3891,7 +4481,8 @@ function tideForecast(project, tasks, now, opts = {}) {
|
|
|
3891
4481
|
};
|
|
3892
4482
|
const today = todayISO(now);
|
|
3893
4483
|
const base = noon$2(today);
|
|
3894
|
-
const
|
|
4484
|
+
const { shares, crew } = weekShares(tasks, base, opts.calendar);
|
|
4485
|
+
const paced = simulate(remaining, weekly, hash(`${project.id}|${remaining}|${today}`), Math.max(0, opts.pace ?? 1), shares);
|
|
3895
4486
|
const at = (share, list = runs) => list[Math.min(list.length - 1, Math.ceil(share * list.length) - 1)];
|
|
3896
4487
|
let lastDue = null;
|
|
3897
4488
|
for (const t of tasks) {
|
|
@@ -3899,7 +4490,7 @@ function tideForecast(project, tasks, now, opts = {}) {
|
|
|
3899
4490
|
if (!lastDue || t.dueDate > lastDue.dueDate) lastDue = t;
|
|
3900
4491
|
}
|
|
3901
4492
|
const floor = lastDue ? differenceInCalendarDays(noon$2(lastDue.dueDate), base) : 0;
|
|
3902
|
-
const sounded = soundingFloors(tasks, opts.reflections, base, hash(`${project.id}|sound|${today}`));
|
|
4493
|
+
const sounded = soundingFloors(tasks, opts.reflections, base, hash(`${project.id}|sound|${today}`), opts.calendar);
|
|
3903
4494
|
const runs = paced.map((d, i) => Math.max(d, floor, sounded.floors?.[i] ?? 0)).sort((a, b) => a - b);
|
|
3904
4495
|
const heldBy = lastDue && floor > at(.5, paced) ? lastDue : null;
|
|
3905
4496
|
const dayOf = (days) => Number.isFinite(days) ? todayISO(addDays(base, days)) : null;
|
|
@@ -3929,7 +4520,8 @@ function tideForecast(project, tasks, now, opts = {}) {
|
|
|
3929
4520
|
curve,
|
|
3930
4521
|
rough: finished < ROUGH_BELOW,
|
|
3931
4522
|
heldBy,
|
|
3932
|
-
sounded: sounded.sounded
|
|
4523
|
+
sounded: sounded.sounded,
|
|
4524
|
+
ashore: ashoreUntil(opts.calendar, crew, base, at(.85))
|
|
3933
4525
|
};
|
|
3934
4526
|
}
|
|
3935
4527
|
/** "15%", with "under 5%" and "over 95%" at the ends, where a thousand runs can't be that sure. */
|
|
@@ -4684,9 +5276,10 @@ function slipHeadline(cost) {
|
|
|
4684
5276
|
*/
|
|
4685
5277
|
function programSlipCost(data, programId, now) {
|
|
4686
5278
|
let sum = 0;
|
|
5279
|
+
const opts = tideOptions(data);
|
|
4687
5280
|
for (const p of data.projects) {
|
|
4688
5281
|
if (p.programId !== programId || p.budget <= 0 || p.health === "done") continue;
|
|
4689
|
-
const tide = tideForecast(p, tasksOf(data.tasks, p.id), now);
|
|
5282
|
+
const tide = tideForecast(p, tasksOf(data.tasks, p.id), now, opts);
|
|
4690
5283
|
sum += slipCost(p, tide, spendEntriesOf(data, p.id), now)?.likely?.cost ?? 0;
|
|
4691
5284
|
}
|
|
4692
5285
|
return sum;
|
|
@@ -4961,7 +5554,7 @@ function filterTasks(tasks, f) {
|
|
|
4961
5554
|
*/
|
|
4962
5555
|
function tideLine(data, project, today) {
|
|
4963
5556
|
const tasks = tasksOf(data.tasks, project.id);
|
|
4964
|
-
const tide = tideForecast(project, tasks, parseIso(`${today}T12:00:00`),
|
|
5557
|
+
const tide = tideForecast(project, tasks, parseIso(`${today}T12:00:00`), tideOptions(data));
|
|
4965
5558
|
if (tide.kind === "no-pace") return {
|
|
4966
5559
|
text: "No forecast yet: nothing finished in the last few weeks",
|
|
4967
5560
|
late: false
|
|
@@ -5368,7 +5961,7 @@ function keepText(keep, now) {
|
|
|
5368
5961
|
*/
|
|
5369
5962
|
function slipLine(data, project, today) {
|
|
5370
5963
|
const now = parseIso(`${today}T12:00:00`);
|
|
5371
|
-
const tide = tideForecast(project, tasksOf(data.tasks, project.id), now);
|
|
5964
|
+
const tide = tideForecast(project, tasksOf(data.tasks, project.id), now, tideOptions(data));
|
|
5372
5965
|
const cost = slipCost(project, tide, spendEntriesOf(data, project.id), now);
|
|
5373
5966
|
if (!cost?.likely || cost.likely.daysLate <= 0) return null;
|
|
5374
5967
|
return slipHeadline(cost);
|
|
@@ -6486,7 +7079,7 @@ function composeBriefBody(opts) {
|
|
|
6486
7079
|
}
|
|
6487
7080
|
return lines.join("\n").trim();
|
|
6488
7081
|
}
|
|
6489
|
-
function firstName$
|
|
7082
|
+
function firstName$2(person) {
|
|
6490
7083
|
return person?.name.split(/\s+/)[0] ?? "Someone";
|
|
6491
7084
|
}
|
|
6492
7085
|
function similar(a, b) {
|
|
@@ -6524,7 +7117,7 @@ function headlineOf(drums, flow) {
|
|
|
6524
7117
|
}
|
|
6525
7118
|
if (idle.length === 1) {
|
|
6526
7119
|
const d = idle[0];
|
|
6527
|
-
return d.owner ? `${firstName$
|
|
7120
|
+
return d.owner ? `${firstName$2(d.owner)} is in play elsewhere. ${d.constraint.title} is starving.` : `${d.constraint.title} is idle. Protect it or retire it.`;
|
|
6528
7121
|
}
|
|
6529
7122
|
if (idle.length > 1) return `${idle.length} constraints are idle. The work is not at the constraint.`;
|
|
6530
7123
|
if (system && flow.impliedDays != null && flow.impliedDays > 21) return `At this load, ${formatWait(flow.impliedDays)}. ${system.constraint.title}.`;
|
|
@@ -6588,7 +7181,7 @@ function briefOf(data, opts = {}) {
|
|
|
6588
7181
|
for (const d of drums.filter((x) => x.idle)) push({
|
|
6589
7182
|
id: `idle-${d.constraint.id}`,
|
|
6590
7183
|
title: `${d.constraint.title} is idle`,
|
|
6591
|
-
detail: d.owner ? `${firstName$
|
|
7184
|
+
detail: d.owner ? `${firstName$2(d.owner)} is in play elsewhere. ${d.program.name} is waiting.` : `${d.program.name} has a named constraint and nobody protecting it.`,
|
|
6592
7185
|
href: `/programs/${d.program.id}`
|
|
6593
7186
|
});
|
|
6594
7187
|
for (const d of drums.filter((x) => x.constraint.kind === "collision" && !x.idle)) push({
|
|
@@ -8579,7 +9172,7 @@ const PING_KIND_LABEL = {
|
|
|
8579
9172
|
idle: "Quiet blockers",
|
|
8580
9173
|
mention: "Mentions"
|
|
8581
9174
|
};
|
|
8582
|
-
function firstName(name) {
|
|
9175
|
+
function firstName$1(name) {
|
|
8583
9176
|
return name.split(/\s+/)[0] ?? name;
|
|
8584
9177
|
}
|
|
8585
9178
|
/** FNV-1a over the text, as eight hex digits: a short, stable name for a set of ids. */
|
|
@@ -8618,7 +9211,7 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
8618
9211
|
pings.push({
|
|
8619
9212
|
id: `assigned:${task.id}:${task.assignedAt}`,
|
|
8620
9213
|
kind: "assigned",
|
|
8621
|
-
title: by ? `${firstName(by.name)} gave you a task` : "You have a new task",
|
|
9214
|
+
title: by ? `${firstName$1(by.name)} gave you a task` : "You have a new task",
|
|
8622
9215
|
detail: `“${task.title}”${project ? ` · ${project.name}` : ""}`,
|
|
8623
9216
|
at: task.assignedAt,
|
|
8624
9217
|
taskId: task.id,
|
|
@@ -8632,7 +9225,7 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
8632
9225
|
const project = projectOf(data, blocker);
|
|
8633
9226
|
const names = [...new Set(waiting.map((t) => {
|
|
8634
9227
|
const who = data.people.find((p) => p.id === t.assigneeId);
|
|
8635
|
-
return who ? firstName(who.name) : "Someone";
|
|
9228
|
+
return who ? firstName$1(who.name) : "Someone";
|
|
8636
9229
|
}))];
|
|
8637
9230
|
const blocked = waiting.length === 1 ? `“${waiting[0].title}”` : `${waiting.length} tasks`;
|
|
8638
9231
|
const upstream = openBlockerNames(blocker, data.tasks);
|
|
@@ -8667,7 +9260,7 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
8667
9260
|
pings.push({
|
|
8668
9261
|
id: `changes:${m.id}:${shortHash(note)}`,
|
|
8669
9262
|
kind: "approval",
|
|
8670
|
-
title: `${approver ? firstName(approver.name) : "The approver"} asked for changes to ${m.name}`,
|
|
9263
|
+
title: `${approver ? firstName$1(approver.name) : "The approver"} asked for changes to ${m.name}`,
|
|
8671
9264
|
detail: note ? `${where}${excerpt(note)}` : program?.name ?? "",
|
|
8672
9265
|
at: m.dueDate,
|
|
8673
9266
|
programId: m.programId,
|
|
@@ -8713,7 +9306,7 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
8713
9306
|
pings.push({
|
|
8714
9307
|
id: `sounding:${task.id}:${s.askedAt}`,
|
|
8715
9308
|
kind: "sounding",
|
|
8716
|
-
title: `${by ? firstName(by.name) : "Someone"} asked how long a task will take`,
|
|
9309
|
+
title: `${by ? firstName$1(by.name) : "Someone"} asked how long a task will take`,
|
|
8717
9310
|
detail: `“${task.title}”${project ? ` · ${project.name}` : ""} · answer alone; the guesses show together`,
|
|
8718
9311
|
at: s.askedAt,
|
|
8719
9312
|
taskId: task.id,
|
|
@@ -8730,7 +9323,7 @@ function inboxOf(data, personId, now = clockNow()) {
|
|
|
8730
9323
|
pings.push({
|
|
8731
9324
|
id: `mention:${comment.id}`,
|
|
8732
9325
|
kind: "mention",
|
|
8733
|
-
title: author ? `${firstName(author.name)} mentioned you` : "You were mentioned",
|
|
9326
|
+
title: author ? `${firstName$1(author.name)} mentioned you` : "You were mentioned",
|
|
8734
9327
|
detail: snippet ? `on “${task.title}”${project ? ` · ${project.name}` : ""} — ${snippet}` : `on “${task.title}”${project ? ` · ${project.name}` : ""}`,
|
|
8735
9328
|
at: comment.createdAt,
|
|
8736
9329
|
taskId: task.id,
|
|
@@ -8838,14 +9431,28 @@ function InboxView({ active, width, height, go }) {
|
|
|
8838
9431
|
//#endregion
|
|
8839
9432
|
//#region src/lib/load.ts
|
|
8840
9433
|
/** Open tasks for one person, bucketed by the week they are due, plus what is already late or has no date. */
|
|
8841
|
-
function weeklyLoad(tasks, personId, now, weeks = 6) {
|
|
9434
|
+
function weeklyLoad(tasks, personId, now, weeks = 6, calendar) {
|
|
8842
9435
|
const mine = tasks.filter((t) => t.assigneeId === personId && t.status !== "done");
|
|
8843
9436
|
const today = iso$2(now);
|
|
8844
9437
|
const origin = startOfWeek(now, { weekStartsOn: 1 });
|
|
8845
|
-
const buckets = Array.from({ length: weeks }, (_, i) =>
|
|
8846
|
-
start
|
|
8847
|
-
|
|
8848
|
-
|
|
9438
|
+
const buckets = Array.from({ length: weeks }, (_, i) => {
|
|
9439
|
+
const start = iso$2(addDays(origin, i * 7));
|
|
9440
|
+
const off = [];
|
|
9441
|
+
if (calendar?.active) for (let d = 0; d < 5; d++) {
|
|
9442
|
+
const date = iso$2(addDays(origin, i * 7 + d));
|
|
9443
|
+
const why = calendar.offOn(personId, date);
|
|
9444
|
+
if (why) off.push({
|
|
9445
|
+
date,
|
|
9446
|
+
why
|
|
9447
|
+
});
|
|
9448
|
+
}
|
|
9449
|
+
return {
|
|
9450
|
+
start,
|
|
9451
|
+
due: 0,
|
|
9452
|
+
workDays: 5 - off.length,
|
|
9453
|
+
off
|
|
9454
|
+
};
|
|
9455
|
+
});
|
|
8849
9456
|
let overdue = 0;
|
|
8850
9457
|
let undated = 0;
|
|
8851
9458
|
for (const t of mine) {
|
|
@@ -18220,8 +18827,8 @@ const STATUSES = [
|
|
|
18220
18827
|
* for an agent: dates for half, 85% and 95% of the runs, or why there's none.
|
|
18221
18828
|
* Given the spend ledger, also what each week late costs (src/lib/slip-cost.ts).
|
|
18222
18829
|
*/
|
|
18223
|
-
function finishForecast(project, tasks, now, spend,
|
|
18224
|
-
const tide = tideForecast(project, tasks, now,
|
|
18830
|
+
function finishForecast(project, tasks, now, spend, data) {
|
|
18831
|
+
const tide = tideForecast(project, tasks, now, data ? tideOptions(data) : {});
|
|
18225
18832
|
if (tide.kind !== "forecast") return { none: tideQuiet(tide) };
|
|
18226
18833
|
const lastDue = dueAfterFinish(tide);
|
|
18227
18834
|
const dueLater = lastDue ? { lastDue: {
|
|
@@ -18248,6 +18855,14 @@ function finishForecast(project, tasks, now, spend, reflections) {
|
|
|
18248
18855
|
basedOn: `${tide.finished} task${tide.finished === 1 ? "" : "s"} finished in the last ${tide.weeks} week${tide.weeks === 1 ? "" : "s"}`,
|
|
18249
18856
|
...tide.rough ? { rough: true } : {},
|
|
18250
18857
|
...tide.sounded ? { soundedTasks: tide.sounded } : {},
|
|
18858
|
+
...tide.ashore.holidays.length || tide.ashore.awayDays ? { timeAshore: {
|
|
18859
|
+
holidays: tide.ashore.holidays.map((h) => ({
|
|
18860
|
+
date: h.date,
|
|
18861
|
+
name: h.name,
|
|
18862
|
+
country: h.country
|
|
18863
|
+
})),
|
|
18864
|
+
awayDays: tide.ashore.awayDays
|
|
18865
|
+
} } : {},
|
|
18251
18866
|
...tide.weeks < 2 ? { shortHistory: true } : {},
|
|
18252
18867
|
...dueLater,
|
|
18253
18868
|
...slip
|
|
@@ -18306,6 +18921,7 @@ function me(ctx) {
|
|
|
18306
18921
|
return ctx.data.people.find((p) => p.accountId === ctx.accountId) ?? null;
|
|
18307
18922
|
}
|
|
18308
18923
|
const nameOf$2 = (people, id) => id && people.get(id)?.name || null;
|
|
18924
|
+
const LIFEBOATS_ALONE = "Nobody else on the crew yet; harbor invite EMAIL adds someone.";
|
|
18309
18925
|
/** One line of "only one person knows these" (src/lib/lifeboats.ts), for an agent. */
|
|
18310
18926
|
function lifeboatOut(data, line, people) {
|
|
18311
18927
|
return {
|
|
@@ -18678,7 +19294,7 @@ const HARBOR_TOOLS = [
|
|
|
18678
19294
|
done: c.done,
|
|
18679
19295
|
late: c.late
|
|
18680
19296
|
},
|
|
18681
|
-
finish: finishForecast(p, tasksOf(data.tasks, p.id), ctx.now, spendBy.get(p.id) ?? [], data
|
|
19297
|
+
finish: finishForecast(p, tasksOf(data.tasks, p.id), ctx.now, spendBy.get(p.id) ?? [], data),
|
|
18682
19298
|
...limitsOf(data, p.id),
|
|
18683
19299
|
...hillOf(p, today),
|
|
18684
19300
|
...publicRoadmap(ctx, p.id)
|
|
@@ -18749,7 +19365,7 @@ const HARBOR_TOOLS = [
|
|
|
18749
19365
|
waitsOn,
|
|
18750
19366
|
milestones,
|
|
18751
19367
|
openRaid: raid,
|
|
18752
|
-
finish: finishForecast(p, tasks, ctx.now, spendEntriesOf(data, p.id), data
|
|
19368
|
+
finish: finishForecast(p, tasks, ctx.now, spendEntriesOf(data, p.id), data),
|
|
18753
19369
|
...charterOf(p, tasks),
|
|
18754
19370
|
...notesOf(ctx, p),
|
|
18755
19371
|
...hillOf(p, today),
|
|
@@ -19252,6 +19868,40 @@ const HARBOR_TOOLS = [
|
|
|
19252
19868
|
};
|
|
19253
19869
|
}
|
|
19254
19870
|
}),
|
|
19871
|
+
tool({
|
|
19872
|
+
name: "time_ashore",
|
|
19873
|
+
title: "Time ashore",
|
|
19874
|
+
description: "Whose public holidays the harbor takes (country codes), anyone who takes another country's, and the days off coming up: public holidays, and who is away and when they're back. The forecast and the weekly load count these as days nobody works." + DATA_NOTE,
|
|
19875
|
+
input: object({
|
|
19876
|
+
harbor: harborArg,
|
|
19877
|
+
days: number().int().min(1).max(366).default(56).describe("How far ahead to look, in days.")
|
|
19878
|
+
}),
|
|
19879
|
+
run: (ctx, input) => {
|
|
19880
|
+
const { data } = ctx;
|
|
19881
|
+
const today = todayISO(ctx.now);
|
|
19882
|
+
const until = plus(today, input.days);
|
|
19883
|
+
return {
|
|
19884
|
+
countries: workCalendar(data).countries.map((c) => ({
|
|
19885
|
+
code: c,
|
|
19886
|
+
name: countryName(c)
|
|
19887
|
+
})),
|
|
19888
|
+
people: data.people.filter((p) => countryCode(p.country)).map((p) => ({
|
|
19889
|
+
name: p.name,
|
|
19890
|
+
country: countryCode(p.country)
|
|
19891
|
+
})),
|
|
19892
|
+
holidays: harborHolidays(data, today, until).map((h) => ({
|
|
19893
|
+
date: h.date,
|
|
19894
|
+
name: h.names
|
|
19895
|
+
})),
|
|
19896
|
+
away: data.people.filter((p) => p.away?.since && p.away.until > today && p.away.since.slice(0, 10) <= until).map((p) => ({
|
|
19897
|
+
name: p.name,
|
|
19898
|
+
from: p.away.since.slice(0, 10),
|
|
19899
|
+
back: p.away.until
|
|
19900
|
+
})),
|
|
19901
|
+
known: COUNTRIES.map((c) => c.code)
|
|
19902
|
+
};
|
|
19903
|
+
}
|
|
19904
|
+
}),
|
|
19255
19905
|
tool({
|
|
19256
19906
|
name: "long_pole",
|
|
19257
19907
|
title: "What's holding this up",
|
|
@@ -19464,8 +20114,10 @@ const HARBOR_TOOLS = [
|
|
|
19464
20114
|
const program = input.program ? findOne(data.programs, input.program, "program") : null;
|
|
19465
20115
|
const lines = lifeboats(data, ctx.now, { programIds: program ? /* @__PURE__ */ new Set([program.id]) : null });
|
|
19466
20116
|
const people = byId(data.people);
|
|
20117
|
+
const alone = data.people.filter((p) => !isAgent(p)).length <= 1;
|
|
19467
20118
|
return {
|
|
19468
20119
|
...program ? { program: program.name } : {},
|
|
20120
|
+
...alone && lines.length > 0 ? { hint: LIFEBOATS_ALONE } : {},
|
|
19469
20121
|
count: lines.length,
|
|
19470
20122
|
lines: lines.slice(input.offset, input.offset + input.limit).map((l) => lifeboatOut(data, l, people)),
|
|
19471
20123
|
away: awayNote(data, lines, todayISO(ctx.now)),
|
|
@@ -19558,7 +20210,8 @@ function pickData(s) {
|
|
|
19558
20210
|
checkIns: s.checkIns ?? NONE,
|
|
19559
20211
|
spendEntries: s.spendEntries ?? NONE,
|
|
19560
20212
|
reflections: s.reflections ?? NONE,
|
|
19561
|
-
priorityRules: s.priorityRules ?? null
|
|
20213
|
+
priorityRules: s.priorityRules ?? null,
|
|
20214
|
+
workCalendar: s.workCalendar ?? null
|
|
19562
20215
|
};
|
|
19563
20216
|
}
|
|
19564
20217
|
/** The document as sent to the server: normalized and without inline file data. */
|
|
@@ -21837,6 +22490,19 @@ function peopleSpendSlice({ set, get, write }) {
|
|
|
21837
22490
|
...patch
|
|
21838
22491
|
} : p) });
|
|
21839
22492
|
},
|
|
22493
|
+
setHolidayCountries: (codes) => {
|
|
22494
|
+
const countries = [...new Set(codes.map((c) => countryCode(c)).filter((c) => c !== null))].slice(0, 12);
|
|
22495
|
+
write({ workCalendar: countries.length ? { countries } : null });
|
|
22496
|
+
},
|
|
22497
|
+
setPersonCountry: (id, code) => {
|
|
22498
|
+
const country = code ? countryCode(code) : null;
|
|
22499
|
+
if (code && !country) return false;
|
|
22500
|
+
write({ people: get().people.map((p) => p.id === id ? {
|
|
22501
|
+
...p,
|
|
22502
|
+
country
|
|
22503
|
+
} : p) });
|
|
22504
|
+
return true;
|
|
22505
|
+
},
|
|
21840
22506
|
addPerson: (p) => {
|
|
21841
22507
|
const id = uid("p");
|
|
21842
22508
|
const row = {
|
|
@@ -22020,6 +22686,7 @@ var init_people_spend = __esmMin((() => {
|
|
|
22020
22686
|
init_assignment();
|
|
22021
22687
|
init_spend();
|
|
22022
22688
|
init_utils();
|
|
22689
|
+
init_countries();
|
|
22023
22690
|
init_shared();
|
|
22024
22691
|
init_allocation();
|
|
22025
22692
|
}));
|
|
@@ -22145,7 +22812,7 @@ function pickCollections(doc, keys) {
|
|
|
22145
22812
|
function emptyRows() {
|
|
22146
22813
|
return Object.fromEntries(ROW_COLLECTIONS.map((k) => [k, []]));
|
|
22147
22814
|
}
|
|
22148
|
-
/** A collection as rows; `spendEntries`, `reflections` and `
|
|
22815
|
+
/** A collection as rows; `spendEntries`, `reflections`, `priorityRules` and `workCalendar` may be missing from older documents. */
|
|
22149
22816
|
function rowsOf(doc, key) {
|
|
22150
22817
|
if (isKeyed(key)) return KEYED[key].rows(doc[key]);
|
|
22151
22818
|
return doc[key] ?? [];
|
|
@@ -22229,7 +22896,8 @@ var init_changes = __esmMin((() => {
|
|
|
22229
22896
|
"checkIns",
|
|
22230
22897
|
"spendEntries",
|
|
22231
22898
|
"reflections",
|
|
22232
|
-
"priorityRules"
|
|
22899
|
+
"priorityRules",
|
|
22900
|
+
"workCalendar"
|
|
22233
22901
|
];
|
|
22234
22902
|
ROW_COLLECTIONS = [
|
|
22235
22903
|
"tasks",
|
|
@@ -22249,7 +22917,8 @@ var init_changes = __esmMin((() => {
|
|
|
22249
22917
|
"boardConfigs",
|
|
22250
22918
|
"ropes",
|
|
22251
22919
|
"readPingIds",
|
|
22252
|
-
"priorityRules"
|
|
22920
|
+
"priorityRules",
|
|
22921
|
+
"workCalendar"
|
|
22253
22922
|
];
|
|
22254
22923
|
KEYED = {
|
|
22255
22924
|
boardConfigs: {
|
|
@@ -22280,6 +22949,17 @@ var init_changes = __esmMin((() => {
|
|
|
22280
22949
|
const { id: _id, ...rules } = rows[0];
|
|
22281
22950
|
return rules;
|
|
22282
22951
|
}
|
|
22952
|
+
},
|
|
22953
|
+
workCalendar: {
|
|
22954
|
+
rows: (value) => value ? [{
|
|
22955
|
+
...value,
|
|
22956
|
+
id: "calendar"
|
|
22957
|
+
}] : [],
|
|
22958
|
+
value: (rows) => {
|
|
22959
|
+
if (!rows[0]) return null;
|
|
22960
|
+
const { id: _id, ...calendar } = rows[0];
|
|
22961
|
+
return calendar;
|
|
22962
|
+
}
|
|
22283
22963
|
}
|
|
22284
22964
|
};
|
|
22285
22965
|
isKeyed = (key) => key in KEYED;
|
|
@@ -28376,7 +29056,7 @@ async function pickHarbor(deps) {
|
|
|
28376
29056
|
//#endregion
|
|
28377
29057
|
//#region src/tui/fog-sweep.ts
|
|
28378
29058
|
/**
|
|
28379
|
-
* `harbor fog` in a terminal: the tasks nobody has touched for three weeks
|
|
29059
|
+
* `harbor fog --sweep` in a terminal: the tasks nobody has touched for three weeks
|
|
28380
29060
|
* (lib/fog.ts), one at a time, on the store's own actions. k keeps it
|
|
28381
29061
|
* (clears the fog), d asks for a new date, x drops it, s skips, q stops.
|
|
28382
29062
|
* The CLI saves through the sync loop afterwards.
|
|
@@ -28463,6 +29143,192 @@ function fogSummary(t) {
|
|
|
28463
29143
|
return parts.length ? `${parts.join(", ")}.` : "Nothing changed.";
|
|
28464
29144
|
}
|
|
28465
29145
|
|
|
29146
|
+
//#endregion
|
|
29147
|
+
//#region src/tui/watch.ts
|
|
29148
|
+
init_indexes();
|
|
29149
|
+
init_swatches();
|
|
29150
|
+
/** Most lines one change prints; a whole reload says how many more there were. */
|
|
29151
|
+
const WATCH_MAX = 12;
|
|
29152
|
+
const QUOTE_MAX = 90;
|
|
29153
|
+
function firstWords(body) {
|
|
29154
|
+
const one = body.replace(/\s+/g, " ").trim();
|
|
29155
|
+
return one.length > QUOTE_MAX ? `${one.slice(0, 89)}…` : one;
|
|
29156
|
+
}
|
|
29157
|
+
function firstName(p) {
|
|
29158
|
+
return p?.name.split(" ")[0] || "Someone";
|
|
29159
|
+
}
|
|
29160
|
+
function watchLines(prev, next, meId) {
|
|
29161
|
+
const out = [];
|
|
29162
|
+
const people = byId(next.people);
|
|
29163
|
+
const before = byId(prev.tasks);
|
|
29164
|
+
const label = (t) => `${keyOf$1(next, t)} ${t.title}`;
|
|
29165
|
+
const seen = new Set(prev.activity.map((a) => a.id));
|
|
29166
|
+
const fresh = next.activity.filter((a) => !seen.has(a.id));
|
|
29167
|
+
const who = (t) => {
|
|
29168
|
+
const hit = fresh.find((a) => a.text.includes(`“${t.title}”`));
|
|
29169
|
+
if (!hit) return null;
|
|
29170
|
+
return hit.personId === meId ? "You" : firstName(people.get(hit.personId));
|
|
29171
|
+
};
|
|
29172
|
+
const said = (t, phrase) => {
|
|
29173
|
+
const name = who(t);
|
|
29174
|
+
return name ? `${name} ${phrase}` : phrase.charAt(0).toUpperCase() + phrase.slice(1);
|
|
29175
|
+
};
|
|
29176
|
+
for (const t of next.tasks) {
|
|
29177
|
+
const was = before.get(t.id);
|
|
29178
|
+
const mine = t.assigneeId === meId;
|
|
29179
|
+
if (!was) {
|
|
29180
|
+
const name = who(t);
|
|
29181
|
+
out.push({
|
|
29182
|
+
text: mine && name !== "You" ? `${name ?? "Someone"} added ${label(t)} for you` : `${name ?? "Someone"} added ${label(t)}`,
|
|
29183
|
+
mine,
|
|
29184
|
+
tone: "plain"
|
|
29185
|
+
});
|
|
29186
|
+
continue;
|
|
29187
|
+
}
|
|
29188
|
+
if (t.assigneeId !== was.assigneeId && mine) {
|
|
29189
|
+
const from = t.assignedBy && t.assignedBy !== meId ? firstName(people.get(t.assignedBy)) : null;
|
|
29190
|
+
out.push({
|
|
29191
|
+
text: `You were handed ${label(t)}${from ? ` by ${from}` : ""}`,
|
|
29192
|
+
mine: true,
|
|
29193
|
+
tone: "good"
|
|
29194
|
+
});
|
|
29195
|
+
} else if (t.assigneeId !== was.assigneeId && was.assigneeId === meId) {
|
|
29196
|
+
const to = t.assigneeId ? firstName(people.get(t.assigneeId)) : "nobody";
|
|
29197
|
+
out.push({
|
|
29198
|
+
text: `Your task ${label(t)} went to ${to}`,
|
|
29199
|
+
mine: true,
|
|
29200
|
+
tone: "warn"
|
|
29201
|
+
});
|
|
29202
|
+
}
|
|
29203
|
+
if (t.status !== was.status) {
|
|
29204
|
+
const done = t.status === "done";
|
|
29205
|
+
out.push({
|
|
29206
|
+
text: done ? said(t, `finished ${label(t)}`) : said(t, `moved ${label(t)} to ${TASK_STATUS_LABEL[t.status]}`),
|
|
29207
|
+
mine,
|
|
29208
|
+
tone: done ? "good" : "plain"
|
|
29209
|
+
});
|
|
29210
|
+
} else if (t.dueDate !== was.dueDate && mine) out.push({
|
|
29211
|
+
text: t.dueDate ? `${label(t)} is due ${t.dueDate} now` : `${label(t)} has no due date now`,
|
|
29212
|
+
mine: true,
|
|
29213
|
+
tone: "plain"
|
|
29214
|
+
});
|
|
29215
|
+
}
|
|
29216
|
+
for (const t of prev.tasks) {
|
|
29217
|
+
if (next.tasks.some((x) => x.id === t.id)) continue;
|
|
29218
|
+
out.push({
|
|
29219
|
+
text: `${label(t)} was removed`,
|
|
29220
|
+
mine: t.assigneeId === meId,
|
|
29221
|
+
tone: "warn"
|
|
29222
|
+
});
|
|
29223
|
+
}
|
|
29224
|
+
const oldComments = new Set(prev.comments.map((c) => c.id));
|
|
29225
|
+
const tasks = byId(next.tasks);
|
|
29226
|
+
for (const c of next.comments) {
|
|
29227
|
+
if (oldComments.has(c.id)) continue;
|
|
29228
|
+
const t = tasks.get(c.taskId);
|
|
29229
|
+
if (!t) continue;
|
|
29230
|
+
const yours = t.assigneeId === meId;
|
|
29231
|
+
const mentioned = c.mentionedIds.includes(meId);
|
|
29232
|
+
const name = c.authorId === meId ? "You" : firstName(people.get(c.authorId));
|
|
29233
|
+
out.push({
|
|
29234
|
+
text: mentioned ? `${name} mentioned you on ${label(t)}` : `${name} commented on ${yours && name !== "You" ? "your task " : ""}${label(t)}`,
|
|
29235
|
+
quote: firstWords(c.body),
|
|
29236
|
+
mine: (yours || mentioned) && c.authorId !== meId,
|
|
29237
|
+
tone: mentioned ? "warn" : "plain"
|
|
29238
|
+
});
|
|
29239
|
+
}
|
|
29240
|
+
const health = (kind) => {
|
|
29241
|
+
const was = byId(prev[kind]);
|
|
29242
|
+
for (const item of next[kind]) {
|
|
29243
|
+
const old = was.get(item.id);
|
|
29244
|
+
if (!old || old.health === item.health) continue;
|
|
29245
|
+
out.push({
|
|
29246
|
+
text: `${item.name}: ${HEALTH_LABEL$1[old.health]} → ${HEALTH_LABEL$1[item.health]}`,
|
|
29247
|
+
mine: item.ownerId === meId,
|
|
29248
|
+
tone: item.health === "off-track" ? "bad" : item.health === "at-risk" ? "warn" : item.health === "on-track" || item.health === "done" ? "good" : "plain"
|
|
29249
|
+
});
|
|
29250
|
+
}
|
|
29251
|
+
};
|
|
29252
|
+
health("programs");
|
|
29253
|
+
health("projects");
|
|
29254
|
+
if (out.length > 12) {
|
|
29255
|
+
const more = out.length - 11;
|
|
29256
|
+
return [...out.slice(0, 11), {
|
|
29257
|
+
text: `and ${more} more changes`,
|
|
29258
|
+
mine: false,
|
|
29259
|
+
tone: "plain"
|
|
29260
|
+
}];
|
|
29261
|
+
}
|
|
29262
|
+
return out;
|
|
29263
|
+
}
|
|
29264
|
+
|
|
29265
|
+
//#endregion
|
|
29266
|
+
//#region src/tui/watch-run.ts
|
|
29267
|
+
init_clock();
|
|
29268
|
+
init_plain();
|
|
29269
|
+
init_repo_sync();
|
|
29270
|
+
init_store();
|
|
29271
|
+
init_sync();
|
|
29272
|
+
const ANSI = {
|
|
29273
|
+
dim: "\x1B[2m",
|
|
29274
|
+
bold: "\x1B[1m",
|
|
29275
|
+
reset: "\x1B[0m",
|
|
29276
|
+
good: "\x1B[36m",
|
|
29277
|
+
warn: "\x1B[33m",
|
|
29278
|
+
bad: "\x1B[31m"
|
|
29279
|
+
};
|
|
29280
|
+
/** One ticker line for the terminal: the time dim, the rest bright when it's yours. */
|
|
29281
|
+
function watchText(line, at, opts) {
|
|
29282
|
+
const time = wallClock(at).slice(11, 16);
|
|
29283
|
+
const room = (opts.width ?? 0) - 6;
|
|
29284
|
+
const fit = (s, n) => opts.width && s.length > n ? `${s.slice(0, Math.max(1, n - 1))}…` : s;
|
|
29285
|
+
const text = fit(line.text, room);
|
|
29286
|
+
const quote = line.quote ? fit(`“${line.quote}”`, room - 2) : null;
|
|
29287
|
+
if (!opts.colour) return [`${time} ${text}`, ...quote ? [` ${quote}`] : []].join("\n");
|
|
29288
|
+
const tone = line.tone === "plain" ? "" : ANSI[line.tone];
|
|
29289
|
+
const body = line.mine ? `${ANSI.bold}${tone}${text}${ANSI.reset}` : `${tone || ANSI.dim}${text}${ANSI.reset}`;
|
|
29290
|
+
return [`${ANSI.dim}${time}${ANSI.reset} ${body}`, ...quote ? [`${ANSI.dim} ${quote}${ANSI.reset}`] : []].join("\n");
|
|
29291
|
+
}
|
|
29292
|
+
async function watchOn(opts) {
|
|
29293
|
+
const out = (text) => opts.write(`${text}\n`);
|
|
29294
|
+
const store = createAppStore();
|
|
29295
|
+
const sync = createSync({
|
|
29296
|
+
store,
|
|
29297
|
+
transport: opts.transport,
|
|
29298
|
+
account: {
|
|
29299
|
+
id: opts.account.id,
|
|
29300
|
+
name: opts.account.name || opts.account.email
|
|
29301
|
+
},
|
|
29302
|
+
onNotice: (n) => out(plain(n))
|
|
29303
|
+
});
|
|
29304
|
+
await sync.open(opts.harbor.id);
|
|
29305
|
+
try {
|
|
29306
|
+
const project = opts.project ? projectNamed(store.getState().projects, opts.project) : null;
|
|
29307
|
+
const dim = (s) => opts.colour ? `${ANSI.dim}${s}${ANSI.reset}` : s;
|
|
29308
|
+
out(dim(`⚓ ${plain(opts.harbor.name)}${project ? ` · ${plain(project.name)}` : ""} · on watch${opts.bell ? " · bell on" : ""} · q to stop`));
|
|
29309
|
+
const narrow = (d) => project ? {
|
|
29310
|
+
...d,
|
|
29311
|
+
tasks: d.tasks.filter((t) => t.projectId === project.id),
|
|
29312
|
+
projects: d.projects.filter((p) => p.id === project.id),
|
|
29313
|
+
programs: []
|
|
29314
|
+
} : d;
|
|
29315
|
+
const unsubscribe = store.subscribe((s, prev) => {
|
|
29316
|
+
if (s.tasks === prev.tasks && s.comments === prev.comments && s.projects === prev.projects && s.programs === prev.programs) return;
|
|
29317
|
+
const lines = watchLines(narrow(prev), narrow(s), s.currentUserId);
|
|
29318
|
+
const at = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
29319
|
+
for (const line of lines) out(watchText(line, at, {
|
|
29320
|
+
colour: opts.colour,
|
|
29321
|
+
width: opts.width?.()
|
|
29322
|
+
}));
|
|
29323
|
+
if (opts.bell && lines.some((l) => l.mine)) opts.write("\x07");
|
|
29324
|
+
});
|
|
29325
|
+
await opts.stopped;
|
|
29326
|
+
unsubscribe();
|
|
29327
|
+
} finally {
|
|
29328
|
+
sync.close();
|
|
29329
|
+
}
|
|
29330
|
+
}
|
|
29331
|
+
|
|
28466
29332
|
//#endregion
|
|
28467
29333
|
//#region src/repo/folder.ts
|
|
28468
29334
|
/**
|
|
@@ -28851,7 +29717,7 @@ function harborFor(harbors, named, saved) {
|
|
|
28851
29717
|
if (harbors.length === 1) return harbors[0];
|
|
28852
29718
|
throw new HarborError(`You're in ${harbors.length} harbors (${harbors.map((h) => h.name).join(", ")}). Add --harbor NAME, or run harbor use NAME first.`);
|
|
28853
29719
|
}
|
|
28854
|
-
/** `harbor fog
|
|
29720
|
+
/** `harbor fog --sweep`: the sweep, one task at a time, then one save. */
|
|
28855
29721
|
async function fogHere(opts) {
|
|
28856
29722
|
const transport = plainTransport(httpTransport(opts.source, opts.fetcher));
|
|
28857
29723
|
const answer = await transport.harbors();
|
|
@@ -28887,7 +29753,45 @@ async function fogHere(opts) {
|
|
|
28887
29753
|
await sync.flush();
|
|
28888
29754
|
sync.close();
|
|
28889
29755
|
}
|
|
28890
|
-
if (sync.state !== "saved") throw new HarborError("Harbor didn't take those answers. Run harbor fog again.");
|
|
29756
|
+
if (sync.state !== "saved") throw new HarborError("Harbor didn't take those answers. Run harbor fog --sweep again.");
|
|
29757
|
+
}
|
|
29758
|
+
/**
|
|
29759
|
+
* `harbor watch`: the harbor's changes as they land, a line each, until q
|
|
29760
|
+
* or Ctrl+C. Nothing is changed from here; it only listens.
|
|
29761
|
+
*/
|
|
29762
|
+
async function watchHere(opts) {
|
|
29763
|
+
const transport = plainTransport(httpTransport(opts.source, opts.fetcher));
|
|
29764
|
+
const answer = await transport.harbors();
|
|
29765
|
+
const harbor = harborFor(answer.harbors, opts.harbor, opts.saved);
|
|
29766
|
+
const stopped = new Promise((resolve) => {
|
|
29767
|
+
const input = process.stdin;
|
|
29768
|
+
const stop = () => {
|
|
29769
|
+
input.off("data", onKey);
|
|
29770
|
+
process.off("SIGINT", stop);
|
|
29771
|
+
if (input.isTTY) input.setRawMode(false);
|
|
29772
|
+
input.pause();
|
|
29773
|
+
resolve();
|
|
29774
|
+
};
|
|
29775
|
+
const onKey = (chunk) => {
|
|
29776
|
+
const key = chunk.toString();
|
|
29777
|
+
if (key === "q" || key === "Q" || key === "") stop();
|
|
29778
|
+
};
|
|
29779
|
+
if (input.isTTY) input.setRawMode(true);
|
|
29780
|
+
input.on("data", onKey);
|
|
29781
|
+
input.resume();
|
|
29782
|
+
process.on("SIGINT", stop);
|
|
29783
|
+
});
|
|
29784
|
+
await watchOn({
|
|
29785
|
+
transport,
|
|
29786
|
+
account: answer.account,
|
|
29787
|
+
harbor,
|
|
29788
|
+
project: opts.project,
|
|
29789
|
+
bell: opts.bell,
|
|
29790
|
+
colour: opts.colour,
|
|
29791
|
+
width: () => process.stdout.columns,
|
|
29792
|
+
write: (text) => process.stdout.write(text),
|
|
29793
|
+
stopped
|
|
29794
|
+
});
|
|
28891
29795
|
}
|
|
28892
29796
|
/** Questions asked on the plain terminal, before the app takes it over. Ctrl+C stops. */
|
|
28893
29797
|
function terminalAsk(stoppedText = "Stopped. Nothing was made.") {
|
|
@@ -28930,4 +29834,4 @@ function planTransport(source, fetcher) {
|
|
|
28930
29834
|
}
|
|
28931
29835
|
|
|
28932
29836
|
//#endregion
|
|
28933
|
-
export { SPLASH_MS, TransportError, firstHarbor, fogHere, harborNamed, httpTransport, initHere, initLocalHere, localPlanAt, planTransport, runApp, runLocalApp, startApp, syncHere, tryHere };
|
|
29837
|
+
export { SPLASH_MS, TransportError, firstHarbor, fogHere, harborNamed, httpTransport, initHere, initLocalHere, localPlanAt, planTransport, runApp, runLocalApp, startApp, syncHere, tryHere, watchHere };
|