@ham2k/extension-sdk 0.1.0 → 0.3.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/AGENTS.md +139 -0
- package/LICENSE +21 -0
- package/README.md +19 -8
- package/dist/activityExports.js +12 -6
- package/dist/activityScoring.js +2 -0
- package/dist/dxcc.js +15 -2
- package/dist/index.d.ts +71 -4
- package/dist/index.js +16 -14
- package/dist/modes.js +20 -0
- package/dist/refTransforms.js +34 -0
- package/dist/referenceActivity.js +89 -18
- package/dist/scoring.js +11 -4
- package/dist/segments.js +17 -0
- package/dist/templateContext.js +4 -0
- package/docs/distribution.md +445 -0
- package/docs/forms.md +279 -0
- package/docs/hooks.md +1379 -0
- package/docs/settings.md +524 -0
- package/docs/templates.md +206 -0
- package/package.json +22 -3
- package/samples/README.md +33 -0
- package/samples/k2hrc-cqww/build.mjs +6 -0
- package/samples/k2hrc-cqww/manifest.json +23 -0
- package/samples/k2hrc-cqww/src/index.ts +273 -0
- package/samples/k2hrc-hamqth/build.mjs +6 -0
- package/samples/k2hrc-hamqth/manifest.json +24 -0
- package/samples/k2hrc-hamqth/src/index.ts +202 -0
- package/samples/k2hrc-llota/build.mjs +6 -0
- package/samples/k2hrc-llota/manifest.json +25 -0
- package/samples/k2hrc-llota/src/index.ts +172 -0
- package/samples/k2hrc-radio/build.mjs +6 -0
- package/samples/k2hrc-radio/manifest.json +23 -0
- package/samples/k2hrc-radio/src/index.ts +145 -0
|
@@ -6,9 +6,39 @@ async function hostApi() {
|
|
|
6
6
|
}
|
|
7
7
|
const NEARBY_DELTA = 1.5;
|
|
8
8
|
const MAX_SUGGESTIONS = 30;
|
|
9
|
+
const learnedPrefixes = /* @__PURE__ */ new Map();
|
|
10
|
+
async function learnedReferencePrefix(category, entityPrefix, fallback) {
|
|
11
|
+
const cacheKey = `${category}:${entityPrefix}`;
|
|
12
|
+
const cached = learnedPrefixes.get(cacheKey);
|
|
13
|
+
if (cached) return cached;
|
|
14
|
+
const host = await hostApi();
|
|
15
|
+
let rows = [];
|
|
16
|
+
try {
|
|
17
|
+
rows = await host.dbLookupSelectAll(category, "", entityPrefix);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
host.log(`${category}: prefix lookup failed for ${entityPrefix}: ${e}`);
|
|
20
|
+
}
|
|
21
|
+
const counts = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const row of rows) {
|
|
23
|
+
const prefix = row.key.split("-")[0];
|
|
24
|
+
if (prefix) counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
let best;
|
|
27
|
+
for (const [prefix, count] of counts) {
|
|
28
|
+
if (best === void 0 || count > (counts.get(best) ?? 0)) best = prefix;
|
|
29
|
+
}
|
|
30
|
+
if (best === void 0) return fallback;
|
|
31
|
+
learnedPrefixes.set(cacheKey, best);
|
|
32
|
+
return best;
|
|
33
|
+
}
|
|
9
34
|
function normalize(ref) {
|
|
10
35
|
return (ref ?? "").toUpperCase().trim();
|
|
11
36
|
}
|
|
37
|
+
function looksLikeReference(text, referenceRegex) {
|
|
38
|
+
if (referenceRegex.test(text)) return true;
|
|
39
|
+
if (/\s/.test(text)) return false;
|
|
40
|
+
return /[0-9]/.test(text) || /[-/]/.test(text);
|
|
41
|
+
}
|
|
12
42
|
function refsOfType(container, type) {
|
|
13
43
|
return (container.refs ?? []).filter((r) => r.type === type && r.ref);
|
|
14
44
|
}
|
|
@@ -36,8 +66,22 @@ function referenceActivity(config) {
|
|
|
36
66
|
return null;
|
|
37
67
|
}
|
|
38
68
|
}
|
|
69
|
+
function undecorated(ref, reference, name) {
|
|
70
|
+
return {
|
|
71
|
+
...ref,
|
|
72
|
+
ref: reference,
|
|
73
|
+
name,
|
|
74
|
+
program,
|
|
75
|
+
label: void 0,
|
|
76
|
+
shortLabel: void 0,
|
|
77
|
+
grid: void 0,
|
|
78
|
+
lat: void 0,
|
|
79
|
+
lon: void 0,
|
|
80
|
+
location: void 0
|
|
81
|
+
};
|
|
82
|
+
}
|
|
39
83
|
function decorate(ref, reference, row, unknown) {
|
|
40
|
-
if (!row?.name) return
|
|
84
|
+
if (!row?.name) return undecorated(ref, reference, unknown);
|
|
41
85
|
const data = row.data ?? {};
|
|
42
86
|
return {
|
|
43
87
|
...ref,
|
|
@@ -52,6 +96,16 @@ function referenceActivity(config) {
|
|
|
52
96
|
shortLabel: `${config.label} ${reference}`
|
|
53
97
|
};
|
|
54
98
|
}
|
|
99
|
+
async function refListInput(refType, args, ctx) {
|
|
100
|
+
const extras = config.refInput ? await config.refInput(args, ctx) : {};
|
|
101
|
+
return {
|
|
102
|
+
kind: "refList",
|
|
103
|
+
refType,
|
|
104
|
+
placeholder: extras.placeholder ?? config.placeholder,
|
|
105
|
+
pattern: config.referenceRegex.source,
|
|
106
|
+
transforms: extras.transforms ?? config.transforms
|
|
107
|
+
};
|
|
108
|
+
}
|
|
55
109
|
const refHandler = {
|
|
56
110
|
async validateRef({ ref }, _ctx) {
|
|
57
111
|
const normalized = normalize(ref.ref);
|
|
@@ -61,7 +115,7 @@ function referenceActivity(config) {
|
|
|
61
115
|
const t = config.tFor(ctx);
|
|
62
116
|
const reference = normalize(ref.ref);
|
|
63
117
|
if (!config.referenceRegex.test(reference)) {
|
|
64
|
-
return
|
|
118
|
+
return undecorated(ref, reference, t("invalidReference"));
|
|
65
119
|
}
|
|
66
120
|
return decorate(ref, reference, await lookupReference(reference), t("unknownReference"));
|
|
67
121
|
},
|
|
@@ -97,7 +151,7 @@ function referenceActivity(config) {
|
|
|
97
151
|
}
|
|
98
152
|
};
|
|
99
153
|
const activityHook = {
|
|
100
|
-
async operationControls(
|
|
154
|
+
async operationControls({ operation }, ctx) {
|
|
101
155
|
return [
|
|
102
156
|
{
|
|
103
157
|
key: `${config.key}/activation`,
|
|
@@ -107,12 +161,7 @@ function referenceActivity(config) {
|
|
|
107
161
|
order: 10,
|
|
108
162
|
optionType: "optional",
|
|
109
163
|
allowsMultiple,
|
|
110
|
-
input: {
|
|
111
|
-
kind: "refList",
|
|
112
|
-
refType: config.activationType,
|
|
113
|
-
placeholder: config.placeholder,
|
|
114
|
-
pattern: config.referenceRegex.source
|
|
115
|
-
}
|
|
164
|
+
input: await refListInput(config.activationType, { operation, side: "activation" }, ctx)
|
|
116
165
|
}
|
|
117
166
|
];
|
|
118
167
|
},
|
|
@@ -122,7 +171,7 @@ function referenceActivity(config) {
|
|
|
122
171
|
// on whether the operation is activating — both write the same hunting
|
|
123
172
|
// refs, and POTA's HaLo port already settled on the single control.
|
|
124
173
|
...config.huntingType ? {
|
|
125
|
-
async loggingControls(
|
|
174
|
+
async loggingControls({ operation, qso }, ctx) {
|
|
126
175
|
return [
|
|
127
176
|
{
|
|
128
177
|
key: `${config.key}/hunter`,
|
|
@@ -132,17 +181,12 @@ function referenceActivity(config) {
|
|
|
132
181
|
order: 10,
|
|
133
182
|
optionType: "optional",
|
|
134
183
|
allowsMultiple: true,
|
|
135
|
-
input: {
|
|
136
|
-
kind: "refList",
|
|
137
|
-
refType: config.huntingType,
|
|
138
|
-
placeholder: config.placeholder,
|
|
139
|
-
pattern: config.referenceRegex.source
|
|
140
|
-
}
|
|
184
|
+
input: await refListInput(config.huntingType, { operation, qso, side: "hunting" }, ctx)
|
|
141
185
|
}
|
|
142
186
|
];
|
|
143
187
|
}
|
|
144
188
|
} : {},
|
|
145
|
-
async suggest({ location, searchTerm },
|
|
189
|
+
async suggest({ location, searchTerm, scoped }, ctx) {
|
|
146
190
|
const host = await hostApi();
|
|
147
191
|
const rows = searchTerm ? await host.dbLookupSelectAll(category, searchTerm) : location ? await host.dbLookupSelectByLocation(category, location.lat, location.lon, NEARBY_DELTA) : [];
|
|
148
192
|
const withDistance = rows.map((row) => ({
|
|
@@ -152,7 +196,7 @@ function referenceActivity(config) {
|
|
|
152
196
|
if (!searchTerm) {
|
|
153
197
|
withDistance.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity));
|
|
154
198
|
}
|
|
155
|
-
|
|
199
|
+
const suggestions = withDistance.slice(0, MAX_SUGGESTIONS).map(({ row, distance }) => {
|
|
156
200
|
const data = row.data ?? {};
|
|
157
201
|
return {
|
|
158
202
|
type: config.activationType,
|
|
@@ -170,6 +214,31 @@ function referenceActivity(config) {
|
|
|
170
214
|
allowsMultiple
|
|
171
215
|
};
|
|
172
216
|
});
|
|
217
|
+
const typed = scoped ? normalize(searchTerm) : "";
|
|
218
|
+
if (typed && looksLikeReference(typed, config.referenceRegex) && !suggestions.some((s) => s.ref === typed)) {
|
|
219
|
+
const t = config.tFor(ctx);
|
|
220
|
+
const valid = config.referenceRegex.test(typed);
|
|
221
|
+
const row = valid ? await lookupReference(typed) : null;
|
|
222
|
+
const data = row?.data ?? {};
|
|
223
|
+
const name = row?.name || (valid ? t("unknownReference") : t("invalidReference"));
|
|
224
|
+
if (suggestions.length >= MAX_SUGGESTIONS) suggestions.pop();
|
|
225
|
+
suggestions.unshift({
|
|
226
|
+
type: config.activationType,
|
|
227
|
+
ref: typed,
|
|
228
|
+
name,
|
|
229
|
+
grid: data.grid,
|
|
230
|
+
lat: row?.lat,
|
|
231
|
+
lon: row?.lon,
|
|
232
|
+
location: data.location,
|
|
233
|
+
program,
|
|
234
|
+
label: `${config.label} ${typed}: ${name}`,
|
|
235
|
+
shortLabel: `${config.label} ${typed}`,
|
|
236
|
+
distance: void 0,
|
|
237
|
+
relevance: 1,
|
|
238
|
+
allowsMultiple
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return suggestions;
|
|
173
242
|
}
|
|
174
243
|
};
|
|
175
244
|
function activationFields(operation) {
|
|
@@ -222,5 +291,7 @@ function referenceActivity(config) {
|
|
|
222
291
|
return { refHandler, activityHook, adifFieldsHook, adifImportHook };
|
|
223
292
|
}
|
|
224
293
|
export {
|
|
294
|
+
learnedReferencePrefix,
|
|
295
|
+
looksLikeReference,
|
|
225
296
|
referenceActivity
|
|
226
297
|
};
|
package/dist/scoring.js
CHANGED
|
@@ -16,6 +16,9 @@ function startAtMillisOf(qso) {
|
|
|
16
16
|
const at = qso.startAtMillis;
|
|
17
17
|
return typeof at === "number" ? at : 0;
|
|
18
18
|
}
|
|
19
|
+
function candidateAsOfNow(qso) {
|
|
20
|
+
return typeof qso.startAtMillis === "number" ? qso : { ...qso, startAtMillis: Date.now() };
|
|
21
|
+
}
|
|
19
22
|
function contestScorer(scorer, options = {}) {
|
|
20
23
|
const scope = options.scope ?? "always";
|
|
21
24
|
const scopedRefTypes = typeof scope === "object" && Array.isArray(scope.refTypes) ? scope.refTypes : void 0;
|
|
@@ -118,10 +121,10 @@ function contestScorer(scorer, options = {}) {
|
|
|
118
121
|
async scoreQso(args, ctx) {
|
|
119
122
|
const sheet = await liveScoresheet(args, ctx, args.excludeUuid);
|
|
120
123
|
const { score } = scorer.scoreQso(
|
|
121
|
-
{ scoresheet: sheet, qso: args.qso, operation: args.operation, ref: args.ref, isNewDay: false },
|
|
124
|
+
{ scoresheet: sheet, qso: candidateAsOfNow(args.qso), operation: args.operation, ref: args.ref, isNewDay: false },
|
|
122
125
|
ctx
|
|
123
126
|
);
|
|
124
|
-
return { notices: score.notices ?? [], alerts: score.alerts ?? [] };
|
|
127
|
+
return { notices: score.notices ?? [], alerts: score.alerts ?? [], ...score.dupe !== void 0 && { dupe: score.dupe } };
|
|
125
128
|
},
|
|
126
129
|
/// The same live judgement as `scoreQso`, for many candidates at once —
|
|
127
130
|
/// the Spots Panel annotating every spot on screen. The log is folded ONCE
|
|
@@ -141,14 +144,18 @@ function contestScorer(scorer, options = {}) {
|
|
|
141
144
|
const { score } = scorer.scoreQso(
|
|
142
145
|
{
|
|
143
146
|
scoresheet: copyCheckpoint(base),
|
|
144
|
-
qso: candidate.qso,
|
|
147
|
+
qso: candidateAsOfNow(candidate.qso),
|
|
145
148
|
operation: args.operation,
|
|
146
149
|
ref: args.ref,
|
|
147
150
|
isNewDay: false
|
|
148
151
|
},
|
|
149
152
|
ctx
|
|
150
153
|
);
|
|
151
|
-
result[candidate.key] = {
|
|
154
|
+
result[candidate.key] = {
|
|
155
|
+
notices: score.notices ?? [],
|
|
156
|
+
alerts: score.alerts ?? [],
|
|
157
|
+
...score.dupe !== void 0 && { dupe: score.dupe }
|
|
158
|
+
};
|
|
152
159
|
}
|
|
153
160
|
return result;
|
|
154
161
|
}
|
package/dist/segments.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function operationForQso(base, qso, segments) {
|
|
2
|
+
if (!segments || segments.length === 0) return base;
|
|
3
|
+
const at = typeof qso.startAtMillis === "number" ? qso.startAtMillis : 0;
|
|
4
|
+
let effective = base;
|
|
5
|
+
for (const segment of segments) {
|
|
6
|
+
if (segment.fromMillis > at) break;
|
|
7
|
+
effective = segment.operation;
|
|
8
|
+
}
|
|
9
|
+
return effective;
|
|
10
|
+
}
|
|
11
|
+
function segmentsWith(segments, transform) {
|
|
12
|
+
return segments?.map((segment) => ({ ...segment, operation: transform(segment.operation) }));
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
operationForQso,
|
|
16
|
+
segmentsWith
|
|
17
|
+
};
|
package/dist/templateContext.js
CHANGED
|
@@ -26,6 +26,10 @@ function opValues(operation, extra = {}) {
|
|
|
26
26
|
return {
|
|
27
27
|
...atMillis > 0 ? dateValues(atMillis) : { date: "", dateCompact: "", time: "", at: "" },
|
|
28
28
|
station,
|
|
29
|
+
// `call` says the same thing, for symmetry with `qso.call` — the name an
|
|
30
|
+
// operator writing a CW message reaches for first; without it,
|
|
31
|
+
// `{{ op.call }}` renders empty with nothing to say why.
|
|
32
|
+
call: station,
|
|
29
33
|
// Deduped, like halo_core's `Operation.stationCalls`: "N0DEV, N0DEV" is a
|
|
30
34
|
// typo, and a template fanning out over this would otherwise repeat
|
|
31
35
|
// itself once per typo.
|