@ham2k/extension-sdk 0.10.0 → 0.11.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/dist/activityExports.js +8 -4
- package/dist/index.d.ts +26 -2
- package/docs/distribution.md +219 -113
- package/docs/hooks.md +47 -8
- package/docs/settings.md +1 -1
- package/package.json +4 -4
- package/samples/k2hrc-cqww/src/index.ts +2 -2
package/dist/activityExports.js
CHANGED
|
@@ -83,7 +83,8 @@ function activityExportHook(rules) {
|
|
|
83
83
|
exportData: args.exportData,
|
|
84
84
|
exportTitle: args.exportTitle,
|
|
85
85
|
mainHandler: rules.key,
|
|
86
|
-
includeFieldsFrom: rules.includeFieldsFrom
|
|
86
|
+
includeFieldsFrom: rules.includeFieldsFrom,
|
|
87
|
+
operatorFallback: rules.operatorFallback
|
|
87
88
|
});
|
|
88
89
|
return {
|
|
89
90
|
filename: filenameFor(args.operation, args.qsos, wanted, args.compactFilenames),
|
|
@@ -155,7 +156,8 @@ function huntingExportHook(rules) {
|
|
|
155
156
|
// key ('pota'), which is what its `adifFields` hook registers under;
|
|
156
157
|
// the '-hunter' suffix belongs to the export registration alone.
|
|
157
158
|
mainHandler: rules.key,
|
|
158
|
-
includeFieldsFrom: rules.includeFieldsFrom
|
|
159
|
+
includeFieldsFrom: rules.includeFieldsFrom,
|
|
160
|
+
operatorFallback: rules.operatorFallback
|
|
159
161
|
});
|
|
160
162
|
return {
|
|
161
163
|
// The FULL qsos list, matching the option's filename — see
|
|
@@ -177,7 +179,8 @@ async function adifForExport({
|
|
|
177
179
|
exportData,
|
|
178
180
|
exportTitle,
|
|
179
181
|
mainHandler,
|
|
180
|
-
includeFieldsFrom
|
|
182
|
+
includeFieldsFrom,
|
|
183
|
+
operatorFallback
|
|
181
184
|
}) {
|
|
182
185
|
const { hooks } = await import("@ham2k/extension-sdk");
|
|
183
186
|
const entries = await hooks.invokeOne("export", "adif", "generateExport", {
|
|
@@ -193,7 +196,8 @@ async function adifForExport({
|
|
|
193
196
|
exportData,
|
|
194
197
|
exportTitle,
|
|
195
198
|
mainHandler,
|
|
196
|
-
includeFieldsFrom
|
|
199
|
+
includeFieldsFrom,
|
|
200
|
+
operatorFallback
|
|
197
201
|
});
|
|
198
202
|
const content = entries.find((e) => e.ok)?.value?.content;
|
|
199
203
|
if (!content) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.
|
|
1
|
+
// @ham2k/extension-sdk 0.11.0
|
|
2
2
|
/** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
|
|
3
3
|
export interface PanelScene {
|
|
4
4
|
version: 1;
|
|
@@ -661,6 +661,7 @@ export interface AdifFieldsHook {
|
|
|
661
661
|
fieldsForOneQSO(args: {
|
|
662
662
|
qso: Record<string, JSONValue>;
|
|
663
663
|
operation: Record<string, JSONValue>;
|
|
664
|
+
mainHandler?: boolean;
|
|
664
665
|
}, ctx: HookContext): Promise<{
|
|
665
666
|
name: string;
|
|
666
667
|
value: string;
|
|
@@ -718,7 +719,9 @@ export interface ExportRequest {
|
|
|
718
719
|
includePrivateData?: boolean;
|
|
719
720
|
mainHandler?: string;
|
|
720
721
|
includeFieldsFrom?: string[];
|
|
722
|
+
operatorFallback?: OperatorFallback;
|
|
721
723
|
}
|
|
724
|
+
export type OperatorFallback = "base" | "station";
|
|
722
725
|
export interface ExportResult {
|
|
723
726
|
filename: string;
|
|
724
727
|
mimeType: string;
|
|
@@ -1057,6 +1060,24 @@ export interface SyncTransportHook {
|
|
|
1057
1060
|
ref?: string | null;
|
|
1058
1061
|
error?: string;
|
|
1059
1062
|
}>;
|
|
1063
|
+
uploadStashFile?(args: {
|
|
1064
|
+
file: {
|
|
1065
|
+
filename: string;
|
|
1066
|
+
content: string;
|
|
1067
|
+
mimeType?: string;
|
|
1068
|
+
format?: string;
|
|
1069
|
+
};
|
|
1070
|
+
groupUuid: string;
|
|
1071
|
+
groupTitle?: string;
|
|
1072
|
+
}, ctx: HookContext): Promise<{
|
|
1073
|
+
uploaded?: boolean;
|
|
1074
|
+
error?: string;
|
|
1075
|
+
fatal?: boolean;
|
|
1076
|
+
}>;
|
|
1077
|
+
listStashFiles?(args: {
|
|
1078
|
+
limit?: number;
|
|
1079
|
+
}, ctx: HookContext): Promise<Record<string, JSONValue>>;
|
|
1080
|
+
emailStashLink?(args: Record<string, never>, ctx: HookContext): Promise<Record<string, JSONValue>>;
|
|
1060
1081
|
}
|
|
1061
1082
|
export interface AccountField {
|
|
1062
1083
|
key: string;
|
|
@@ -1760,6 +1781,7 @@ export interface ActivityExportRules {
|
|
|
1760
1781
|
activationType: string;
|
|
1761
1782
|
icon?: string;
|
|
1762
1783
|
includeFieldsFrom?: string[];
|
|
1784
|
+
operatorFallback?: OperatorFallback;
|
|
1763
1785
|
}
|
|
1764
1786
|
export declare function activityExportHook(rules: ActivityExportRules): {
|
|
1765
1787
|
getExportTypes(): Promise<ExportTypeDefinition[]>;
|
|
@@ -1774,13 +1796,14 @@ export interface HuntingExportRules {
|
|
|
1774
1796
|
icon?: string;
|
|
1775
1797
|
color?: string;
|
|
1776
1798
|
includeFieldsFrom?: string[];
|
|
1799
|
+
operatorFallback?: OperatorFallback;
|
|
1777
1800
|
}
|
|
1778
1801
|
export declare function huntingExportHook(rules: HuntingExportRules): {
|
|
1779
1802
|
getExportTypes(): Promise<ExportTypeDefinition[]>;
|
|
1780
1803
|
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
1781
1804
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
1782
1805
|
};
|
|
1783
|
-
export declare function adifForExport({ operation, qsos, segments, includePrivateData, includeLookupData, exportSettings, exportData, exportTitle, mainHandler, includeFieldsFrom, }: {
|
|
1806
|
+
export declare function adifForExport({ operation, qsos, segments, includePrivateData, includeLookupData, exportSettings, exportData, exportTitle, mainHandler, includeFieldsFrom, operatorFallback, }: {
|
|
1784
1807
|
operation: Record<string, JSONValue>;
|
|
1785
1808
|
qsos: Record<string, JSONValue>[];
|
|
1786
1809
|
segments?: OperationSegmentPayload[];
|
|
@@ -1791,6 +1814,7 @@ export declare function adifForExport({ operation, qsos, segments, includePrivat
|
|
|
1791
1814
|
exportTitle?: string;
|
|
1792
1815
|
mainHandler: string;
|
|
1793
1816
|
includeFieldsFrom?: string[];
|
|
1817
|
+
operatorFallback?: OperatorFallback;
|
|
1794
1818
|
}): Promise<string>;
|
|
1795
1819
|
export interface ActivityAdifImportRules {
|
|
1796
1820
|
sig: string;
|
package/docs/distribution.md
CHANGED
|
@@ -310,12 +310,10 @@ packer refuses anything else.
|
|
|
310
310
|
There is no signing yet, so anyone could claim it, and the key is what the app
|
|
311
311
|
shows when it says where a hook came from.
|
|
312
312
|
|
|
313
|
-
The extensions built into the app use bare keys (`
|
|
314
|
-
reserved too: hook identity has to stay unambiguous
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
the packer cannot, because a standalone tool has no way to know what the app it
|
|
318
|
-
will be installed into contains.
|
|
313
|
+
The core extensions built into the app use bare keys (`adif`, `scoring`, …).
|
|
314
|
+
Those are reserved too: hook identity has to stay unambiguous. The app refuses
|
|
315
|
+
any collision at install — the packer cannot, because a standalone tool has no
|
|
316
|
+
way to know what the app it will be installed into contains.
|
|
319
317
|
|
|
320
318
|
## What an installed extension does not get
|
|
321
319
|
|
|
@@ -325,14 +323,14 @@ follows where the code came from, not what it calls itself. A feature that
|
|
|
325
323
|
needs one degrades the same way it does in a build without that value
|
|
326
324
|
configured.
|
|
327
325
|
|
|
328
|
-
The sandbox is otherwise the same one the
|
|
326
|
+
The sandbox is otherwise the same one the core extensions run in — no
|
|
329
327
|
filesystem, no network beyond the `domains` and `webSockets` the manifest
|
|
330
328
|
declares and the user approved, and timers only within their budget
|
|
331
329
|
(README's "Timers").
|
|
332
330
|
|
|
333
331
|
## Installing one
|
|
334
332
|
|
|
335
|
-
**Settings → Extensions → Install from file…**, pick the `.h2kext`, and agree
|
|
333
|
+
**Settings → Extensions & Features → Install from file…**, pick the `.h2kext`, and agree
|
|
336
334
|
to what it asks for. The runtime restarts with it loaded, and the extension
|
|
337
335
|
appears in its own category alongside the app's own, marked *Installed* and
|
|
338
336
|
with a way to remove it again.
|
|
@@ -381,108 +379,126 @@ came from. Nothing is written until the operator agrees.
|
|
|
381
379
|
|
|
382
380
|
### Pre-loaded extensions
|
|
383
381
|
|
|
384
|
-
The app
|
|
385
|
-
`
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
382
|
+
The app builds in only its **core** extensions — the ones that are the app
|
|
383
|
+
itself: `adif`, `scoring`, `lookups`, `templates`, the command sets and
|
|
384
|
+
`ham2k-lofi` sync. Every other extension is a catalog bundle, and the app ships
|
|
385
|
+
some of them already packaged under `app/assets/preloaded-extensions/`,
|
|
386
|
+
installing them into the ordinary store on first run. Afterwards they are
|
|
387
|
+
ordinary installed extensions — the same rows, the same update check, the same
|
|
388
|
+
Update and uninstall — so an operator starts with a working app and the catalog
|
|
389
|
+
can move it forward from there.
|
|
390
|
+
|
|
391
|
+
Which ones ship is staged. Today it is the catalog twin of every extension the
|
|
392
|
+
app used to build in (see [Retired built-ins](#retired-built-ins)), so that an
|
|
393
|
+
install moving off them has every one to hand with no network. Once installs
|
|
394
|
+
still on a build with the built-ins are few enough, it shrinks to the eight a
|
|
395
|
+
new install needs — `ham2k-pota`, `ham2k-sota`, `ham2k-wwff` and the five
|
|
396
|
+
lookups — and the rest are fetched from the catalog when switched on
|
|
397
|
+
([HALO-743](https://cabo.ham2k.com/halo/c/743)). The app's own tests rely on
|
|
398
|
+
only those eight, the core, and the never-shipped samples, so the shrink
|
|
399
|
+
touches none of them.
|
|
400
|
+
|
|
401
|
+
**They have to be kept current.** A pre-load is frozen into the binary, so
|
|
402
|
+
every release must carry the catalog's current stable release of each one: an
|
|
403
|
+
older one starts every new install behind the catalog until its first update
|
|
404
|
+
check. `node scripts/preloaded-extensions.mjs check` compares them with what
|
|
405
|
+
the catalog serves, byte for byte and at the app's extension API level;
|
|
406
|
+
`node scripts/preloaded-extensions.mjs update` replaces any that are behind
|
|
407
|
+
(`update <key>` adds one). Publishing a new release of a pre-loaded extension
|
|
408
|
+
is therefore also a change to this repository, and the release cut refuses to
|
|
409
|
+
ship pre-loads that fail the check ([releases.md §6.4](https://github.com/ham2k/halo/blob/main/docs/releases.md)).
|
|
390
410
|
|
|
391
411
|
They are installed once, not every launch. A record of the highest version
|
|
392
412
|
ever pre-loaded per key is what makes that true: an app update carrying a
|
|
393
413
|
newer bundle installs it, a catalog release already ahead of it is left
|
|
394
414
|
alone, and a key the operator uninstalled stays gone.
|
|
395
415
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
(`
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
the
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
from the listing fails the upgrade and is asked for again next launch: a
|
|
479
|
-
listing lacks a key the same way whether the catalog will never serve it or has
|
|
480
|
-
not published it yet, and only the app knows which. A built-in retired from the
|
|
481
|
-
catalog on purpose joins that list in the same change.
|
|
416
|
+
An installed bundle may not claim a key the app builds in, and a pre-load is
|
|
417
|
+
no exception: it goes through the same door as any other install.
|
|
418
|
+
|
|
419
|
+
**Build secrets are not among what they get.** Every value a twin once needed —
|
|
420
|
+
SOTA's OAuth client id, WWFF's API key — ships as a constant inside the
|
|
421
|
+
extension that uses it, because these bundles are served to anyone who asks and
|
|
422
|
+
a `.h2kext` is a zip. An installed extension sees no build secret, whatever key
|
|
423
|
+
it claims and wherever it came from.
|
|
424
|
+
|
|
425
|
+
### Retired built-ins
|
|
426
|
+
|
|
427
|
+
The app used to build in forty-one more extensions — the activities, contests,
|
|
428
|
+
lookups, a spot source and a dashboard panel. Their catalog copies are the same
|
|
429
|
+
extensions under the reserved prefix (`pota` became `ham2k-pota`; `ham2k-lookup`
|
|
430
|
+
kept its name), because the catalog cannot serve a bare key. Everything the app
|
|
431
|
+
stores per extension hangs off the name — its settings group, its panel values,
|
|
432
|
+
its account credentials, the operator's on/off choice — so each twin inherits
|
|
433
|
+
from its built-in. The app keeps a table of the retired keys and whether each
|
|
434
|
+
was on by default (`kRetiredBuiltIns`), since an install may skip any number of
|
|
435
|
+
releases between the last one with its built-ins and this one; it never shrinks.
|
|
436
|
+
|
|
437
|
+
**The move.** An install that ran the built-ins — one whose default profile had
|
|
438
|
+
a callsign before its extension state was first recorded
|
|
439
|
+
(`seedExtensionsMigratedDefault`) — moves onto their twins at its first boot on
|
|
440
|
+
a build without them, unasked: there is nothing left to run instead. The boot's
|
|
441
|
+
catch-up (`catchUpOnCatalogExtensions`) carries each built-in's on/off choice
|
|
442
|
+
onto its twin, over whatever an earlier attempt left there; installs the
|
|
443
|
+
pre-loads, which carry the built-in's settings, panel values and credentials on
|
|
444
|
+
a twin's first install, and its default where the operator never chose; then
|
|
445
|
+
fetches the twin of any other built-in the operator had switched on. It records
|
|
446
|
+
the move done once the pre-load has run, so a boot killed half way carries the
|
|
447
|
+
choices again next time. It needs no network while every twin is pre-loaded.
|
|
448
|
+
The boot starts the extension runtime before settings are wired in, so a
|
|
449
|
+
catch-up that finds none does not count, and wiring them runs it.
|
|
450
|
+
Choices keep being carried after the move, too: onboarding and the
|
|
451
|
+
`ONBOARD_AS` define name activities by their built-in keys, and
|
|
452
|
+
`offeredKeyFor` writes each choice under both names, so a twin that arrives
|
|
453
|
+
later still inherits it.
|
|
454
|
+
|
|
455
|
+
Once moved, the operator sees one notice — not a dialog, since the move was not
|
|
456
|
+
theirs to decide — saying their extensions now come from the catalog, naming
|
|
457
|
+
anything that did not come along, and counting the twins still to arrive from a
|
|
458
|
+
catalog that could not be reached. Any catch-up, the move or not, raises the
|
|
459
|
+
same notice when an extension was put aside or the runtime would not start at
|
|
460
|
+
all, and says so in place of "ready".
|
|
461
|
+
|
|
462
|
+
A pre-load is recorded as offered only once it installed, or when the store
|
|
463
|
+
refuses the bundle itself: a keychain that would not open or a full disk is
|
|
464
|
+
tried again next launch, and meanwhile the adoption fetches the catalog's copy
|
|
465
|
+
if the operator had it on. A twin the pre-load did install is never fetched
|
|
466
|
+
back after the operator uninstalls it. A choice stored under the pre-load's own
|
|
467
|
+
name — `ham2k-lookup`'s, which kept its name — is kept rather than dropped as
|
|
468
|
+
stale.
|
|
469
|
+
|
|
470
|
+
**What does not come along.** A few built-ins have no twin on purpose — `qp`,
|
|
471
|
+
which the catalog carries as one extension per party instead, and `stateparks`,
|
|
472
|
+
one per event — and the app names them itself (`kBuiltInsWithoutTwin`). The
|
|
473
|
+
notice names each one the operator had switched on, since it is where they learn
|
|
474
|
+
it is gone. Any other twin missing from the listing is asked for again next
|
|
475
|
+
launch: a listing lacks a key the same way whether the catalog will never serve
|
|
476
|
+
it or has not published it yet, and only the app knows which. A built-in
|
|
477
|
+
retired from the catalog on purpose joins that list in the same change.
|
|
478
|
+
|
|
479
|
+
**A twin that will not run.** A bundle the engine will not evaluate takes the
|
|
480
|
+
whole runtime with it, so the catch-up puts that one aside — the boot names the
|
|
481
|
+
bundle it died on — and restarts once more, the notice naming it in a sentence
|
|
482
|
+
of its own. Refused rather than withdrawn: the panel still lists it, badged,
|
|
483
|
+
because the row is where an operator sees that something they installed is
|
|
484
|
+
doing nothing, and updating it there is what lifts the refusal. Keyed by the
|
|
485
|
+
VERSION that failed, so a fixed release arrives as an ordinary update and
|
|
486
|
+
clears it. Only an INSTALLED extension is ever put aside; a core one that will
|
|
487
|
+
not evaluate is a defect in the app, and skipping it would ship an install
|
|
488
|
+
quietly missing part of itself. A refused extension is treated like a revoked
|
|
489
|
+
one in everything else: it keeps its switch, and its downloaded data expires on
|
|
490
|
+
the same clock as any extension the app is not loading. A failure that names no
|
|
491
|
+
bundle — a kernel that will not load, an extension that throws while
|
|
492
|
+
ACTIVATING — puts nothing aside, and is reported.
|
|
493
|
+
|
|
494
|
+
A twin fills the same lookup categories its built-in did, under data-file keys
|
|
495
|
+
of its own. Each retired built-in is stamped disabled, so a week on the
|
|
496
|
+
stale-data purge clears the data files it left — leaving alone the rows a live
|
|
497
|
+
extension's data file still fills.
|
|
482
498
|
|
|
483
499
|
### Installing from the catalog
|
|
484
500
|
|
|
485
|
-
**Settings → Extensions
|
|
501
|
+
**Settings → Extensions & Features** lists what [catalog.ham2k.net](https://catalog.ham2k.net)
|
|
486
502
|
publishes on its `stable` channel, in the catalog's own order for the
|
|
487
503
|
operator's callsign (`relevance`, above). Install and Update go through the same consent screen as a
|
|
488
504
|
file, over bytes that must hash to the sha256 the listing gives — a mismatch
|
|
@@ -496,6 +512,39 @@ catalog's own bundle links (`…/api/v1/extensions/<key>/versions/<version>/bund
|
|
|
496
512
|
take this verified path when they arrive as a deep link; every other host
|
|
497
513
|
takes the plain one above.
|
|
498
514
|
|
|
515
|
+
### Release tracks
|
|
516
|
+
|
|
517
|
+
A catalog channel — `stable`, `unstable` or `bleeding` — says how finished a
|
|
518
|
+
release is, and the catalog cascades: asking for `unstable` sees unstable and
|
|
519
|
+
stable releases. The panel's listing asks for `stable`. Under developer mode,
|
|
520
|
+
**Developer → Extension Catalog → Catalog Search Tracks** (Only Stable,
|
|
521
|
+
Unstable or better, Bleeding or better) widens it; turning developer mode off
|
|
522
|
+
puts the listing back on stable without touching anything installed. Only
|
|
523
|
+
the panel's listing reads it: fetching the catalog twins of retired built-ins
|
|
524
|
+
reads stable, since those install with no consent screen, and restoring a
|
|
525
|
+
missing extension reads the track it followed.
|
|
526
|
+
|
|
527
|
+
The other way to an unfinished release is a link: the catalog's install
|
|
528
|
+
button, or an address pasted into the panel's search field — a catalog
|
|
529
|
+
bundle URL, an extension's page (`/extensions/<key>?channel=unstable`, which
|
|
530
|
+
installs that channel's current release), the `install_extension` deep link
|
|
531
|
+
itself, or an https URL ending in `.h2kext`; anything else is a search. A
|
|
532
|
+
bundle URL is resolved against the listing on `bleeding`, which sees every
|
|
533
|
+
release, and the version's channel is the steadiest one it is approved on. A
|
|
534
|
+
page link records the channel it asked for as the track even when the release
|
|
535
|
+
there turns out to be steadier — the operator chose that track.
|
|
536
|
+
|
|
537
|
+
Whichever way it arrives, the consent screen warns over a release that is not
|
|
538
|
+
stable, and the install records its channel in the provenance record as the
|
|
539
|
+
TRACK the extension follows (`channel`, absent meaning stable). The update
|
|
540
|
+
check asks about each extension on its own track — one request per track in
|
|
541
|
+
use, side by side — and offers only what is newer than what is installed, so an extension installed at `0.1` unstable is updated to `1.0` stable,
|
|
542
|
+
and then to `1.0.1` unstable. The track only widens on an install: a stable
|
|
543
|
+
release landing over an unstable install leaves it on unstable. A pre-load
|
|
544
|
+
counts as the catalog's own here. The row and the details show the track, and
|
|
545
|
+
the details' **Follow stable releases only** puts it back on stable; an
|
|
546
|
+
uninstall or a file install clears it with the rest of the record.
|
|
547
|
+
|
|
499
548
|
At most once every six hours — on launch and on resume — the app asks the
|
|
500
549
|
catalog whether anything installed has a newer release, and says so in the
|
|
501
550
|
status bar and on the row; the notice comes down with the last update taken.
|
|
@@ -526,15 +575,72 @@ Beyond what the packer already refused:
|
|
|
526
575
|
|
|
527
576
|
Each has its own message. A bundle that cannot be installed says why.
|
|
528
577
|
|
|
529
|
-
##
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
578
|
+
## Where installed bundles live
|
|
579
|
+
|
|
580
|
+
Everything above is platform-neutral — the zip walk, the manifest rules, the
|
|
581
|
+
consent screen, the hash check. What differs is only where the bytes are kept
|
|
582
|
+
(`BundleStorage`):
|
|
583
|
+
|
|
584
|
+
- **natively**, one directory per key under the app's data directory, one
|
|
585
|
+
file per bundle entry;
|
|
586
|
+
- **on web**, one IndexedDB record per key (database `halo-extensions`),
|
|
587
|
+
holding every entry's bytes under its own name. IndexedDB rather than the
|
|
588
|
+
Origin Private File System, which would mirror the native layout: every
|
|
589
|
+
browser HaLo runs in supports it the same way, while OPFS writes from the
|
|
590
|
+
page are newer and patchier on Safari.
|
|
591
|
+
|
|
592
|
+
So the web build installs from a file, installs and updates from the catalog,
|
|
593
|
+
pre-loads the shipped bundles and moves off the retired built-ins exactly as
|
|
594
|
+
the native ones do.
|
|
595
|
+
|
|
596
|
+
### When the store is lost
|
|
597
|
+
|
|
598
|
+
The store can go while the preferences describing it survive: a browser
|
|
599
|
+
clearing IndexedDB alone, a data directory deleted by hand, a store that
|
|
600
|
+
failed mid-session. (When a browser clears a whole site, preferences go with
|
|
601
|
+
it, and the app starts as a new install.) So what the operator installed is
|
|
602
|
+
kept apart from the store that holds it, and put back from there.
|
|
603
|
+
|
|
604
|
+
`extensionsInstalled` records every extension the operator installed
|
|
605
|
+
(`{key: name}`). Every install writes it; only an uninstall — or Forget, below
|
|
606
|
+
— removes it. An install that predates it is seeded from its store once, and
|
|
607
|
+
after that it never follows the store: a store found short is exactly what it
|
|
608
|
+
must not shrink to. Whatever is recorded there and not in the store is
|
|
609
|
+
**missing**, however it went — judged by the keys the store holds, not by the
|
|
610
|
+
extensions the app offers, since a built-in or a dev server can stand in front
|
|
611
|
+
of an installed bundle. A store that cannot be read at all has lost nothing:
|
|
612
|
+
that launch offers nothing, rather than everything back into a failing store.
|
|
613
|
+
Where each came from is its provenance record's answer, and a key with none
|
|
614
|
+
came from a file.
|
|
615
|
+
|
|
616
|
+
The operator's on/off choice for a missing extension is kept too: the boot
|
|
617
|
+
cleanup that prunes settings for keys the app no longer knows treats every
|
|
618
|
+
recorded key as known.
|
|
619
|
+
|
|
620
|
+
Putting them back, after the app starts:
|
|
621
|
+
|
|
622
|
+
- **Shipped bundles** come back at boot from the app's own assets, unasked and
|
|
623
|
+
offline. The pre-load record says they were already offered, which is true
|
|
624
|
+
and beside the point; they are installed anyway, never as a first install,
|
|
625
|
+
so their settings and on/off choice stand. An install that has not taken the
|
|
626
|
+
upgrade gets back only what it had.
|
|
627
|
+
- **Everything else** is offered in a dialog once the boot has settled:
|
|
628
|
+
- extensions from the catalog in use are listed to install, at the release
|
|
629
|
+
it serves now. **Later** keeps every one for the next launch;
|
|
630
|
+
- extensions from a file are listed to install again from their files, and
|
|
631
|
+
ones from a different catalog as not available here. Neither is fetched —
|
|
632
|
+
the bytes would come from a publisher the operator never agreed to;
|
|
633
|
+
- every row has **Forget**, which drops it with everything it stored — the
|
|
634
|
+
only way out for one the catalog no longer serves, which would otherwise
|
|
635
|
+
be offered, and fail, every launch.
|
|
636
|
+
- **Install** installs each on its own: one that is not listed, will not
|
|
637
|
+
download or fails the check costs only itself. The runtime restarts with
|
|
638
|
+
everything that landed; a bundle it will not evaluate is set aside, as the
|
|
639
|
+
upgrade does, and the runtime restarts without it. The result names what
|
|
640
|
+
could not be installed (offered again next launch) and what installed but
|
|
641
|
+
would not run (kept, marked in the panel, where an update is the way back);
|
|
642
|
+
says so when the runtime would not start at all, rather than calling it a
|
|
643
|
+
success; and says the app is still busy — offering them again next launch
|
|
644
|
+
— when another catalog pass holds the store.
|
|
645
|
+
- The offer waits for the boot's catch-up to finish, or for an upgrade that
|
|
646
|
+
every catch-up stepped aside for to let go.
|
package/docs/hooks.md
CHANGED
|
@@ -162,7 +162,8 @@ describes it whenever it was last worked. It still labels a carried-forward
|
|
|
162
162
|
field by where it came from (`Call History` for a typed one, `Call History
|
|
163
163
|
(guess)` for a guessed one), and a station's whole location travels in one
|
|
164
164
|
of the two so the merge's grid/city reconciliation cannot split it — see
|
|
165
|
-
`
|
|
165
|
+
`ham2k-call-history`'s `src/index.ts` in
|
|
166
|
+
[ham2k/extensions](https://github.com/ham2k/extensions).
|
|
166
167
|
|
|
167
168
|
## ✅ `recentContextLookup` — live-context lookups
|
|
168
169
|
|
|
@@ -741,12 +742,17 @@ knowing them. Called by the `adif` extension **inside the runtime** via
|
|
|
741
742
|
|
|
742
743
|
```ts
|
|
743
744
|
interface AdifFieldsHook {
|
|
744
|
-
fieldsForOneQSO(args: { qso, operation }, ctx): Promise<{name, value}[]>
|
|
745
|
+
fieldsForOneQSO(args: { qso, operation, mainHandler? }, ctx): Promise<{name, value}[]>
|
|
745
746
|
// One field set PER ADIF RECORD this contact should produce.
|
|
746
|
-
fieldCombinationsForOneQSO?(args: { qso, operation }, ctx): Promise<{name, value}[][]>
|
|
747
|
+
fieldCombinationsForOneQSO?(args: { qso, operation, mainHandler? }, ctx): Promise<{name, value}[][]>
|
|
747
748
|
}
|
|
748
749
|
```
|
|
749
750
|
|
|
751
|
+
`mainHandler` is true when the file is this extension's own export — app-polo's
|
|
752
|
+
argument of the same name. It is how a program writes what only its own
|
|
753
|
+
submission asks for: SOTA's files carry both stations' gridsquares, which the
|
|
754
|
+
full export leaves to the contact's own privacy rules.
|
|
755
|
+
|
|
750
756
|
POTA contributes `SIG/SIG_INFO/POTA_REF` (hunted refs on the QSO) and
|
|
751
757
|
`MY_SIG/MY_SIG_INFO/MY_POTA_REF` (activation refs on the operation).
|
|
752
758
|
|
|
@@ -819,11 +825,24 @@ would claim no reference at all.
|
|
|
819
825
|
**A field name is written once per record, and the first to carry a value
|
|
820
826
|
wins.** ADIF gives no meaning to a repeated field, so where two hooks answer
|
|
821
827
|
the same name — the full export asks every program on the contact, and a park
|
|
822
|
-
and a summit both answer `MY_SIG` — the later one is dropped
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
828
|
+
and a summit both answer `MY_SIG` — the later one is dropped. Hooks are asked
|
|
829
|
+
main handler first, then each `includeFieldsFrom` key in the order given, so
|
|
830
|
+
that order decides — whichever of the two methods each hook answered. The
|
|
831
|
+
contact's own fields come after every hook's: a program knows what its file
|
|
832
|
+
needs in a field better than the contact's generic answer does (the section a
|
|
833
|
+
Field Day station sent, a satellite's downlink band). The exception is what
|
|
834
|
+
identifies the contact — `CALL`, `QSO_DATE`, `TIME_ON`, `QSO_DATE_OFF`,
|
|
835
|
+
`TIME_OFF`, `BAND`, `FREQ`, `MODE`, `SUBMODE`, `STATION_CALLSIGN`,
|
|
836
|
+
`OPERATOR`: a hook answering one of those is dropped, since it would rewrite
|
|
837
|
+
every record it is asked about, the whole-log backup included. A program whose
|
|
838
|
+
uploader needs `OPERATOR` on every record asks for it on the export instead,
|
|
839
|
+
`ExportRequest.operatorFallback` (`operatorFallback` on `activityExportHook`'s
|
|
840
|
+
and `huntingExportHook`'s rules): the core writes `OPERATOR` only where it says
|
|
841
|
+
something `STATION_CALLSIGN` doesn't, and with `'base'` or `'station'` it
|
|
842
|
+
writes the contact's own operator on every record, falling back to the station
|
|
843
|
+
call's base call or the station call itself. This is a
|
|
844
|
+
deliberate divergence from app-polo, whose contact fields come first and whose
|
|
845
|
+
main handler's are appended unchecked, so a name both answer is written twice. The **full export**, which asks every
|
|
827
846
|
program on the contact, has no such order: hooks answering `fieldCombinationsForOneQSO` are asked as one
|
|
828
847
|
group and hooks answering `fieldsForOneQSO` as another, so which of two programs
|
|
829
848
|
keeps a shared `MY_SIG` there is not something either of them chose.
|
|
@@ -1397,6 +1416,26 @@ report — as a real CaBo card; the host writes the title, body and kind, the
|
|
|
1397
1416
|
transport adds who/where/what-build and owns the account gate.
|
|
1398
1417
|
Implemented by: `ham2k-lofi`.
|
|
1399
1418
|
|
|
1419
|
+
`uploadStashFile` / `listStashFiles` / `emailStashLink`
|
|
1420
|
+
(optional) back the Exports pane's Stash button, which sends export files
|
|
1421
|
+
to a store the operator fetches from any browser (Ham2K's File Stash, at
|
|
1422
|
+
stash.ham2k.net). `uploadStashFile` takes ONE file and the `groupUuid` of
|
|
1423
|
+
the batch it belongs to — the host mints it, since the sandbox has no uuid
|
|
1424
|
+
source, and calls once per file: its call timeout does not stop a hook, so a
|
|
1425
|
+
whole batch in one call would go on uploading unseen after the host had
|
|
1426
|
+
told the operator it failed. It answers `{ uploaded: true }` or `{ error }`,
|
|
1427
|
+
never a throw, with `fatal: true` on any failure but the server refusing
|
|
1428
|
+
that one file — sync off, no account or permission, no answer — so the host
|
|
1429
|
+
stops instead of having every remaining file fail the same way. No answer
|
|
1430
|
+
says the file may have arrived: a body can reach the server after the reply
|
|
1431
|
+
is lost.
|
|
1432
|
+
`listStashFiles` answers the server's `{ stash_files: [...] }`, newest
|
|
1433
|
+
first, which the host reads to warn a free account before a new stash
|
|
1434
|
+
replaces one not yet fetched; `emailStashLink` mails the account's own
|
|
1435
|
+
address a link to its stash. Both answer `{ error }` for a refusal. All
|
|
1436
|
+
three own the same sync gate `submitReport` does.
|
|
1437
|
+
Implemented by: `ham2k-lofi`.
|
|
1438
|
+
|
|
1400
1439
|
## ✅ `panel` — panes an extension contributes to a view
|
|
1401
1440
|
|
|
1402
1441
|
A panel is a PANE the operator can put in a customizable view's layout
|
package/docs/settings.md
CHANGED
|
@@ -521,5 +521,5 @@ standalone dialog — not inline in a `settingsPanel`'s `FormRenderer`.
|
|
|
521
521
|
|
|
522
522
|
A `settingsPanel` field can reference an account for display/navigation via
|
|
523
523
|
`fieldType: 'account'` (see above), but the account itself is still fully
|
|
524
|
-
owned by its `AccountHook` registration. `
|
|
524
|
+
owned by its `AccountHook` registration. `ham2k-qrz` continues
|
|
525
525
|
to register under `account` — there is no migration to do.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ham2k/extension-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ham2k",
|
|
@@ -71,12 +71,12 @@
|
|
|
71
71
|
"build": "node build.mjs"
|
|
72
72
|
},
|
|
73
73
|
"//peerDependencies": [
|
|
74
|
-
"The libraries the extension host
|
|
74
|
+
"The libraries the extension host provides. An extension does NOT need these",
|
|
75
75
|
"installed: the build preset rewrites each one the manifest declares into a",
|
|
76
76
|
"lookup on the host's single instance, before esbuild ever resolves it, and",
|
|
77
|
-
"the published .d.ts
|
|
77
|
+
"the published .d.ts has their types rolled in. They are here, optional,",
|
|
78
78
|
"for the one case that does reach the filesystem \u2014 `inline: [...]`, which",
|
|
79
|
-
"ships your own copy because you need a version the host does not
|
|
79
|
+
"ships your own copy because you need a version the host does not provide."
|
|
80
80
|
],
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"@ham2k/lib-callsigns": "*",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
// band, one contact per station per band. What this sample leaves out is
|
|
18
18
|
// everything around them — Cabrillo export, the ADIF exchange fields, the
|
|
19
19
|
// country-file zone suggestion, and writing a guessed exchange onto the QSO.
|
|
20
|
-
// See `docs/hooks.md` §`scoring`, and `
|
|
21
|
-
//
|
|
20
|
+
// See `docs/hooks.md` §`scoring`, and the catalog's `ham2k-cqww` for the whole
|
|
21
|
+
// thing.
|
|
22
22
|
|
|
23
23
|
import { annotateCallAgainstCountryFile, contestArithmetic, contestScorer, contestSummary, defineExtension } from "@ham2k/extension-sdk"
|
|
24
24
|
import type { ContestScorer, HookContext, JSONValue, LoggingControlDescriptor, Ref } from "@ham2k/extension-sdk"
|