@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 +9 -1
- package/docs/distribution.md +7 -5
- package/docs/hooks.md +14 -4
- package/package.json +1 -1
- package/samples/README.md +1 -0
- package/samples/k2hrc-dxcc/build.mjs +6 -0
- package/samples/k2hrc-dxcc/manifest.json +23 -0
- package/samples/k2hrc-dxcc/src/index.ts +104 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 1.12.
|
|
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;
|
package/docs/distribution.md
CHANGED
|
@@ -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
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
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` (
|
|
1666
|
-
|
|
1667
|
-
|
|
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
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,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
|
+
})
|