@aglyn/aglyn 1.0.0-beta.266 → 1.0.0-beta.268
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/package.json +12 -12
- package/src/lib/app-utils/health-report.d.ts +78 -6
- package/src/lib/app-utils/health-report.js +113 -6
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/notifications.js +6 -10
- package/src/lib/app-utils/notifications.js.map +1 -1
- package/src/lib/app-utils/upload-inspection.d.ts +9 -0
- package/src/lib/app-utils/upload-inspection.js +27 -0
- package/src/lib/app-utils/upload-inspection.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-erase.d.ts +81 -0
- package/src/lib/plugin-manager/plugin-site-erase.js +56 -0
- package/src/lib/plugin-manager/plugin-site-erase.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/aglyn",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.268",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"./package.json": "./package.json"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
41
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
42
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
43
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
44
|
-
"@aglyn/shared-util-first-touch": "1.0.0-beta.
|
|
45
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
46
|
-
"@aglyn/shared-util-logger": "1.0.0-beta.
|
|
47
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
48
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
49
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
40
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.268",
|
|
41
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.268",
|
|
42
|
+
"@aglyn/shared-data-types": "1.0.0-beta.268",
|
|
43
|
+
"@aglyn/shared-util-email": "1.0.0-beta.268",
|
|
44
|
+
"@aglyn/shared-util-first-touch": "1.0.0-beta.268",
|
|
45
|
+
"@aglyn/shared-util-http": "1.0.0-beta.268",
|
|
46
|
+
"@aglyn/shared-util-logger": "1.0.0-beta.268",
|
|
47
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.268",
|
|
48
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.268",
|
|
49
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.268",
|
|
50
50
|
"@data-driven-forms/react-form-renderer": "^4.2.0",
|
|
51
51
|
"@msgpack/msgpack": "^3.1.3",
|
|
52
52
|
"@types/unist": "^3.0.3",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@mui/material": "^9.3.1",
|
|
63
63
|
"@mui/system": "^9.2.0",
|
|
64
|
-
"firebase": "^
|
|
64
|
+
"firebase": "^13.0.0",
|
|
65
65
|
"next": "^16.3.3",
|
|
66
66
|
"react": "^19.2.8"
|
|
67
67
|
},
|
|
@@ -531,20 +531,51 @@ export declare function signupDroughtHealth(markers: SignupServedMarker[] | null
|
|
|
531
531
|
* 10/17's). The streak of two was red only on 10/11, AGL-3724, and nowhere
|
|
532
532
|
* else. A streak of three missed AGL-3724 entirely.
|
|
533
533
|
*
|
|
534
|
+
* ## A site is not the question; reaching the console is (2026-10-18)
|
|
535
|
+
*
|
|
536
|
+
* The streak rule went red on 2026-10-18 at 15:37Z for the same reason the
|
|
537
|
+
* count did: the two newest people had no site, and both had LEFT. One left
|
|
538
|
+
* nine seconds after the workspace appeared. The other reached the site list,
|
|
539
|
+
* walked four steps of the guided start, and stopped. Sign-ups on either side
|
|
540
|
+
* of them made sites on the same build. "No site" is an outcome that leaving
|
|
541
|
+
* produces as surely as a broken landing does, so no rule over sites alone
|
|
542
|
+
* can tell the two apart.
|
|
543
|
+
*
|
|
544
|
+
* What AGL-3724 broke was the landing itself: the person never REACHED the
|
|
545
|
+
* console after the workspace was made. That is recorded now. Every org is
|
|
546
|
+
* born with `landedAtMs: null` ({@link ORG_LANDED_AT_PATH}), and the org
|
|
547
|
+
* shell's activity beat, which fires the moment the workspace opens, sets it
|
|
548
|
+
* the first time (`stampOrgLastActivity`). So an org counts as stranded only
|
|
549
|
+
* when it has no site AND its owner never reached the console. A person who
|
|
550
|
+
* arrived and then left is a drop-off, which is not this check's business.
|
|
551
|
+
*
|
|
552
|
+
* An org without the field predates the marker, and the check has no
|
|
553
|
+
* evidence about it either way, so it ABSTAINS on it (`unmarkedOrgs`). It is
|
|
554
|
+
* never counted as stranded, the way a check that cannot see its subject
|
|
555
|
+
* says so instead of guessing.
|
|
556
|
+
*
|
|
534
557
|
* Pure, like its siblings: the route reads, this decides. The body carries
|
|
535
558
|
* counts only, never an org id, slug or visitor key, because the endpoint is
|
|
536
559
|
* public.
|
|
537
560
|
*/
|
|
538
561
|
export interface SignupLandingCheck extends HealthCheck {
|
|
539
|
-
/**
|
|
562
|
+
/**
|
|
563
|
+
* Orgs in the window with no site whose owner never reached the console.
|
|
564
|
+
* Null when the read failed.
|
|
565
|
+
*/
|
|
540
566
|
strandedOrgs: number | null;
|
|
541
|
-
/** People in the window
|
|
567
|
+
/** People in the window none of whose orgs has a site or a landing. */
|
|
542
568
|
strandedPeople: number | null;
|
|
543
569
|
/**
|
|
544
|
-
* How many of the newest people in the window, in a row,
|
|
570
|
+
* How many of the newest people in the window, in a row, are stranded:
|
|
545
571
|
* what the verdict reads. Null when the read failed.
|
|
546
572
|
*/
|
|
547
573
|
strandedStreak: number | null;
|
|
574
|
+
/**
|
|
575
|
+
* Orgs with no site that predate the landing marker, which the check
|
|
576
|
+
* abstains on. Null when the read failed.
|
|
577
|
+
*/
|
|
578
|
+
unmarkedOrgs: number | null;
|
|
548
579
|
/** Every org in the window, the context for the counts above. */
|
|
549
580
|
orgsInWindow: number | null;
|
|
550
581
|
/** How far back the window reaches. */
|
|
@@ -569,12 +600,21 @@ export declare const SIGNUP_LANDING_GRACE_MINUTES = 30;
|
|
|
569
600
|
*/
|
|
570
601
|
export declare const MAX_STRANDED_SIGNUPS = 2;
|
|
571
602
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
603
|
+
* Where an org records that its owner reached the console (see
|
|
604
|
+
* {@link SignupLandingCheck}). `null` from birth, epoch millis once the
|
|
605
|
+
* workspace has been opened, absent on an org made before it existed.
|
|
606
|
+
*/
|
|
607
|
+
export declare const ORG_LANDED_AT_PATH = "landedAtMs";
|
|
608
|
+
/**
|
|
609
|
+
* One org as the landing check reads it: whether it has any site, whether
|
|
610
|
+
* its owner reached the console, when it was made, and who made it.
|
|
611
|
+
* `landed` is `undefined` for an org that predates the marker. `visitor` is
|
|
612
|
+
* an opaque key that never leaves the check; an org without one is its own
|
|
613
|
+
* person.
|
|
575
614
|
*/
|
|
576
615
|
export interface SignupLandingOrg {
|
|
577
616
|
hostCount: number;
|
|
617
|
+
landed?: boolean;
|
|
578
618
|
createdAtMs?: number | null;
|
|
579
619
|
visitor?: string | null;
|
|
580
620
|
}
|
|
@@ -1497,6 +1537,10 @@ export interface CronBeatStore {
|
|
|
1497
1537
|
collection(name: string): {
|
|
1498
1538
|
doc(id: string): {
|
|
1499
1539
|
set(data: Record<string, unknown>, options?: unknown): Promise<unknown>;
|
|
1540
|
+
/** Present on a real Firestore handle; {@link writeCronBeats} reads the summary through it. */
|
|
1541
|
+
get?(): Promise<{
|
|
1542
|
+
get(field: string): unknown;
|
|
1543
|
+
}>;
|
|
1500
1544
|
};
|
|
1501
1545
|
};
|
|
1502
1546
|
}
|
|
@@ -1514,6 +1558,34 @@ export declare const CRON_BEAT_EVERY_MINUTE_MIN_INTERVAL_MS: number;
|
|
|
1514
1558
|
/** Test seam. */
|
|
1515
1559
|
export declare function resetCronBeatThrottleForTests(): void;
|
|
1516
1560
|
export declare function writeCronBeat(store: CronBeatStore, jobId: string, now?: number): Promise<boolean>;
|
|
1561
|
+
/**
|
|
1562
|
+
* How long a job's own document may lag its summary entry when the jobs are
|
|
1563
|
+
* marked together (AGL-3932).
|
|
1564
|
+
*
|
|
1565
|
+
* The reader judges from the summary and opens a job's own document only to
|
|
1566
|
+
* confirm an entry the summary calls late, so a job marked every fifteen
|
|
1567
|
+
* minutes does not need its own document rewritten every fifteen minutes:
|
|
1568
|
+
* half an hour, under every plugin job's forty-five-minute grace, keeps the
|
|
1569
|
+
* confirming mark fresh enough to agree with a summary that has stopped.
|
|
1570
|
+
*/
|
|
1571
|
+
export declare const CRON_BEAT_OWN_MARK_REFRESH_MS: number;
|
|
1572
|
+
/**
|
|
1573
|
+
* The marks one run leaves for several jobs at once — the plugin-crons route
|
|
1574
|
+
* stamps twenty jobs a run, and {@link writeCronBeat} per job was forty
|
|
1575
|
+
* writes every fifteen minutes, half of everything the platform wrote
|
|
1576
|
+
* (AGL-3932).
|
|
1577
|
+
*
|
|
1578
|
+
* One summary write carries every job's entry (merged, like the single mark's).
|
|
1579
|
+
* A job's own document is rewritten only when the summary's current entry
|
|
1580
|
+
* for it is missing or older than {@link CRON_BEAT_OWN_MARK_REFRESH_MS},
|
|
1581
|
+
* which one read of the summary tells; a store without `get`, or a summary
|
|
1582
|
+
* that cannot be read or written, falls back to the single-mark shape for
|
|
1583
|
+
* every job, so no run can leave fewer marks than before.
|
|
1584
|
+
*
|
|
1585
|
+
* NEVER THROWS, as {@link writeCronBeat} never throws, and for the same
|
|
1586
|
+
* reason. Returns false only when nothing landed for some job.
|
|
1587
|
+
*/
|
|
1588
|
+
export declare function writeCronBeats(store: CronBeatStore, jobIds: readonly string[], now?: number): Promise<boolean>;
|
|
1517
1589
|
/**
|
|
1518
1590
|
* The beats a summary document holds, ignoring anything malformed.
|
|
1519
1591
|
*
|
|
@@ -548,6 +548,11 @@ export function signupDroughtHealth(markers, attemptMarkers, orgCreations, ms, m
|
|
|
548
548
|
* Red when the two newest people in the window are both stranded. See
|
|
549
549
|
* {@link SignupLandingCheck}.
|
|
550
550
|
*/ export const MAX_STRANDED_SIGNUPS = 2;
|
|
551
|
+
/**
|
|
552
|
+
* Where an org records that its owner reached the console (see
|
|
553
|
+
* {@link SignupLandingCheck}). `null` from birth, epoch millis once the
|
|
554
|
+
* workspace has been opened, absent on an org made before it existed.
|
|
555
|
+
*/ export const ORG_LANDED_AT_PATH = 'landedAtMs';
|
|
551
556
|
export function signupLandingHealth(orgs, ms, maxStranded = MAX_STRANDED_SIGNUPS, windowHours = SIGNUP_LANDING_WINDOW_HOURS, graceMinutes = SIGNUP_LANDING_GRACE_MINUTES) {
|
|
552
557
|
const shape = {
|
|
553
558
|
windowHours,
|
|
@@ -564,31 +569,41 @@ export function signupLandingHealth(orgs, ms, maxStranded = MAX_STRANDED_SIGNUPS
|
|
|
564
569
|
strandedOrgs: null,
|
|
565
570
|
strandedPeople: null,
|
|
566
571
|
strandedStreak: null,
|
|
572
|
+
unmarkedOrgs: null,
|
|
567
573
|
orgsInWindow: null
|
|
568
574
|
}, shape);
|
|
569
575
|
}
|
|
570
576
|
// `!(n > 0)` rather than `n === 0`, so a corrupt count (NaN, a string)
|
|
571
577
|
// reads as "no site" and cannot hide a stranded org.
|
|
572
578
|
const hasSite = (landing)=>Number(landing.hostCount) > 0;
|
|
573
|
-
|
|
579
|
+
// Each org is one of: arrived (a site, or its owner reached the console),
|
|
580
|
+
// stranded (neither, and the marker says so), or unknown (neither, and it
|
|
581
|
+
// predates the marker).
|
|
582
|
+
const arrived = (landing)=>hasSite(landing) || landing.landed === true;
|
|
583
|
+
const isStranded = (landing)=>!arrived(landing) && landing.landed === false;
|
|
584
|
+
const strandedOrgs = orgs.filter(isStranded).length;
|
|
585
|
+
const unmarkedOrgs = orgs.filter((landing)=>!arrived(landing) && landing.landed === undefined).length;
|
|
574
586
|
const people = new Map();
|
|
575
587
|
orgs.forEach((landing, index)=>{
|
|
576
|
-
var _ref, _ref1;
|
|
588
|
+
var _ref, _ref1, _ref2;
|
|
577
589
|
const key = landing.visitor ? `visitor:${landing.visitor}` : `org:${index}`;
|
|
578
590
|
const createdAtMs = Number(landing.createdAtMs);
|
|
579
591
|
const latestMs = Number.isFinite(createdAtMs) ? createdAtMs : 0;
|
|
580
592
|
const person = people.get(key);
|
|
581
593
|
people.set(key, {
|
|
582
594
|
latestMs: Math.max((_ref = person == null ? void 0 : person.latestMs) != null ? _ref : 0, latestMs),
|
|
583
|
-
|
|
595
|
+
arrived: ((_ref1 = person == null ? void 0 : person.arrived) != null ? _ref1 : false) || arrived(landing),
|
|
596
|
+
stranded: ((_ref2 = person == null ? void 0 : person.stranded) != null ? _ref2 : false) || isStranded(landing)
|
|
584
597
|
});
|
|
585
598
|
});
|
|
599
|
+
// A person with nothing but unmarked orgs is not graded at all: neither
|
|
600
|
+
// arrived nor stranded, so they neither start nor break a streak.
|
|
586
601
|
const newestFirst = [
|
|
587
602
|
...people.values()
|
|
588
|
-
].sort((a, b)=>b.latestMs - a.latestMs);
|
|
589
|
-
const strandedPeople = newestFirst.filter((person)=>!person.
|
|
603
|
+
].filter((person)=>person.arrived || person.stranded).sort((a, b)=>b.latestMs - a.latestMs);
|
|
604
|
+
const strandedPeople = newestFirst.filter((person)=>!person.arrived).length;
|
|
590
605
|
let strandedStreak = 0;
|
|
591
|
-
while(strandedStreak < newestFirst.length && !newestFirst[strandedStreak].
|
|
606
|
+
while(strandedStreak < newestFirst.length && !newestFirst[strandedStreak].arrived){
|
|
592
607
|
strandedStreak += 1;
|
|
593
608
|
}
|
|
594
609
|
const stranded = strandedStreak >= maxStranded;
|
|
@@ -601,6 +616,7 @@ export function signupLandingHealth(orgs, ms, maxStranded = MAX_STRANDED_SIGNUPS
|
|
|
601
616
|
strandedOrgs,
|
|
602
617
|
strandedPeople,
|
|
603
618
|
strandedStreak,
|
|
619
|
+
unmarkedOrgs,
|
|
604
620
|
orgsInWindow: orgs.length
|
|
605
621
|
}, shape);
|
|
606
622
|
}
|
|
@@ -1947,6 +1963,97 @@ export async function writeCronBeat(store, jobId, now = Date.now()) {
|
|
|
1947
1963
|
return false;
|
|
1948
1964
|
}
|
|
1949
1965
|
}
|
|
1966
|
+
/**
|
|
1967
|
+
* How long a job's own document may lag its summary entry when the jobs are
|
|
1968
|
+
* marked together (AGL-3932).
|
|
1969
|
+
*
|
|
1970
|
+
* The reader judges from the summary and opens a job's own document only to
|
|
1971
|
+
* confirm an entry the summary calls late, so a job marked every fifteen
|
|
1972
|
+
* minutes does not need its own document rewritten every fifteen minutes:
|
|
1973
|
+
* half an hour, under every plugin job's forty-five-minute grace, keeps the
|
|
1974
|
+
* confirming mark fresh enough to agree with a summary that has stopped.
|
|
1975
|
+
*/ export const CRON_BEAT_OWN_MARK_REFRESH_MS = 30 * 60000;
|
|
1976
|
+
/**
|
|
1977
|
+
* The marks one run leaves for several jobs at once — the plugin-crons route
|
|
1978
|
+
* stamps twenty jobs a run, and {@link writeCronBeat} per job was forty
|
|
1979
|
+
* writes every fifteen minutes, half of everything the platform wrote
|
|
1980
|
+
* (AGL-3932).
|
|
1981
|
+
*
|
|
1982
|
+
* One summary write carries every job's entry (merged, like the single mark's).
|
|
1983
|
+
* A job's own document is rewritten only when the summary's current entry
|
|
1984
|
+
* for it is missing or older than {@link CRON_BEAT_OWN_MARK_REFRESH_MS},
|
|
1985
|
+
* which one read of the summary tells; a store without `get`, or a summary
|
|
1986
|
+
* that cannot be read or written, falls back to the single-mark shape for
|
|
1987
|
+
* every job, so no run can leave fewer marks than before.
|
|
1988
|
+
*
|
|
1989
|
+
* NEVER THROWS, as {@link writeCronBeat} never throws, and for the same
|
|
1990
|
+
* reason. Returns false only when nothing landed for some job.
|
|
1991
|
+
*/ export async function writeCronBeats(store, jobIds, now = Date.now()) {
|
|
1992
|
+
const ids = [
|
|
1993
|
+
...new Set(jobIds)
|
|
1994
|
+
].filter((id)=>!CRON_BEAT_RESERVED_DOCS.has(id));
|
|
1995
|
+
if (!ids.length) return true;
|
|
1996
|
+
const beats = store.collection(CRON_BEAT_COLLECTION);
|
|
1997
|
+
const summary = beats.doc(CRON_BEAT_SUMMARY_DOC);
|
|
1998
|
+
let current = null;
|
|
1999
|
+
try {
|
|
2000
|
+
if (summary.get) {
|
|
2001
|
+
const snapshot = await summary.get();
|
|
2002
|
+
current = new Map(cronBeatsFromSummary(snapshot.get('beats')).map((beat)=>[
|
|
2003
|
+
beat.jobId,
|
|
2004
|
+
beat.atMs
|
|
2005
|
+
]));
|
|
2006
|
+
}
|
|
2007
|
+
} catch (unused) {
|
|
2008
|
+
// Unreadable: every job gets its own mark below, as before.
|
|
2009
|
+
}
|
|
2010
|
+
const refresh = current === null ? ids : ids.filter((id)=>{
|
|
2011
|
+
const last = current.get(id);
|
|
2012
|
+
return last === undefined || now - last >= CRON_BEAT_OWN_MARK_REFRESH_MS || now < last;
|
|
2013
|
+
});
|
|
2014
|
+
let landed = true;
|
|
2015
|
+
for (const id of refresh){
|
|
2016
|
+
try {
|
|
2017
|
+
await beats.doc(id).set({
|
|
2018
|
+
jobId: id,
|
|
2019
|
+
atMs: now,
|
|
2020
|
+
at: new Date(now).toISOString()
|
|
2021
|
+
}, {
|
|
2022
|
+
merge: true
|
|
2023
|
+
});
|
|
2024
|
+
} catch (unused) {
|
|
2025
|
+
landed = false;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
try {
|
|
2029
|
+
await summary.set({
|
|
2030
|
+
beats: Object.fromEntries(ids.map((id)=>[
|
|
2031
|
+
id,
|
|
2032
|
+
now
|
|
2033
|
+
]))
|
|
2034
|
+
}, {
|
|
2035
|
+
merge: true
|
|
2036
|
+
});
|
|
2037
|
+
} catch (unused) {
|
|
2038
|
+
// The summary is what the reader reads first; without it, give every job
|
|
2039
|
+
// the mark the reader confirms against, as the single mark would have.
|
|
2040
|
+
for (const id of ids){
|
|
2041
|
+
if (refresh.includes(id)) continue;
|
|
2042
|
+
try {
|
|
2043
|
+
await beats.doc(id).set({
|
|
2044
|
+
jobId: id,
|
|
2045
|
+
atMs: now,
|
|
2046
|
+
at: new Date(now).toISOString()
|
|
2047
|
+
}, {
|
|
2048
|
+
merge: true
|
|
2049
|
+
});
|
|
2050
|
+
} catch (unused) {
|
|
2051
|
+
landed = false;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return landed;
|
|
2056
|
+
}
|
|
1950
2057
|
/**
|
|
1951
2058
|
* The beats a summary document holds, ignoring anything malformed.
|
|
1952
2059
|
*
|