@ham2k/extension-sdk 0.2.0 → 0.4.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 +20 -4
- package/dist/activityExports.js +48 -11
- package/dist/activityScoring.js +2 -0
- package/dist/dxcc.js +15 -2
- package/dist/exportSettings.js +133 -0
- package/dist/index.d.ts +133 -5
- package/dist/index.js +17 -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 +36 -0
- package/docs/hooks.md +193 -28
- package/docs/settings.md +11 -9
- package/docs/templates.md +27 -25
- 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/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.
|
|
1
|
+
// @ham2k/extension-sdk 0.4.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;
|
|
@@ -377,10 +388,44 @@ export interface AdifImportHook {
|
|
|
377
388
|
}[];
|
|
378
389
|
}, ctx: HookContext): Promise<(AdifImportResult | null)[]>;
|
|
379
390
|
}
|
|
391
|
+
export interface ExportSettings {
|
|
392
|
+
includePrivateData?: boolean;
|
|
393
|
+
includeLookupData?: boolean;
|
|
394
|
+
customTemplates?: boolean;
|
|
395
|
+
filenameTemplate?: string;
|
|
396
|
+
compactFilenameTemplate?: string;
|
|
397
|
+
titleTemplate?: string;
|
|
398
|
+
adifNotesTemplate?: string;
|
|
399
|
+
adifCommentTemplate?: string;
|
|
400
|
+
adifQslMessageTemplate?: string;
|
|
401
|
+
}
|
|
402
|
+
export interface GlobalExportSettings extends ExportSettings {
|
|
403
|
+
referenceFilenameTemplate?: string;
|
|
404
|
+
referenceCompactFilenameTemplate?: string;
|
|
405
|
+
referenceTitleTemplate?: string;
|
|
406
|
+
otherFilenameTemplate?: string;
|
|
407
|
+
otherCompactFilenameTemplate?: string;
|
|
408
|
+
otherTitleTemplate?: string;
|
|
409
|
+
}
|
|
410
|
+
export interface ExportTypeDefinition {
|
|
411
|
+
exportType: string;
|
|
412
|
+
label: string;
|
|
413
|
+
format: string;
|
|
414
|
+
activationType?: string;
|
|
415
|
+
templateCategory?: "reference" | "other";
|
|
416
|
+
defaults?: ExportSettings;
|
|
417
|
+
templateSample?: Record<string, JSONValue>;
|
|
418
|
+
}
|
|
380
419
|
export interface ExportRequest {
|
|
420
|
+
exportSettings?: ExportSettings;
|
|
421
|
+
exportData?: Record<string, JSONValue>;
|
|
422
|
+
exportTitle?: string;
|
|
423
|
+
includeLookupData?: boolean;
|
|
381
424
|
operation: Record<string, JSONValue>;
|
|
382
425
|
qsos: Record<string, JSONValue>[];
|
|
426
|
+
segments?: OperationSegmentPayload[];
|
|
383
427
|
exportType?: string;
|
|
428
|
+
exportKey?: string;
|
|
384
429
|
compactFilenames?: boolean;
|
|
385
430
|
includePrivateData?: boolean;
|
|
386
431
|
mainHandler?: string;
|
|
@@ -397,7 +442,10 @@ export interface ExportOptionsRequest {
|
|
|
397
442
|
compactFilenames?: boolean;
|
|
398
443
|
}
|
|
399
444
|
export interface ExportOption {
|
|
445
|
+
qsoCount?: number;
|
|
446
|
+
templateData?: Record<string, JSONValue>;
|
|
400
447
|
exportType: string;
|
|
448
|
+
exportKey?: string;
|
|
401
449
|
format: string;
|
|
402
450
|
label: string;
|
|
403
451
|
filename?: string;
|
|
@@ -409,6 +457,7 @@ export interface ExportOption {
|
|
|
409
457
|
refType?: string;
|
|
410
458
|
}
|
|
411
459
|
export interface ExportHook {
|
|
460
|
+
getExportTypes?(args: Record<string, never>, ctx: HookContext): Promise<ExportTypeDefinition[]>;
|
|
412
461
|
suggestExportOptions?(args: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
413
462
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
414
463
|
}
|
|
@@ -464,6 +513,7 @@ export interface ScoreQsoRequest {
|
|
|
464
513
|
export interface QsoScoreNotices {
|
|
465
514
|
notices: string[];
|
|
466
515
|
alerts: string[];
|
|
516
|
+
dupe?: boolean;
|
|
467
517
|
}
|
|
468
518
|
export interface ScoreCandidatesRequest {
|
|
469
519
|
operation: Record<string, JSONValue>;
|
|
@@ -517,6 +567,10 @@ export interface CommandAction {
|
|
|
517
567
|
setPower?: {
|
|
518
568
|
watts: number;
|
|
519
569
|
};
|
|
570
|
+
setKeyerSpeed?: {
|
|
571
|
+
wpm: number;
|
|
572
|
+
};
|
|
573
|
+
showKeyer?: Record<string, never>;
|
|
520
574
|
updateQso?: Record<string, JSONValue>;
|
|
521
575
|
addQso?: Record<string, JSONValue>;
|
|
522
576
|
sendSpots?: {
|
|
@@ -540,6 +594,10 @@ export interface CommandAction {
|
|
|
540
594
|
operatorCall?: string;
|
|
541
595
|
multiIdentifier?: string;
|
|
542
596
|
};
|
|
597
|
+
markSegment?: {
|
|
598
|
+
kind: "start" | "break" | "stop";
|
|
599
|
+
setLocation?: boolean;
|
|
600
|
+
};
|
|
543
601
|
setCallField?: {
|
|
544
602
|
text: string;
|
|
545
603
|
};
|
|
@@ -575,6 +633,9 @@ export interface CommandAction {
|
|
|
575
633
|
deltaMillis?: number;
|
|
576
634
|
speed?: number;
|
|
577
635
|
};
|
|
636
|
+
devBench?: {
|
|
637
|
+
qsoCount?: number;
|
|
638
|
+
};
|
|
578
639
|
toggleExperiment?: {
|
|
579
640
|
token: string;
|
|
580
641
|
};
|
|
@@ -723,6 +784,14 @@ export interface ActivationApi {
|
|
|
723
784
|
registerHook(category: string, params: RegisterHookParams): void;
|
|
724
785
|
hostCall(method: string, params: Record<string, unknown>): Promise<unknown>;
|
|
725
786
|
}
|
|
787
|
+
export interface ExtensionRelevance {
|
|
788
|
+
entities?: string[];
|
|
789
|
+
countries?: string[];
|
|
790
|
+
continents?: string[];
|
|
791
|
+
ituRegions?: number[];
|
|
792
|
+
dates?: string[];
|
|
793
|
+
interests?: string[];
|
|
794
|
+
}
|
|
726
795
|
export interface ExtensionManifest {
|
|
727
796
|
key: string;
|
|
728
797
|
name: string;
|
|
@@ -739,6 +808,7 @@ export interface ExtensionManifest {
|
|
|
739
808
|
allowUserAgentOverride?: boolean;
|
|
740
809
|
requiresLocation?: boolean;
|
|
741
810
|
experiments?: string[];
|
|
811
|
+
relevance?: ExtensionRelevance;
|
|
742
812
|
translations?: Record<string, {
|
|
743
813
|
name?: string;
|
|
744
814
|
description?: string;
|
|
@@ -838,7 +908,7 @@ export interface DataFileDefinition {
|
|
|
838
908
|
onLoadRawData?: (data: any) => void;
|
|
839
909
|
onRemoveRawData?: () => Promise<void>;
|
|
840
910
|
}
|
|
841
|
-
export type FormFieldType = "text" | "multiline" | "email" | "callsign" | "number" | "select" | "radio" | "checkbox" | "secret" | "list" | "multiselect" | "account";
|
|
911
|
+
export type FormFieldType = "text" | "multiline" | "textTemplate" | "email" | "callsign" | "number" | "select" | "radio" | "checkbox" | "secret" | "list" | "multiselect" | "account";
|
|
842
912
|
export interface FormFieldOption {
|
|
843
913
|
label: string;
|
|
844
914
|
value: any;
|
|
@@ -852,6 +922,8 @@ export interface FormField {
|
|
|
852
922
|
label: string;
|
|
853
923
|
value?: any;
|
|
854
924
|
placeholder?: string;
|
|
925
|
+
templateContext?: "export" | "adif" | "text";
|
|
926
|
+
templateSample?: Record<string, JSONValue>;
|
|
855
927
|
uppercase?: boolean;
|
|
856
928
|
options?: FormFieldOption[] | string;
|
|
857
929
|
validate?: (value: any, state: Record<string, any>) => string | null | undefined | Promise<string | null | undefined>;
|
|
@@ -883,7 +955,7 @@ export interface FormHeader {
|
|
|
883
955
|
title: string;
|
|
884
956
|
subtitle?: string;
|
|
885
957
|
devMode?: boolean;
|
|
886
|
-
style?: "default" | "section";
|
|
958
|
+
style?: "default" | "section" | "category";
|
|
887
959
|
environment?: string | string[];
|
|
888
960
|
}
|
|
889
961
|
export interface FormSectionDivider {
|
|
@@ -901,6 +973,8 @@ export interface FormMarkdownBlock {
|
|
|
901
973
|
title?: string;
|
|
902
974
|
}
|
|
903
975
|
export interface FormActionElement {
|
|
976
|
+
style?: "row";
|
|
977
|
+
icon?: string;
|
|
904
978
|
type: "action";
|
|
905
979
|
key: string;
|
|
906
980
|
label: string;
|
|
@@ -1129,13 +1203,17 @@ type AnnotatedCallInfo$1 = CombinedCallInfo & {
|
|
|
1129
1203
|
originalValues?: AnnotatedCallInfo$1;
|
|
1130
1204
|
cfValues?: AnnotatedCallInfo$1;
|
|
1131
1205
|
};
|
|
1132
|
-
export declare function annotateCallAgainstCountryFile(call: string
|
|
1206
|
+
export declare function annotateCallAgainstCountryFile(call: string, options?: {
|
|
1207
|
+
wae?: boolean;
|
|
1208
|
+
}): AnnotatedCallInfo$1;
|
|
1133
1209
|
export declare function entityPrefixForCall(call: string | undefined): string | undefined;
|
|
1134
1210
|
export declare function flagForDxccCode(dxccCode: number | undefined): string | undefined;
|
|
1135
1211
|
export declare function regionAndCountryForDxccCode(dxccCode: number | undefined): {
|
|
1136
1212
|
ituRegion?: number;
|
|
1137
1213
|
countryCode?: string;
|
|
1138
1214
|
};
|
|
1215
|
+
export declare function countryPrefixForCall(call: string | undefined): string | undefined;
|
|
1216
|
+
export declare function entityForCall(call: string | undefined): Pick<AnnotatedCallInfo$1, "dxccCode" | "continent" | "entityName">;
|
|
1139
1217
|
export declare const LOCATION_ACCURACY: {
|
|
1140
1218
|
readonly EXACT: 0;
|
|
1141
1219
|
readonly ACCURATE: 2;
|
|
@@ -1149,6 +1227,14 @@ export declare function applyLatLonFromApiFields(target: {
|
|
|
1149
1227
|
}, raw: Record<string, unknown>): void;
|
|
1150
1228
|
export declare function canonicalMode(mode: string): string;
|
|
1151
1229
|
export declare function modesMatchForDupe(worked: string, candidate: string): boolean;
|
|
1230
|
+
export declare function expectedSideband({ freqKHz, band }: {
|
|
1231
|
+
freqKHz?: number;
|
|
1232
|
+
band?: string;
|
|
1233
|
+
}): string | undefined;
|
|
1234
|
+
export declare function isUnexpectedSideband(mode: string | undefined | null, { freqKHz, band }: {
|
|
1235
|
+
freqKHz?: number;
|
|
1236
|
+
band?: string;
|
|
1237
|
+
}): boolean;
|
|
1152
1238
|
export interface ContestScorerOptions {
|
|
1153
1239
|
scope?: ScoringScope;
|
|
1154
1240
|
}
|
|
@@ -1156,6 +1242,8 @@ export declare function contestScorer<S extends Scoresheet = Scoresheet>(scorer:
|
|
|
1156
1242
|
scope: ScoringScope;
|
|
1157
1243
|
};
|
|
1158
1244
|
export declare function tally(key: string, forScope: "day" | "operation", total: number, extra?: Partial<ScoreTally>): ScoreTally;
|
|
1245
|
+
export declare function operationForQso(base: Record<string, JSONValue>, qso: Record<string, JSONValue>, segments?: OperationSegmentPayload[]): Record<string, JSONValue>;
|
|
1246
|
+
export declare function segmentsWith(segments: OperationSegmentPayload[] | undefined, transform: (operation: Record<string, JSONValue>) => Record<string, JSONValue>): OperationSegmentPayload[] | undefined;
|
|
1159
1247
|
export type UniqueAxis = "day" | "band" | "mode" | "ref";
|
|
1160
1248
|
export interface ActivityScoringRules {
|
|
1161
1249
|
label: string;
|
|
@@ -1188,12 +1276,14 @@ export type ActivityScoresheet = {
|
|
|
1188
1276
|
export declare function activityScorer(rules: ActivityScoringRules): ContestScorer<ActivityScoresheet>;
|
|
1189
1277
|
export interface ActivityExportRules {
|
|
1190
1278
|
key: string;
|
|
1279
|
+
templateSample?: Record<string, JSONValue>;
|
|
1191
1280
|
label: string;
|
|
1192
1281
|
activationType: string;
|
|
1193
1282
|
icon?: string;
|
|
1194
1283
|
includeFieldsFrom?: string[];
|
|
1195
1284
|
}
|
|
1196
1285
|
export declare function activityExportHook(rules: ActivityExportRules): {
|
|
1286
|
+
getExportTypes(): Promise<ExportTypeDefinition[]>;
|
|
1197
1287
|
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
1198
1288
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
1199
1289
|
};
|
|
@@ -1207,13 +1297,19 @@ export interface HuntingExportRules {
|
|
|
1207
1297
|
includeFieldsFrom?: string[];
|
|
1208
1298
|
}
|
|
1209
1299
|
export declare function huntingExportHook(rules: HuntingExportRules): {
|
|
1300
|
+
getExportTypes(): Promise<ExportTypeDefinition[]>;
|
|
1210
1301
|
suggestExportOptions({ operation, qsos, compactFilenames }: ExportOptionsRequest, ctx: HookContext): Promise<ExportOption[]>;
|
|
1211
1302
|
generateExport(args: ExportRequest, ctx: HookContext): Promise<ExportResult>;
|
|
1212
1303
|
};
|
|
1213
|
-
export declare function adifForExport({ operation, qsos, includePrivateData, mainHandler, includeFieldsFrom }: {
|
|
1304
|
+
export declare function adifForExport({ operation, qsos, segments, includePrivateData, includeLookupData, exportSettings, exportData, exportTitle, mainHandler, includeFieldsFrom }: {
|
|
1214
1305
|
operation: Record<string, JSONValue>;
|
|
1215
1306
|
qsos: Record<string, JSONValue>[];
|
|
1307
|
+
segments?: OperationSegmentPayload[];
|
|
1216
1308
|
includePrivateData?: boolean;
|
|
1309
|
+
includeLookupData?: boolean;
|
|
1310
|
+
exportSettings?: ExportSettings;
|
|
1311
|
+
exportData?: Record<string, JSONValue>;
|
|
1312
|
+
exportTitle?: string;
|
|
1217
1313
|
mainHandler: string;
|
|
1218
1314
|
includeFieldsFrom?: string[];
|
|
1219
1315
|
}): Promise<string>;
|
|
@@ -1236,6 +1332,25 @@ export interface ExportNameParts {
|
|
|
1236
1332
|
}
|
|
1237
1333
|
export declare function exportFilename(parts: ExportNameParts): string;
|
|
1238
1334
|
export declare function startMillisOf(operation: Record<string, JSONValue>, qsos?: Record<string, JSONValue>[]): number | undefined;
|
|
1335
|
+
export declare const EXPORT_DEFAULTS: ExportSettings;
|
|
1336
|
+
export declare const GLOBAL_EXPORT_DEFAULTS: GlobalExportSettings;
|
|
1337
|
+
export declare function resolveGlobalExportSettings(global?: GlobalExportSettings): GlobalExportSettings;
|
|
1338
|
+
export declare function exportTypeDefinition(activationType: string, format: string, label: string, defaults?: ExportSettings): ExportTypeDefinition;
|
|
1339
|
+
export declare function resolveExportSettings(definition: ExportTypeDefinition, global?: GlobalExportSettings, overrides?: ExportSettings): ExportSettings;
|
|
1340
|
+
export declare function exportQso(qso: Record<string, JSONValue>, settings: ExportSettings): Record<string, JSONValue>;
|
|
1341
|
+
export declare function prepareExportOption(option: ExportOption & {
|
|
1342
|
+
exportSettings: ExportSettings;
|
|
1343
|
+
}, operation: Record<string, JSONValue>, qsos: Record<string, JSONValue>[], compact: boolean): ExportOption & {
|
|
1344
|
+
exportSettings: ExportSettings;
|
|
1345
|
+
exportTitle: string;
|
|
1346
|
+
exportData: Record<string, JSONValue>;
|
|
1347
|
+
};
|
|
1348
|
+
export type RefTransform = {
|
|
1349
|
+
pattern: string;
|
|
1350
|
+
replacement: string;
|
|
1351
|
+
flags?: string;
|
|
1352
|
+
};
|
|
1353
|
+
export declare function transformsForPrefix(prefix: string): RefTransform[];
|
|
1239
1354
|
export interface ReferenceActivityConfig {
|
|
1240
1355
|
key: string;
|
|
1241
1356
|
label: string;
|
|
@@ -1254,7 +1369,20 @@ export interface ReferenceActivityConfig {
|
|
|
1254
1369
|
adifRefField?: string;
|
|
1255
1370
|
allowsMultiple?: boolean;
|
|
1256
1371
|
linkUrl?: (reference: string) => string;
|
|
1372
|
+
transforms?: RefTransform[];
|
|
1373
|
+
refInput?: (args: RefInputArgs, ctx: HookContext) => Promise<RefInputExtras> | RefInputExtras;
|
|
1374
|
+
}
|
|
1375
|
+
export interface RefInputArgs {
|
|
1376
|
+
operation: Record<string, JSONValue>;
|
|
1377
|
+
qso?: Record<string, JSONValue>;
|
|
1378
|
+
side: "activation" | "hunting";
|
|
1379
|
+
}
|
|
1380
|
+
export interface RefInputExtras {
|
|
1381
|
+
placeholder?: string;
|
|
1382
|
+
transforms?: RefTransform[];
|
|
1257
1383
|
}
|
|
1384
|
+
export declare function learnedReferencePrefix(category: string, entityPrefix: string, fallback: string): Promise<string>;
|
|
1385
|
+
export declare function looksLikeReference(text: string, referenceRegex: RegExp): boolean;
|
|
1258
1386
|
export interface ReferenceActivityHooks {
|
|
1259
1387
|
refHandler: Record<string, unknown>;
|
|
1260
1388
|
activityHook: Record<string, unknown>;
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
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 "./exportSettings.js";
|
|
13
|
+
export * from "./referenceActivity.js";
|
|
14
|
+
export * from "./refTransforms.js";
|
|
15
|
+
export * from "./templates.js";
|
|
16
|
+
export * from "./templateContext.js";
|
|
17
|
+
export * from "./testOperation.js";
|
|
15
18
|
let currentExtensionKey = "unknown";
|
|
16
19
|
const privilegedHostCalls = {};
|
|
17
20
|
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
|
+
};
|
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.
|