@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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.2.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): AnnotatedCallInfo$1;
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 "./activityScoring";
8
- export * from "./activityExports";
9
- export * from "./activityAdifImport";
10
- export * from "./exportNames";
11
- export * from "./referenceActivity";
12
- export * from "./templates";
13
- export * from "./templateContext";
14
- export * from "./testOperation";
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 { ...ref, ref: reference, name: unknown, program };
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 { ...ref, ref: reference, name: t("invalidReference") };
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(_args, ctx) {
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(_args, ctx) {
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 }, _ctx) {
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
- return withDistance.slice(0, MAX_SUGGESTIONS).map(({ row, distance }) => {
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] = { notices: score.notices ?? [], alerts: score.alerts ?? [] };
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
  }
@@ -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
+ };
@@ -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.