@ham2k/extension-sdk 0.10.1 → 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.
@@ -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.10.1
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;
@@ -719,7 +719,9 @@ export interface ExportRequest {
719
719
  includePrivateData?: boolean;
720
720
  mainHandler?: string;
721
721
  includeFieldsFrom?: string[];
722
+ operatorFallback?: OperatorFallback;
722
723
  }
724
+ export type OperatorFallback = "base" | "station";
723
725
  export interface ExportResult {
724
726
  filename: string;
725
727
  mimeType: string;
@@ -1058,6 +1060,24 @@ export interface SyncTransportHook {
1058
1060
  ref?: string | null;
1059
1061
  error?: string;
1060
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>>;
1061
1081
  }
1062
1082
  export interface AccountField {
1063
1083
  key: string;
@@ -1761,6 +1781,7 @@ export interface ActivityExportRules {
1761
1781
  activationType: string;
1762
1782
  icon?: string;
1763
1783
  includeFieldsFrom?: string[];
1784
+ operatorFallback?: OperatorFallback;
1764
1785
  }
1765
1786
  export declare function activityExportHook(rules: ActivityExportRules): {
1766
1787
  getExportTypes(): Promise<ExportTypeDefinition[]>;
@@ -1775,13 +1796,14 @@ export interface HuntingExportRules {
1775
1796
  icon?: string;
1776
1797
  color?: string;
1777
1798
  includeFieldsFrom?: string[];
1799
+ operatorFallback?: OperatorFallback;
1778
1800
  }
1779
1801
  export declare function huntingExportHook(rules: HuntingExportRules): {
1780
1802
  getExportTypes(): Promise<ExportTypeDefinition[]>;
1781
1803
  suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
1782
1804
  generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
1783
1805
  };
1784
- 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, }: {
1785
1807
  operation: Record<string, JSONValue>;
1786
1808
  qsos: Record<string, JSONValue>[];
1787
1809
  segments?: OperationSegmentPayload[];
@@ -1792,6 +1814,7 @@ export declare function adifForExport({ operation, qsos, segments, includePrivat
1792
1814
  exportTitle?: string;
1793
1815
  mainHandler: string;
1794
1816
  includeFieldsFrom?: string[];
1817
+ operatorFallback?: OperatorFallback;
1795
1818
  }): Promise<string>;
1796
1819
  export interface ActivityAdifImportRules {
1797
1820
  sig: string;
@@ -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 (`pota`, `sota`, …). Those are
314
- reserved too: hook identity has to stay unambiguous, and build-secret
315
- visibility is by key prefix, so an installed extension calling itself `sota`
316
- would be asking to read `SOTA_*`. The app refuses any collision at install —
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 built-in extensions run in — no
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 ships eight of the catalog's own extensions already packaged, under
385
- `app/assets/preloaded-extensions/`, and installs them into the ordinary store
386
- on first run: `ham2k-pota`, `ham2k-sota`, `ham2k-wwff`, and the five lookups.
387
- Afterwards they are ordinary installed extensions — the same rows, the same
388
- update check, the same Update and uninstall — so an operator starts with a
389
- working app and the catalog can move it forward from there.
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
- Each one that renames a built-in — `ham2k-pota` is the built-in `pota` under
397
- a name the catalog can serve — carries the operator's settings, panel values
398
- and account credentials across on its first install. Copied, not moved: the
399
- built-in's own state stays where it is.
400
-
401
- One rule bends for these, and only for them:
402
-
403
- - **Built-in collisions.** An installed bundle may not claim a key the app
404
- ships — judged against what the app is currently OFFERING, which once the
405
- upgrade to catalog extensions is taken is the core extensions and nothing
406
- else. `ham2k-lookup`
407
- is both a built-in and a pre-load, and could otherwise never install.
408
-
409
- **Build secrets are not among them, and used to be.** A pre-load holding a
410
- `ham2k-` key could once read the values its name granted. Nothing needs that
411
- now: every value it carried — SOTA's OAuth client id, WWFF's API key — ships
412
- as a constant inside the extension that uses it, because these bundles are
413
- served to anyone who asks and a `.h2kext` is a zip. An installed extension
414
- sees no build secret, whatever key it claims and wherever it came from.
415
-
416
- ### The upgrade to catalog extensions
417
-
418
- A new install — one whose default profile has never had a callsign — starts
419
- on the catalog at its first boot, on the bundles the app ships, with nothing
420
- to offer and nothing to carry; the decision is recorded once
421
- (`seedExtensionsMigratedDefault`). Onboarding still names its activities by
422
- their built-in keys, which `offeredKeyFor` maps to the shipped twin that
423
- actually runs. An install that has been running the app's own extensions is
424
- offered the move at start, on every launch until it is taken. Only a taken upgrade withdraws
425
- anything, and the offer may be deferred. A catalog that cannot be reached
426
- raises no offer and says nothing: it is a launch nobody was watching, and the
427
- offer comes back next time.
428
-
429
- Accepting installs the pre-loads, then fetches the catalog's twin of every
430
- other built-in the operator had switched on — `wca` becomes `ham2k-wca` —
431
- carrying its settings and credentials the same way. Nothing is withdrawn until
432
- that finishes; a download that fails puts the app's own extensions back in use
433
- and the offer returns next launch. Twins that did arrive stay on disk, unused
434
- until an upgrade completes — the built-in each one supersedes is what runs
435
- meanwhile.
436
-
437
- A bundle the engine will not evaluate takes the whole runtime with it, so the
438
- upgrade puts that one aside — the boot names the bundle it died on — and
439
- restarts once more. What boots then is the upgrade minus that extension, named
440
- in the dialog in a sentence of its own. Refused rather than
441
- withdrawn: the panel still lists it, badged, because the row is where an
442
- operator sees that something they installed is doing nothing, and updating it
443
- there is what lifts the refusal. Keyed by the VERSION that failed, so a fixed
444
- release arrives as an ordinary update and clears it. Only an INSTALLED
445
- extension is ever put aside; a built-in that
446
- will not evaluate is a defect in the app, and skipping it would ship an
447
- install quietly missing part of itself.
448
-
449
- A refusal is kept even when putting the bundle aside did not get a runtime
450
- back: it still will not evaluate, and an extension with no built-in behind it
451
- is in the reverted set too, so re-admitting it there would kill the boot that
452
- was about to give the operator everything back. Every dialog, and the notice
453
- that stands in for one, names what stayed out — except a downloaded copy the
454
- revert superseded, whose built-in is running again. Uninstalling clears the
455
- record, so a reinstall of the same bytes gets its chance.
456
-
457
- A refused extension is treated like a revoked one in everything else: it
458
- keeps its switch, and its downloaded data expires on the same clock as any
459
- extension the app is not loading.
460
-
461
- What this does NOT cover is a failure that names no bundle. Only a bundle
462
- that fails while it is being EVALUATED names itself: a kernel that will not
463
- load, or an extension that throws while ACTIVATING, takes the generation down
464
- under a name that is not a key. Those revert the whole upgrade as before, and
465
- it is offered again at the next launch.
466
-
467
- The upgrade restarts the extension runtime, which takes the footer items of
468
- the generation going down with it: a data file's import raises an error rather
469
- than returning into a torn-down runtime, so its progress line is cleared and
470
- the file is left unstamped for the next sweep instead of recorded as fresh
471
- with nothing imported.
472
-
473
- A few built-ins have no twin on purpose — `qp`, which the catalog carries as
474
- one extension per party instead — and the app names them itself
475
- (`kBuiltInsWithoutTwin`). Those are withdrawn without asking the catalog, and
476
- the dialog that reports the upgrade finished names each one the operator had
477
- switched on, since it is where they learn it is gone. Any other twin missing
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 → Catalog** lists what [catalog.ham2k.net](https://catalog.ham2k.net)
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.
@@ -541,8 +590,8 @@ consent screen, the hash check. What differs is only where the bytes are kept
541
590
  page are newer and patchier on Safari.
542
591
 
543
592
  So the web build installs from a file, installs and updates from the catalog,
544
- pre-loads the shipped bundles and takes the upgrade exactly as the native ones
545
- do.
593
+ pre-loads the shipped bundles and moves off the retired built-ins exactly as
594
+ the native ones do.
546
595
 
547
596
  ### When the store is lost
548
597
 
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
- `extensions/lookups/call-history/src/index.ts`.
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
 
@@ -833,7 +834,13 @@ Field Day station sent, a satellite's downlink band). The exception is what
833
834
  identifies the contact — `CALL`, `QSO_DATE`, `TIME_ON`, `QSO_DATE_OFF`,
834
835
  `TIME_OFF`, `BAND`, `FREQ`, `MODE`, `SUBMODE`, `STATION_CALLSIGN`,
835
836
  `OPERATOR`: a hook answering one of those is dropped, since it would rewrite
836
- every record it is asked about, the whole-log backup included. This is a
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
837
844
  deliberate divergence from app-polo, whose contact fields come first and whose
838
845
  main handler's are appended unchecked, so a name both answer is written twice. The **full export**, which asks every
839
846
  program on the contact, has no such order: hooks answering `fieldCombinationsForOneQSO` are asked as one
@@ -1409,6 +1416,26 @@ report — as a real CaBo card; the host writes the title, body and kind, the
1409
1416
  transport adds who/where/what-build and owns the account gate.
1410
1417
  Implemented by: `ham2k-lofi`.
1411
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
+
1412
1439
  ## ✅ `panel` — panes an extension contributes to a view
1413
1440
 
1414
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. `extensions/lookups/qrz` continues
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.10.1",
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",
@@ -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 `contests/cqww` in the app's repository
21
- // for the whole thing.
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"