@ham2k/extension-sdk 1.12.1 → 1.12.2

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/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 1.12.1
1
+ // @ham2k/extension-sdk 1.12.2
2
2
  /** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
3
3
  export interface PanelScene {
4
4
  version: 1;
@@ -1242,6 +1242,14 @@ export interface ChasingProgram {
1242
1242
  supersedes?: string[];
1243
1243
  refLabel?: string;
1244
1244
  imports?: ChasingImporter[];
1245
+ notices?: Partial<Record<ChaseNoticeVerdict, string>>;
1246
+ modeLabels?: Record<string, string>;
1247
+ translations?: Record<string, ChasingNoticeLabels>;
1248
+ }
1249
+ export type ChaseNoticeVerdict = "newRef" | "newBand" | "newMode" | "unconfirmed";
1250
+ export interface ChasingNoticeLabels {
1251
+ notices?: Partial<Record<ChaseNoticeVerdict, string>>;
1252
+ modeLabels?: Record<string, string>;
1245
1253
  }
1246
1254
  export interface ChasingImporter {
1247
1255
  key: string;
@@ -417,12 +417,14 @@ can move it forward from there.
417
417
 
418
418
  Which ones ship is staged. Today it is the catalog twin of every extension the
419
419
  app used to build in (see [Retired built-ins](#retired-built-ins)), so that an
420
- install moving off them has every one to hand with no network. Once installs
421
- still on a build with the built-ins are few enough, it shrinks to the eight a
422
- new install needs — `ham2k-pota`, `ham2k-sota`, `ham2k-wwff` and the five
423
- lookups — and the rest are fetched from the catalog when switched on
420
+ install moving off them has every one to hand with no network, and
421
+ `ham2k-dxcc`, which replaced the core `dxcc` chasing program and is on for
422
+ everyone as that was. Once installs still on a build with the built-ins are few
423
+ enough, it shrinks to the nine a new install needs — `ham2k-pota`,
424
+ `ham2k-sota`, `ham2k-wwff`, `ham2k-dxcc` and the five lookups — and the rest
425
+ are fetched from the catalog when switched on
424
426
  ([HALO-743](https://cabo.ham2k.com/halo/c/743)). The app's own tests rely on
425
- only those eight, the core, and the never-shipped samples, so the shrink
427
+ only those nine, the core, and the never-shipped samples, so the shrink
426
428
  touches none of them.
427
429
 
428
430
  **They have to be kept current.** A pre-load is frozen into the binary, so
package/docs/hooks.md CHANGED
@@ -1607,7 +1607,14 @@ registerHook("chasing", { key: "dxcc", hook: {
1607
1607
  without invoking anything. Raise `version` whenever the same contacts would
1608
1608
  claim something different: the app keeps no claim from an older version.
1609
1609
  `refLabel` is what one reference is called where a notice names it
1610
- ("New POTA Park"), the program's `label` when left out.
1610
+ ("New POTA Park"), the program's `label` when left out. A program may
1611
+ instead word a verdict's notice itself: `notices` by verdict (`newRef`,
1612
+ `newBand`, `newMode`, `unconfirmed`), where `{{band}}` names the band a
1613
+ new band is on and `{{mode}}` the mode a new mode is in, through
1614
+ `modeLabels`, with both per language under `translations` (`{ es: {
1615
+ notices, modeLabels } }`). A verdict it leaves out, or one whose notice
1616
+ lacks the band or mode its words name, reads the app's way; a guess reads
1617
+ "Probably …" around the program's words.
1611
1618
  `supersedes` names the scoring notices the program's verdicts stand in for
1612
1619
  (DXCC: `newDXCC`): wherever the program judges a call or a spot, the
1613
1620
  scorer's notice is dropped in favor of the verdict.
@@ -1662,9 +1669,12 @@ extension API 9: a bundle that registers a program declares `"api": 9`, which
1662
1669
  the packer enforces, so an older app — which would install it and never ask
1663
1670
  it anything — is never offered it.
1664
1671
 
1665
- Implemented by: `dxcc` (core) — Mixed, per-band and per-mode slots for the
1666
- station call worked from each entity; LoTW and paper QSLs confirm, LoTW credit
1667
- verifies. `ham2k-pota` — one slot per park hunted, worked only; its
1672
+ Implemented by: `ham2k-dxcc` (catalog, pre-loaded; core `dxcc` until
1673
+ HALO-844) — Mixed, per-mode and per-band slots for the station call worked
1674
+ from each entity, worded "DXCC ATNO", "DXCC in CW", "DXCC in 80m"; LoTW and
1675
+ paper QSLs confirm, `credits.dxcc` verifies; the DXCC Challenge is a tally of
1676
+ its band slots. A stored goal for `dxcc` carries to it
1677
+ (`kRenamedChasePrograms`). `k2hrc-dxcc` is the SDK's worked example. `ham2k-pota` — one slot per park hunted, worked only; its
1668
1678
  hunted-parks export verifies. `ham2k-was` — Mixed, per-band and per-mode
1669
1679
  slots (plus FT8 and RTTY) for the station call worked from one 4-character
1670
1680
  grid; LoTW and paper QSLs confirm, `WAS` credit verifies.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "1.12.1",
3
+ "version": "1.12.2",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",
package/samples/README.md CHANGED
@@ -10,6 +10,7 @@ building — the header comment in each `src/index.ts` says what it leaves out.
10
10
  | `k2hrc-hamqth` | A callsign lookup, and the `account` hook that asks for a password |
11
11
  | `k2hrc-llota` | An award program: references, an offline list, activation scoring |
12
12
  | `k2hrc-cqww` | A contest: an exchange to type, and a score to keep |
13
+ | `k2hrc-dxcc` | A chasing program: what each contact counts toward, and how its notices read (extension API 9) |
13
14
  | `k2hrc-radio` | An HTML panel, and why you should probably write markdown instead |
14
15
  | `k2hrc-panel-scenes` | Experimental panel scenes: SVG artwork, controls and animations, with simulated radio and weather panels |
15
16
  | `k2hrc-panafall` | A radio's panadapter and waterfall in a scene, styled and tuned by click (extension API 7) |
@@ -0,0 +1,6 @@
1
+ import { build } from "esbuild"
2
+ import { buildExtension } from "@ham2k/extension-tools"
3
+
4
+ await buildExtension(build, { dir: import.meta.dirname })
5
+
6
+ console.log("built build/index.js")
@@ -0,0 +1,23 @@
1
+ {
2
+ "key": "k2hrc-dxcc",
3
+ "name": "DXCC (sample)",
4
+ "shortName": "DXCC*",
5
+ "version": "1.0.0",
6
+ "description": "The DXCC entities a station has worked and confirmed, as a worked example of a chasing program",
7
+ "category": "award",
8
+ "icon": "earth",
9
+ "accentColor": "#2E7D32",
10
+ "api": 9,
11
+ "keywords": ["award", "dxcc", "chasing", "sample"],
12
+ "hooks": ["chasing"],
13
+ "sharedDependencies": {
14
+ "@ham2k/lib-callsigns": "^1.0.0",
15
+ "@ham2k/lib-country-files": "^1.0.0",
16
+ "@ham2k/lib-dxcc-data": "^1.0.0",
17
+ "@ham2k/lib-format-tools": "^1.0.0",
18
+ "@ham2k/lib-geo-tools": "^1.0.0",
19
+ "@ham2k/lib-operation-data": "^1.0.0",
20
+ "i18next": "^23.0.0",
21
+ "liquidjs": "^10.0.0"
22
+ }
23
+ }
@@ -0,0 +1,104 @@
1
+ // Copyright ©️ 2026 Sebastian Delmont <sd@ham2k.com>
2
+ // SPDX-License-Identifier: MIT
3
+ //
4
+ // SAMPLE — a chasing program: what each contact counts toward.
5
+ //
6
+ // An award is a checklist, and a chasing program says which lines of it a
7
+ // contact fills. The app keeps the best claim for each line, ranks spots by
8
+ // what working the station would add, and alerts on what is new. The program
9
+ // never reads the log: `claimsForQsos` is a pure function of each contact,
10
+ // because the app also asks it about spots and about the call being typed.
11
+ //
12
+ // One contact makes several claims, one per kind of line the award keeps —
13
+ // here DXCC's three: Mixed (any band, any mode), its band, and its mode. A
14
+ // missing Mixed line is a new entity, a missing band or mode line a new band
15
+ // or mode, and `notices` says how each reads ("DXCC in 80m").
16
+ //
17
+ // The rules here are DXCC's, simplified: the catalog's `ham2k-dxcc` adds
18
+ // deleted entities, maritime mobile, satellites and the DXCC Challenge. See
19
+ // `docs/hooks.md` §`chasing`.
20
+
21
+ import { defineExtension, entityForCall } from "@ham2k/extension-sdk"
22
+ import type { ChasingHook, JSONValue, QsoClaim } from "@ham2k/extension-sdk"
23
+ import { parseCallsign } from "@ham2k/lib-callsigns"
24
+ import { ADIF_MODES_AND_SUBMODES, superModeForMode } from "@ham2k/lib-operation-data"
25
+
26
+ import manifest from "../manifest.json" with { type: "json" }
27
+
28
+ /// DXCC's three mode awards. `superModeForMode` calls anything it does not
29
+ /// know `DATA`, so a mode is checked against ADIF's list first.
30
+ const KNOWN_MODES = new Set(ADIF_MODES_AND_SUBMODES.map((mode) => mode.toUpperCase()))
31
+ const AWARD_MODES: Record<string, string> = { CW: "CW", PHONE: "PHONE", DATA: "DIGITAL" }
32
+
33
+ function obj(value: JSONValue | undefined): Record<string, JSONValue> {
34
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {}
35
+ }
36
+
37
+ /// Who the claim belongs to: an opaque string the program builds. DXCC goes
38
+ /// to a call, for contacts made from one entity — KI2D and KI2D/P share a
39
+ /// checklist, KI2D/VE2 keeps its own.
40
+ function holderFor(station: Record<string, JSONValue>): string | undefined {
41
+ if (typeof station.call !== "string" || !station.call) return undefined
42
+ const call = station.call.trim().toUpperCase()
43
+ const base = parseCallsign(call).baseCall || call
44
+ const entity = typeof station.dxccCode === "number" ? station.dxccCode : entityForCall(call).dxccCode
45
+ return entity == null ? base : `${base}@${entity}`
46
+ }
47
+
48
+ /// `worked` from the contact itself, `confirmed` by a QSL the award takes.
49
+ function levelOf(qso: Record<string, JSONValue>): Pick<QsoClaim, "level" | "by"> {
50
+ const qsl = obj(qso.qsl)
51
+ if (obj(qsl.lotw).received === true) return { level: "confirmed", by: "lotw" }
52
+ if (obj(qsl.qsl).received === true) return { level: "confirmed", by: "qsl" }
53
+ return { level: "worked" }
54
+ }
55
+
56
+ function claimsFor(qso: Record<string, JSONValue>): QsoClaim[] {
57
+ // An event marker's calls are placeholders, not stations.
58
+ if (qso.band === "event") return []
59
+ const their = obj(qso.their)
60
+ const holder = holderFor(obj(qso.our))
61
+ if (typeof their.call !== "string" || !holder) return []
62
+ const { dxccCode, entityName } = entityForCall(their.call)
63
+ if (dxccCode == null) return []
64
+
65
+ const base = { holder, ref: String(dxccCode), ...(entityName ? { label: entityName } : {}), ...levelOf(qso) }
66
+ const claims: QsoClaim[] = [{ ...base, band: null, mode: null }]
67
+ if (typeof qso.band === "string" && qso.band) claims.push({ ...base, band: qso.band, mode: null })
68
+ const mode = typeof qso.mode === "string" && KNOWN_MODES.has(qso.mode.toUpperCase()) ? AWARD_MODES[superModeForMode(qso.mode.toUpperCase())] : undefined
69
+ if (mode) claims.push({ ...base, band: null, mode })
70
+ return claims
71
+ }
72
+
73
+ const program: ChasingHook = {
74
+ // Plain data, read without calling anything. Raise `version` whenever the
75
+ // same contacts would claim something different.
76
+ program: {
77
+ label: "DXCC*",
78
+ icon: "earth",
79
+ version: 1,
80
+ editions: "lifetime",
81
+ notices: { newRef: "New DXCC* entity", newBand: "DXCC* on {{band}}", newMode: "DXCC* in {{mode}}" },
82
+ modeLabels: { PHONE: "Phone", DIGITAL: "Digital" },
83
+ },
84
+ // Keyed by each contact's uuid; one without a uuid has no key to answer under.
85
+ claimsForQsos: ({ qsos }) =>
86
+ Object.fromEntries(qsos.filter((qso) => typeof qso.uuid === "string" && qso.uuid).map((qso) => [qso.uuid as string, claimsFor(qso)])),
87
+ holdersFor: ({ station }) => {
88
+ const holder = holderFor(station)
89
+ return holder ? [holder] : []
90
+ },
91
+ // One tally per thing worth counting: entities, from their Mixed lines.
92
+ summarizeClaims: ({ slots }) => {
93
+ const entities = slots.filter((slot) => slot.band === null && slot.mode === null)
94
+ const confirmed = entities.filter((slot) => slot.level !== "worked").length
95
+ return { entities: { key: "entities", icon: "earth", label: `${entities.length} entities`, summary: `${confirmed} confirmed`, total: entities.length } }
96
+ },
97
+ }
98
+
99
+ defineExtension({
100
+ ...manifest,
101
+ onActivation({ registerHook }) {
102
+ registerHook("chasing", { hook: program, key: manifest.key })
103
+ },
104
+ })