@ham2k/extension-sdk 0.2.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/LICENSE +21 -0
- package/README.md +0 -4
- 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 +168 -2
- package/docs/forms.md +10 -2
- package/docs/hooks.md +120 -23
- package/docs/settings.md +12 -11
- package/docs/templates.md +11 -9
- package/package.json +11 -2
- package/samples/k2hrc-cqww/manifest.json +0 -1
- package/samples/k2hrc-cqww/src/index.ts +1 -1
- package/samples/k2hrc-hamqth/manifest.json +0 -1
- package/samples/k2hrc-hamqth/src/index.ts +1 -1
- package/samples/k2hrc-llota/manifest.json +1 -1
- package/samples/k2hrc-llota/src/index.ts +1 -1
- package/samples/k2hrc-radio/manifest.json +0 -1
- package/samples/k2hrc-radio/src/index.ts +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Delmont <sd@ham2k.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
package/dist/activityExports.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { exportFilename, startMillisOf } from "./exportNames.js";
|
|
2
|
+
import { segmentsWith } from "./segments.js";
|
|
2
3
|
import { createCachedTranslator } from "./i18n.js";
|
|
3
4
|
import en from "./i18n/activityExports.en.json";
|
|
4
5
|
import es from "./i18n/activityExports.es.json";
|
|
@@ -50,13 +51,16 @@ function activityExportHook(rules) {
|
|
|
50
51
|
if (!refsOfType(args.operation, rules.activationType).some((r) => r.ref === wanted)) {
|
|
51
52
|
throw new Error(`${rules.key}: operation has no ${rules.activationType} reference '${wanted}'`);
|
|
52
53
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const claimOnly = (operation) => ({
|
|
55
|
+
...operation,
|
|
56
|
+
refs: (operation.refs ?? []).filter(
|
|
57
|
+
(r) => r?.type !== rules.activationType || r?.ref === wanted
|
|
58
|
+
)
|
|
59
|
+
});
|
|
57
60
|
const content = await adifForExport({
|
|
58
|
-
operation,
|
|
61
|
+
operation: claimOnly(args.operation),
|
|
59
62
|
qsos: args.qsos,
|
|
63
|
+
segments: segmentsWith(args.segments, claimOnly),
|
|
60
64
|
includePrivateData: args.includePrivateData,
|
|
61
65
|
mainHandler: rules.key,
|
|
62
66
|
includeFieldsFrom: rules.includeFieldsFrom
|
|
@@ -113,6 +117,7 @@ function huntingExportHook(rules) {
|
|
|
113
117
|
const content = await adifForExport({
|
|
114
118
|
operation: args.operation,
|
|
115
119
|
qsos: huntedQsos(args.qsos ?? []),
|
|
120
|
+
segments: args.segments,
|
|
116
121
|
includePrivateData: args.includePrivateData,
|
|
117
122
|
// The hunter file is submitted to ONE program too — see
|
|
118
123
|
// `activityExportHook.generateExport`. `rules.key` is the ACTIVITY's
|
|
@@ -131,11 +136,12 @@ function huntingExportHook(rules) {
|
|
|
131
136
|
}
|
|
132
137
|
};
|
|
133
138
|
}
|
|
134
|
-
async function adifForExport({ operation, qsos, includePrivateData, mainHandler, includeFieldsFrom }) {
|
|
139
|
+
async function adifForExport({ operation, qsos, segments, includePrivateData, mainHandler, includeFieldsFrom }) {
|
|
135
140
|
const { hooks } = await import("@ham2k/extension-sdk");
|
|
136
141
|
const entries = await hooks.invokeOne("export", "adif", "generateExport", {
|
|
137
142
|
operation,
|
|
138
143
|
qsos,
|
|
144
|
+
segments,
|
|
139
145
|
exportType: "adif",
|
|
140
146
|
// Forwarded, or the delegate defaults to withholding and the user's
|
|
141
147
|
// "include private info" setting silently does nothing here.
|
package/dist/activityScoring.js
CHANGED
|
@@ -127,6 +127,8 @@ function activityScorer(rules) {
|
|
|
127
127
|
score.alerts = ["duplicate"];
|
|
128
128
|
} else if (verdict) {
|
|
129
129
|
score.notices = [NOTICE_KEYS[verdict]];
|
|
130
|
+
} else if (priors.length > 0 && score.value > 0 && freshRefRescuesActivation) {
|
|
131
|
+
score.dupe = false;
|
|
130
132
|
}
|
|
131
133
|
const packed = packContact(day, band, mode, huntedRefs);
|
|
132
134
|
if (priors.length > 0) priors.push(packed);
|
package/dist/dxcc.js
CHANGED
|
@@ -2,8 +2,8 @@ import { parseCallsign } from "@ham2k/lib-callsigns";
|
|
|
2
2
|
import { annotateFromCountryFile, useBuiltinCountryFile } from "@ham2k/lib-country-files";
|
|
3
3
|
import { DXCC_BY_CODE } from "@ham2k/lib-dxcc-data";
|
|
4
4
|
useBuiltinCountryFile();
|
|
5
|
-
function annotateCallAgainstCountryFile(call) {
|
|
6
|
-
return annotateFromCountryFile(parseCallsign(call));
|
|
5
|
+
function annotateCallAgainstCountryFile(call, options) {
|
|
6
|
+
return annotateFromCountryFile(parseCallsign(call), options);
|
|
7
7
|
}
|
|
8
8
|
function entityPrefixForCall(call) {
|
|
9
9
|
if (!call) return void 0;
|
|
@@ -17,8 +17,21 @@ function regionAndCountryForDxccCode(dxccCode) {
|
|
|
17
17
|
const entity = DXCC_BY_CODE[dxccCode];
|
|
18
18
|
return { ituRegion: entity?.ituRegion, countryCode: entity?.countryCode };
|
|
19
19
|
}
|
|
20
|
+
function countryPrefixForCall(call) {
|
|
21
|
+
if (!call) return void 0;
|
|
22
|
+
const annotated = annotateCallAgainstCountryFile(call);
|
|
23
|
+
const { countryCode } = regionAndCountryForDxccCode(annotated.dxccCode);
|
|
24
|
+
return countryCode ? countryCode.toUpperCase() : annotated.entityPrefix;
|
|
25
|
+
}
|
|
26
|
+
function entityForCall(call) {
|
|
27
|
+
if (!call) return {};
|
|
28
|
+
const { dxccCode, continent, entityName } = annotateCallAgainstCountryFile(call);
|
|
29
|
+
return { dxccCode, continent, entityName };
|
|
30
|
+
}
|
|
20
31
|
export {
|
|
21
32
|
annotateCallAgainstCountryFile,
|
|
33
|
+
countryPrefixForCall,
|
|
34
|
+
entityForCall,
|
|
22
35
|
entityPrefixForCall,
|
|
23
36
|
flagForDxccCode,
|
|
24
37
|
regionAndCountryForDxccCode
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.
|
|
1
|
+
// @ham2k/extension-sdk 0.3.0
|
|
2
2
|
export type CallInfo = {
|
|
3
3
|
call: string;
|
|
4
4
|
baseCall?: string;
|
|
@@ -103,8 +103,14 @@ export interface HookContext {
|
|
|
103
103
|
distanceUnits?: string;
|
|
104
104
|
decimalSeparator?: string;
|
|
105
105
|
groupingSeparator?: string;
|
|
106
|
+
gridPrecision?: number;
|
|
106
107
|
edition?: string;
|
|
107
108
|
enabledExperiments?: string[];
|
|
109
|
+
experiments?: {
|
|
110
|
+
key: string;
|
|
111
|
+
name: string;
|
|
112
|
+
aliases: string[];
|
|
113
|
+
}[];
|
|
108
114
|
getOperation?(uuid: string): Promise<Record<string, JSONValue> | null>;
|
|
109
115
|
getQsos?(operationUuid: string): Promise<Record<string, JSONValue>[] | null>;
|
|
110
116
|
getSpotsForCall?(call: string, maxAgeMinutes?: number): Promise<Spot[]>;
|
|
@@ -275,6 +281,7 @@ export interface SerialInputDescriptor {
|
|
|
275
281
|
export interface FormInputDescriptor {
|
|
276
282
|
kind: "form";
|
|
277
283
|
refType: string;
|
|
284
|
+
alsoHandles?: string[];
|
|
278
285
|
form: FormDefinition;
|
|
279
286
|
}
|
|
280
287
|
export interface TextInputDescriptor {
|
|
@@ -304,6 +311,10 @@ export interface OptionsInputDescriptor {
|
|
|
304
311
|
matchOn?: ("code" | "name")[];
|
|
305
312
|
allowFreeform?: boolean;
|
|
306
313
|
preferredCodes?: string[];
|
|
314
|
+
multiValue?: {
|
|
315
|
+
separator: string;
|
|
316
|
+
inheritPrefix?: number;
|
|
317
|
+
};
|
|
307
318
|
minCharsForSuggestions?: number;
|
|
308
319
|
placeholder?: string;
|
|
309
320
|
uppercase?: boolean;
|
|
@@ -380,6 +391,7 @@ export interface AdifImportHook {
|
|
|
380
391
|
export interface ExportRequest {
|
|
381
392
|
operation: Record<string, JSONValue>;
|
|
382
393
|
qsos: Record<string, JSONValue>[];
|
|
394
|
+
segments?: OperationSegmentPayload[];
|
|
383
395
|
exportType?: string;
|
|
384
396
|
compactFilenames?: boolean;
|
|
385
397
|
includePrivateData?: boolean;
|
|
@@ -464,6 +476,7 @@ export interface ScoreQsoRequest {
|
|
|
464
476
|
export interface QsoScoreNotices {
|
|
465
477
|
notices: string[];
|
|
466
478
|
alerts: string[];
|
|
479
|
+
dupe?: boolean;
|
|
467
480
|
}
|
|
468
481
|
export interface ScoreCandidatesRequest {
|
|
469
482
|
operation: Record<string, JSONValue>;
|
|
@@ -517,6 +530,10 @@ export interface CommandAction {
|
|
|
517
530
|
setPower?: {
|
|
518
531
|
watts: number;
|
|
519
532
|
};
|
|
533
|
+
setKeyerSpeed?: {
|
|
534
|
+
wpm: number;
|
|
535
|
+
};
|
|
536
|
+
showKeyer?: Record<string, never>;
|
|
520
537
|
updateQso?: Record<string, JSONValue>;
|
|
521
538
|
addQso?: Record<string, JSONValue>;
|
|
522
539
|
sendSpots?: {
|
|
@@ -540,6 +557,10 @@ export interface CommandAction {
|
|
|
540
557
|
operatorCall?: string;
|
|
541
558
|
multiIdentifier?: string;
|
|
542
559
|
};
|
|
560
|
+
markSegment?: {
|
|
561
|
+
kind: "start" | "break" | "stop";
|
|
562
|
+
setLocation?: boolean;
|
|
563
|
+
};
|
|
543
564
|
setCallField?: {
|
|
544
565
|
text: string;
|
|
545
566
|
};
|
|
@@ -575,6 +596,9 @@ export interface CommandAction {
|
|
|
575
596
|
deltaMillis?: number;
|
|
576
597
|
speed?: number;
|
|
577
598
|
};
|
|
599
|
+
devBench?: {
|
|
600
|
+
qsoCount?: number;
|
|
601
|
+
};
|
|
578
602
|
toggleExperiment?: {
|
|
579
603
|
token: string;
|
|
580
604
|
};
|
|
@@ -723,6 +747,14 @@ export interface ActivationApi {
|
|
|
723
747
|
registerHook(category: string, params: RegisterHookParams): void;
|
|
724
748
|
hostCall(method: string, params: Record<string, unknown>): Promise<unknown>;
|
|
725
749
|
}
|
|
750
|
+
export interface ExtensionRelevance {
|
|
751
|
+
entities?: string[];
|
|
752
|
+
countries?: string[];
|
|
753
|
+
continents?: string[];
|
|
754
|
+
ituRegions?: number[];
|
|
755
|
+
dates?: string[];
|
|
756
|
+
interests?: string[];
|
|
757
|
+
}
|
|
726
758
|
export interface ExtensionManifest {
|
|
727
759
|
key: string;
|
|
728
760
|
name: string;
|
|
@@ -739,6 +771,7 @@ export interface ExtensionManifest {
|
|
|
739
771
|
allowUserAgentOverride?: boolean;
|
|
740
772
|
requiresLocation?: boolean;
|
|
741
773
|
experiments?: string[];
|
|
774
|
+
relevance?: ExtensionRelevance;
|
|
742
775
|
translations?: Record<string, {
|
|
743
776
|
name?: string;
|
|
744
777
|
description?: string;
|
|
@@ -883,7 +916,7 @@ export interface FormHeader {
|
|
|
883
916
|
title: string;
|
|
884
917
|
subtitle?: string;
|
|
885
918
|
devMode?: boolean;
|
|
886
|
-
style?: "default" | "section";
|
|
919
|
+
style?: "default" | "section" | "category";
|
|
887
920
|
environment?: string | string[];
|
|
888
921
|
}
|
|
889
922
|
export interface FormSectionDivider {
|
|
@@ -1129,13 +1162,17 @@ type AnnotatedCallInfo$1 = CombinedCallInfo & {
|
|
|
1129
1162
|
originalValues?: AnnotatedCallInfo$1;
|
|
1130
1163
|
cfValues?: AnnotatedCallInfo$1;
|
|
1131
1164
|
};
|
|
1132
|
-
export declare function annotateCallAgainstCountryFile(call: string
|
|
1165
|
+
export declare function annotateCallAgainstCountryFile(call: string, options?: {
|
|
1166
|
+
wae?: boolean;
|
|
1167
|
+
}): AnnotatedCallInfo$1;
|
|
1133
1168
|
export declare function entityPrefixForCall(call: string | undefined): string | undefined;
|
|
1134
1169
|
export declare function flagForDxccCode(dxccCode: number | undefined): string | undefined;
|
|
1135
1170
|
export declare function regionAndCountryForDxccCode(dxccCode: number | undefined): {
|
|
1136
1171
|
ituRegion?: number;
|
|
1137
1172
|
countryCode?: string;
|
|
1138
1173
|
};
|
|
1174
|
+
export declare function countryPrefixForCall(call: string | undefined): string | undefined;
|
|
1175
|
+
export declare function entityForCall(call: string | undefined): Pick<AnnotatedCallInfo$1, "dxccCode" | "continent" | "entityName">;
|
|
1139
1176
|
export declare const LOCATION_ACCURACY: {
|
|
1140
1177
|
readonly EXACT: 0;
|
|
1141
1178
|
readonly ACCURATE: 2;
|
|
@@ -1149,6 +1186,14 @@ export declare function applyLatLonFromApiFields(target: {
|
|
|
1149
1186
|
}, raw: Record<string, unknown>): void;
|
|
1150
1187
|
export declare function canonicalMode(mode: string): string;
|
|
1151
1188
|
export declare function modesMatchForDupe(worked: string, candidate: string): boolean;
|
|
1189
|
+
export declare function expectedSideband({ freqKHz, band }: {
|
|
1190
|
+
freqKHz?: number;
|
|
1191
|
+
band?: string;
|
|
1192
|
+
}): string | undefined;
|
|
1193
|
+
export declare function isUnexpectedSideband(mode: string | undefined | null, { freqKHz, band }: {
|
|
1194
|
+
freqKHz?: number;
|
|
1195
|
+
band?: string;
|
|
1196
|
+
}): boolean;
|
|
1152
1197
|
export interface ContestScorerOptions {
|
|
1153
1198
|
scope?: ScoringScope;
|
|
1154
1199
|
}
|
|
@@ -1156,6 +1201,8 @@ export declare function contestScorer<S extends Scoresheet = Scoresheet>(scorer:
|
|
|
1156
1201
|
scope: ScoringScope;
|
|
1157
1202
|
};
|
|
1158
1203
|
export declare function tally(key: string, forScope: "day" | "operation", total: number, extra?: Partial<ScoreTally>): ScoreTally;
|
|
1204
|
+
export declare function operationForQso(base: Record<string, JSONValue>, qso: Record<string, JSONValue>, segments?: OperationSegmentPayload[]): Record<string, JSONValue>;
|
|
1205
|
+
export declare function segmentsWith(segments: OperationSegmentPayload[] | undefined, transform: (operation: Record<string, JSONValue>) => Record<string, JSONValue>): OperationSegmentPayload[] | undefined;
|
|
1159
1206
|
export type UniqueAxis = "day" | "band" | "mode" | "ref";
|
|
1160
1207
|
export interface ActivityScoringRules {
|
|
1161
1208
|
label: string;
|
|
@@ -1210,9 +1257,10 @@ export declare function huntingExportHook(rules: HuntingExportRules): {
|
|
|
1210
1257
|
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
1211
1258
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
1212
1259
|
};
|
|
1213
|
-
export declare function adifForExport({ operation, qsos, includePrivateData, mainHandler, includeFieldsFrom }: {
|
|
1260
|
+
export declare function adifForExport({ operation, qsos, segments, includePrivateData, mainHandler, includeFieldsFrom }: {
|
|
1214
1261
|
operation: Record<string, JSONValue>;
|
|
1215
1262
|
qsos: Record<string, JSONValue>[];
|
|
1263
|
+
segments?: OperationSegmentPayload[];
|
|
1216
1264
|
includePrivateData?: boolean;
|
|
1217
1265
|
mainHandler: string;
|
|
1218
1266
|
includeFieldsFrom?: string[];
|
|
@@ -1236,6 +1284,12 @@ export interface ExportNameParts {
|
|
|
1236
1284
|
}
|
|
1237
1285
|
export declare function exportFilename(parts: ExportNameParts): string;
|
|
1238
1286
|
export declare function startMillisOf(operation: Record<string, JSONValue>, qsos?: Record<string, JSONValue>[]): number | undefined;
|
|
1287
|
+
export type RefTransform = {
|
|
1288
|
+
pattern: string;
|
|
1289
|
+
replacement: string;
|
|
1290
|
+
flags?: string;
|
|
1291
|
+
};
|
|
1292
|
+
export declare function transformsForPrefix(prefix: string): RefTransform[];
|
|
1239
1293
|
export interface ReferenceActivityConfig {
|
|
1240
1294
|
key: string;
|
|
1241
1295
|
label: string;
|
|
@@ -1254,7 +1308,20 @@ export interface ReferenceActivityConfig {
|
|
|
1254
1308
|
adifRefField?: string;
|
|
1255
1309
|
allowsMultiple?: boolean;
|
|
1256
1310
|
linkUrl?: (reference: string) => string;
|
|
1311
|
+
transforms?: RefTransform[];
|
|
1312
|
+
refInput?: (args: RefInputArgs, ctx: HookContext) => Promise<RefInputExtras> | RefInputExtras;
|
|
1313
|
+
}
|
|
1314
|
+
export interface RefInputArgs {
|
|
1315
|
+
operation: Record<string, JSONValue>;
|
|
1316
|
+
qso?: Record<string, JSONValue>;
|
|
1317
|
+
side: "activation" | "hunting";
|
|
1318
|
+
}
|
|
1319
|
+
export interface RefInputExtras {
|
|
1320
|
+
placeholder?: string;
|
|
1321
|
+
transforms?: RefTransform[];
|
|
1257
1322
|
}
|
|
1323
|
+
export declare function learnedReferencePrefix(category: string, entityPrefix: string, fallback: string): Promise<string>;
|
|
1324
|
+
export declare function looksLikeReference(text: string, referenceRegex: RegExp): boolean;
|
|
1258
1325
|
export interface ReferenceActivityHooks {
|
|
1259
1326
|
refHandler: Record<string, unknown>;
|
|
1260
1327
|
activityHook: Record<string, unknown>;
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
export * from "./types";
|
|
2
|
-
export * from "./i18n";
|
|
3
|
-
export * from "./dxcc";
|
|
4
|
-
export * from "./location";
|
|
5
|
-
export * from "./modes";
|
|
6
|
-
export * from "./scoring";
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./
|
|
9
|
-
export * from "./
|
|
10
|
-
export * from "./
|
|
11
|
-
export * from "./
|
|
12
|
-
export * from "./
|
|
13
|
-
export * from "./
|
|
14
|
-
export * from "./
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./i18n.js";
|
|
3
|
+
export * from "./dxcc.js";
|
|
4
|
+
export * from "./location.js";
|
|
5
|
+
export * from "./modes.js";
|
|
6
|
+
export * from "./scoring.js";
|
|
7
|
+
export * from "./segments.js";
|
|
8
|
+
export * from "./activityScoring.js";
|
|
9
|
+
export * from "./activityExports.js";
|
|
10
|
+
export * from "./activityAdifImport.js";
|
|
11
|
+
export * from "./exportNames.js";
|
|
12
|
+
export * from "./referenceActivity.js";
|
|
13
|
+
export * from "./refTransforms.js";
|
|
14
|
+
export * from "./templates.js";
|
|
15
|
+
export * from "./templateContext.js";
|
|
16
|
+
export * from "./testOperation.js";
|
|
15
17
|
let currentExtensionKey = "unknown";
|
|
16
18
|
const privilegedHostCalls = {};
|
|
17
19
|
function kernel() {
|
package/dist/modes.js
CHANGED
|
@@ -22,7 +22,27 @@ function modesMatchForDupe(worked, candidate) {
|
|
|
22
22
|
const superMode = COARSE_SUPER_MODES[c];
|
|
23
23
|
return Boolean(superMode) && KNOWN_MODES.has(w) && superModeForMode(w) === superMode;
|
|
24
24
|
}
|
|
25
|
+
const LSB_BANDS = /* @__PURE__ */ new Set(["2190m", "2200m", "630m", "560m", "160m", "80m", "75m", "40m"]);
|
|
26
|
+
const UPPER_SIDEBAND_FLOOR_KHZ = 9e3;
|
|
27
|
+
const SIXTY_METRE_LO_KHZ = 5e3;
|
|
28
|
+
const SIXTY_METRE_HI_KHZ = 6e3;
|
|
29
|
+
function expectedSideband({ freqKHz, band }) {
|
|
30
|
+
if (freqKHz != null) {
|
|
31
|
+
if (freqKHz >= UPPER_SIDEBAND_FLOOR_KHZ) return "USB";
|
|
32
|
+
return freqKHz >= SIXTY_METRE_LO_KHZ && freqKHz < SIXTY_METRE_HI_KHZ ? "USB" : "LSB";
|
|
33
|
+
}
|
|
34
|
+
if (!band) return void 0;
|
|
35
|
+
return LSB_BANDS.has(band) ? "LSB" : "USB";
|
|
36
|
+
}
|
|
37
|
+
function isUnexpectedSideband(mode, { freqKHz, band }) {
|
|
38
|
+
const upper = (mode ?? "").toUpperCase();
|
|
39
|
+
if (upper !== "USB" && upper !== "LSB") return false;
|
|
40
|
+
const expected = expectedSideband({ freqKHz, band });
|
|
41
|
+
return expected !== void 0 && upper !== expected;
|
|
42
|
+
}
|
|
25
43
|
export {
|
|
26
44
|
canonicalMode,
|
|
45
|
+
expectedSideband,
|
|
46
|
+
isUnexpectedSideband,
|
|
27
47
|
modesMatchForDupe
|
|
28
48
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function transformsForPrefix(prefix) {
|
|
2
|
+
return [
|
|
3
|
+
// a run of spaces between two refs -> ", " (normalize to one separator style)
|
|
4
|
+
{ pattern: "([A-Z0-9]-\\d+|TEST) +(?=[A-Z0-9])", replacement: "${1}, ", flags: "gi" },
|
|
5
|
+
// bare number (or "TEST"), at the start or right after a separator -> prefix-number
|
|
6
|
+
// (the separator itself is captured and replayed, not consumed, so a
|
|
7
|
+
// pasted "US-1234,5678" keeps its comma instead of merging into one ref).
|
|
8
|
+
// Braced `${1}`/`${2}`, not bare `$1`/`$2` — `prefix` can itself start
|
|
9
|
+
// with a digit (e.g. DXCC "3D2" for Fiji), and a bare "$1" immediately
|
|
10
|
+
// followed by that would misparse as capture group 13.
|
|
11
|
+
{ pattern: "(^|,\\s*)(\\d\\d+|TEST)", replacement: `\${1}${prefix}-\${2}`, flags: "gi" },
|
|
12
|
+
// "US1234" -> "US-1234". Anchored with a negative lookbehind so it only
|
|
13
|
+
// fires at the START of a not-yet-dashed reference — without it, a
|
|
14
|
+
// multi-character DXCC prefix that mixes letters and digits (e.g.
|
|
15
|
+
// Fiji's "3D2") gets its OWN internal "D2" mismatched as an undashed
|
|
16
|
+
// reference once transform 2 above has already inserted "3D2-1234",
|
|
17
|
+
// corrupting it into "3D-2-1234".
|
|
18
|
+
{ pattern: "(?<![A-Z0-9])([A-Z]+)(\\d+|TEST)", replacement: "${1}-${2}", flags: "gi" },
|
|
19
|
+
// NOTE: deliberately no "eagerly insert the next prefix right after a
|
|
20
|
+
// trailing comma" rule here (app-polo's inputs have one) — transform 2
|
|
21
|
+
// above already prefixes a second reference once 2+ digits appear after
|
|
22
|
+
// the comma, the same way it does at the start of the field. An eager
|
|
23
|
+
// version that inserted "US-" the instant a trailing comma appeared
|
|
24
|
+
// makes deleting back through a second reference impossible: backspacing
|
|
25
|
+
// "US-1234,US-5678" down to "US-1234,US-" and then to "US-1234," would
|
|
26
|
+
// immediately re-grow the trailing "US-1234,US-" the transform had just
|
|
27
|
+
// reinserted, so the comma itself could never be reached.
|
|
28
|
+
// strip anything that can't appear in a reference list
|
|
29
|
+
{ pattern: "[^A-Z0-9\\-, ]", replacement: "", flags: "gi" }
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
transformsForPrefix
|
|
34
|
+
};
|
|
@@ -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
|
+
};
|