@ham2k/extension-sdk 0.7.0 → 0.8.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.
@@ -1,5 +1,6 @@
1
1
  import { createCachedTranslator } from "./i18n.js";
2
2
  import { modesMatchForDupe } from "./modes.js";
3
+ import { parseCallsign } from "@ham2k/lib-callsigns";
3
4
  import { fmtInteger } from "@ham2k/lib-format-tools";
4
5
  import en from "./i18n/activityScoring.en.json";
5
6
  import es from "./i18n/activityScoring.es.json";
@@ -34,6 +35,7 @@ function activityScorer(rules) {
34
35
  }
35
36
  const freshRefRescuesActivation = rules.freshRefRescuesActivation ?? true;
36
37
  const tracksActivation = rules.tracksActivation ?? true;
38
+ const tracksOperators = rules.tracksOperators ?? true;
37
39
  const allowsMultiple = rules.allowsMultipleReferences ?? false;
38
40
  const activatesDaily = (rules.activates ?? "daily") === "daily";
39
41
  const unique = new Set(rules.uniquePer ?? []);
@@ -79,11 +81,15 @@ function activityScorer(rules) {
79
81
  return {
80
82
  workedByCall: {},
81
83
  activatedRefs: {},
84
+ operatorRefs: {},
85
+ guestOperators: false,
82
86
  huntedRefs: {},
83
87
  activatedQsos: 0,
84
88
  huntedQsos: 0,
85
89
  duplicates: 0,
86
90
  dayActivatedRefs: {},
91
+ dayOperatorRefs: {},
92
+ dayGuestOperators: false,
87
93
  dayHuntedRefs: {},
88
94
  dayActivatedQsos: 0,
89
95
  dayHuntedQsos: 0,
@@ -95,9 +101,14 @@ function activityScorer(rules) {
95
101
  // per QSO makes a full pass quadratic. The harness copies any resume
96
102
  // checkpoint at its boundary, so nothing cached is ever aliased.
97
103
  scoreQso({ scoresheet, qso, operation, isNewDay }) {
104
+ var _a, _b;
98
105
  const sheet = scoresheet;
106
+ sheet.operatorRefs ?? (sheet.operatorRefs = {});
107
+ sheet.dayOperatorRefs ?? (sheet.dayOperatorRefs = {});
99
108
  if (isNewDay && activatesDaily) {
100
109
  sheet.dayActivatedRefs = {};
110
+ sheet.dayOperatorRefs = {};
111
+ sheet.dayGuestOperators = false;
101
112
  sheet.dayHuntedRefs = {};
102
113
  sheet.dayActivatedQsos = 0;
103
114
  sheet.dayHuntedQsos = 0;
@@ -136,9 +147,20 @@ function activityScorer(rules) {
136
147
  sheet.dayDuplicates += 1;
137
148
  return { scoresheet: sheet, score };
138
149
  }
150
+ const operator = tracksOperators ? str((qso.our ?? {}).operatorCall)?.trim().toUpperCase() : void 0;
151
+ if (operator && isGuestOperator(operator, operation)) {
152
+ sheet.guestOperators = true;
153
+ sheet.dayGuestOperators = true;
154
+ }
139
155
  for (const ref of activationRefs) {
140
156
  sheet.activatedRefs[ref] = (sheet.activatedRefs[ref] ?? 0) + activationCredit;
141
157
  sheet.dayActivatedRefs[ref] = (sheet.dayActivatedRefs[ref] ?? 0) + activationCredit;
158
+ if (operator) {
159
+ const shares = (_a = sheet.operatorRefs)[ref] ?? (_a[ref] = {});
160
+ shares[operator] = (shares[operator] ?? 0) + activationCredit;
161
+ const dayShares = (_b = sheet.dayOperatorRefs)[ref] ?? (_b[ref] = {});
162
+ dayShares[operator] = (dayShares[operator] ?? 0) + activationCredit;
163
+ }
142
164
  }
143
165
  sheet.activatedQsos += activationCredit;
144
166
  sheet.dayActivatedQsos += activationCredit;
@@ -160,6 +182,7 @@ function activityScorer(rules) {
160
182
  if (tracksActivation && Object.keys(activatedRefs).length > 0) {
161
183
  tallies.activation = summarizeActivation(
162
184
  activatedRefs,
185
+ (perDay ? scoresheet.dayGuestOperators : scoresheet.guestOperators) ? perDay ? scoresheet.dayOperatorRefs : scoresheet.operatorRefs : {},
163
186
  perDay ? scoresheet.dayActivatedQsos : scoresheet.activatedQsos,
164
187
  perDay ? scoresheet.dayDuplicates : scoresheet.duplicates,
165
188
  huntedQsos,
@@ -176,7 +199,15 @@ function activityScorer(rules) {
176
199
  }
177
200
  };
178
201
  }
179
- function summarizeActivation(activatedRefs, qsos, duplicates, huntedQsos, scope, qsosToActivate, rules, t, ctx) {
202
+ function isGuestOperator(operator, operation) {
203
+ const stations = String(operation.stationCall ?? "").split(",").map((call) => baseCallOf(call)).filter((call) => call.length > 0);
204
+ return !stations.includes(baseCallOf(operator));
205
+ }
206
+ function baseCallOf(call) {
207
+ const trimmed = call.trim().toUpperCase();
208
+ return parseCallsign(trimmed).baseCall || trimmed;
209
+ }
210
+ function summarizeActivation(activatedRefs, operatorRefs, qsos, duplicates, huntedQsos, scope, qsosToActivate, rules, t, ctx) {
180
211
  const refKeys = Object.keys(activatedRefs).sort();
181
212
  const lowest = Math.min(...refKeys.map((key) => activatedRefs[key]));
182
213
  const activated = lowest >= qsosToActivate;
@@ -184,9 +215,14 @@ function summarizeActivation(activatedRefs, qsos, duplicates, huntedQsos, scope,
184
215
  if (!activated) summary = `${fmtInteger(lowest)}/${fmtInteger(qsosToActivate)}`;
185
216
  else if (refKeys.length < 6) summary = `${fmtInteger(lowest)} ${"\u2713".repeat(refKeys.length)}`;
186
217
  else summary = `${fmtInteger(lowest)} \u2713 x ${fmtInteger(refKeys.length)}`;
187
- const detail = refKeys.map(
188
- (key) => activatedRefs[key] >= qsosToActivate ? `\u2705 **${key}: ${fmtInteger(activatedRefs[key])}**` : `\u274C ${key}: ${fmtInteger(activatedRefs[key])}/${fmtInteger(qsosToActivate)}`
189
- );
218
+ const detail = refKeys.flatMap((key) => {
219
+ const line = activatedRefs[key] >= qsosToActivate ? `\u2705 **${key}: ${fmtInteger(activatedRefs[key])}**` : `\u274C ${key}: ${fmtInteger(activatedRefs[key])}/${fmtInteger(qsosToActivate)}`;
220
+ const byOperator = operatorRefs[key] ?? {};
221
+ const operators = Object.keys(byOperator).sort();
222
+ if (operators.length === 0) return [line];
223
+ const shares = operators.map((op) => byOperator[op] >= qsosToActivate ? `${op} \u2713` : `${op} ${fmtInteger(byOperator[op])}/${fmtInteger(qsosToActivate)}`);
224
+ return [line, `\xA0\xA0\xA0\xA0${shares.join(" \xB7 ")}`];
225
+ });
190
226
  const parts = [qsos === 1 ? t("activationQsoOne") : t("activationQsoMany", { formatted: fmtInteger(qsos) })];
191
227
  if (huntedQsos > 0) {
192
228
  const p2p = resolveRuleText(rules.p2pLabel, ctx, t("ref2ref"));
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.7.0
1
+ // @ham2k/extension-sdk 0.8.0
2
2
  /** Experimental v1 scene contract. All coordinates are in the scene's viewBox. */
3
3
  export interface SvgScene {
4
4
  version: 1;
@@ -1549,6 +1549,7 @@ export interface ActivityScoringRules {
1549
1549
  huntingType?: string;
1550
1550
  qsosToActivate?: number | ((activatedRefs: string[]) => number);
1551
1551
  tracksActivation?: boolean;
1552
+ tracksOperators?: boolean;
1552
1553
  freshRefRescuesActivation?: boolean;
1553
1554
  allowsMultipleReferences?: boolean;
1554
1555
  uniquePer?: readonly UniqueAxis[];
@@ -1560,11 +1561,15 @@ export interface ActivityScoringRules {
1560
1561
  export type ActivityScoresheet = {
1561
1562
  workedByCall: Record<string, string[]>;
1562
1563
  activatedRefs: Record<string, number>;
1564
+ operatorRefs: Record<string, Record<string, number>>;
1565
+ guestOperators: boolean;
1563
1566
  huntedRefs: Record<string, number>;
1564
1567
  activatedQsos: number;
1565
1568
  huntedQsos: number;
1566
1569
  duplicates: number;
1567
1570
  dayActivatedRefs: Record<string, number>;
1571
+ dayOperatorRefs: Record<string, Record<string, number>>;
1572
+ dayGuestOperators: boolean;
1568
1573
  dayHuntedRefs: Record<string, number>;
1569
1574
  dayActivatedQsos: number;
1570
1575
  dayHuntedQsos: number;
@@ -352,7 +352,9 @@ from whoever it claims; it is not worth it while a key is all there is.
352
352
  a single confirmation that names each extension and the version it moves to,
353
353
  and skips the per-extension screen for each of them: asking once per entry for
354
354
  the one thing the operator asked for once is how a screen gets pressed through
355
- unread. It is a confirmation of a job the operator started, never a way to
355
+ unread. Pressed on the update notice when the notice's text already named every
356
+ extension it takes, it skips that confirmation too: the press under those names
357
+ was it. It is a confirmation of a job the operator started, never a way to
356
358
  start one — `install_consent_scope_test` holds it to the one function that
357
359
  shows that confirmation first.
358
360
 
@@ -492,7 +494,8 @@ At most once every six hours — on launch and on resume — the app asks the
492
494
  catalog whether anything installed has a newer release, and says so in the
493
495
  status bar and on the row; the notice comes down with the last update taken.
494
496
  It never installs unasked: the notice's **Update all** is the panel's own,
495
- behind the same confirmation, and **More info** opens the panel.
497
+ behind the same confirmation unless the notice named every extension it takes,
498
+ and **More info** opens the panel.
496
499
 
497
500
  An install or update narrates itself as one status-bar progress item, from the
498
501
  first downloaded byte to "Installed", and the Extensions panel mirrors that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",