@ham2k/extension-sdk 0.12.2 → 0.12.6

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/dxcc.js CHANGED
@@ -1,16 +1,22 @@
1
1
  import { parseCallsign } from "@ham2k/lib-callsigns";
2
- import { annotateFromCountryFile, useBuiltinCountryFile } from "@ham2k/lib-country-files";
2
+ import { annotateFromCountryFile, BIGCTY, useBuiltinCountryFile } from "@ham2k/lib-country-files";
3
3
  import { DXCC_BY_CODE } from "@ham2k/lib-dxcc-data";
4
4
  useBuiltinCountryFile();
5
5
  function annotateCallAgainstCountryFile(call, options) {
6
6
  const annotated = annotateFromCountryFile(parseCallsign(call), options);
7
- return annotated.tz ? { ...annotated, tz: ordinarySignTz(annotated.tz) } : annotated;
7
+ if (!WEST_POSITIVE) return annotated;
8
+ return {
9
+ ...annotated,
10
+ ...typeof annotated.lon === "number" ? { lon: 0 - annotated.lon } : {},
11
+ ...annotated.tz ? { tz: flippedTz(annotated.tz) } : {}
12
+ };
8
13
  }
9
- function ordinarySignTz(tz) {
14
+ const WEST_POSITIVE = (BIGCTY.entities.K?.lon ?? 0) > 0;
15
+ function flippedTz(tz) {
10
16
  const match = /^GMT([+-]?\d+(?:\.\d+)?)$/.exec(tz);
11
17
  if (!match) return tz;
12
- const offset = -Number(match[1]);
13
- return offset > 0 ? `GMT+${offset}` : `GMT${offset === 0 ? 0 : offset}`;
18
+ const offset = 0 - Number(match[1]);
19
+ return offset > 0 ? `GMT+${offset}` : `GMT${offset}`;
14
20
  }
15
21
  function entityPrefixForCall(call) {
16
22
  if (!call) return void 0;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // @ham2k/extension-sdk 0.12.2
1
+ // @ham2k/extension-sdk 0.12.6
2
2
  /** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
3
3
  export interface PanelScene {
4
4
  version: 1;
@@ -97,11 +97,193 @@ export interface PanelSceneLayer {
97
97
  * - `nativeSwitch`, `nativeCheckbox`: a number, 1 or 0. Shows `label` beside it.
98
98
  * - `nativeChip`: with `value`, a toggle (1 or 0); without, an action that sends `activate`.
99
99
  * - `nativeText`: shows `label`, or its bound value. Sends nothing.
100
+ * - `spectrum`: the radio's panadapter and waterfall, drawn by the host from sweeps that never
101
+ * reach the extension. Needs `requiresRadioRead`; a host without the feature, or a radio
102
+ * without a scope, shows why instead. A click sends `commit` with the frequency as `value`.
103
+ * Takes no `value`; see `PanelSceneSpectrum` for the rest. Extension API 6: declare `"api": 6`.
100
104
  *
101
105
  * `label` is every control's accessible name. There are no tooltips.
102
106
  */
103
- export type PanelSceneControlKind = "button" | "slider" | "knob" | "nativeButton" | "nativeSlider" | "nativeTextField" | "nativeDropdown" | "nativeSwitch" | "nativeCheckbox" | "nativeRadio" | "nativeSegmented" | "nativeChip" | "nativeText";
104
- export interface PanelSceneControl {
107
+ export type PanelSceneControlKind = "button" | "slider" | "knob" | "nativeButton" | "nativeSlider" | "nativeTextField" | "nativeDropdown" | "nativeSwitch" | "nativeCheckbox" | "nativeRadio" | "nativeSegmented" | "nativeChip" | "nativeText" | "spectrum";
108
+ /** A literal, or the name of a scene value — which a slider with no `event` retunes with no bridge call. */
109
+ export type PanelSceneSpectrumNumber = number | string;
110
+ /**
111
+ * A `spectrum` control's display. All of it happens on the host after the sweep arrives, so it
112
+ * means the same on every radio; the radio's own scope (span, reference level, sweep speed) is
113
+ * `host.setRadioSpectrum`'s. Colors are `#RRGGBB`, or `#AARRGGBB` with transparency. Defaults
114
+ * follow the app's theme.
115
+ */
116
+ export interface PanelSceneSpectrum {
117
+ /** A radio's id from `listRadios`. Omitted, the designated radio. */
118
+ radio?: string;
119
+ /** Default `both`. */
120
+ display?: "both" | "pan" | "waterfall";
121
+ /**
122
+ * Which way the waterfall moves; the panadapter sits on the side it comes from. Frequency rises
123
+ * left to right for `down` (the default) and `up`, bottom to top for `right` and `left`. Text
124
+ * stays upright; offsets and `corner`s are measured as if flowing down.
125
+ */
126
+ flow?: "down" | "up" | "right" | "left";
127
+ /** The panadapter's share of the height with `both`, 0.1–0.9. Default 0.4. */
128
+ split?: number;
129
+ /** dB added to the radio's own floor. */
130
+ floor?: PanelSceneSpectrumNumber;
131
+ /** The panadapter's height in dB. Omitted, the radio's. */
132
+ range?: PanelSceneSpectrumNumber;
133
+ /** Written as the pointer moves: the frequency and level under it. */
134
+ hoverHz?: string;
135
+ hoverDb?: string;
136
+ /** 0–99: the share of the trace each sweep keeps from the last. Default 40. The waterfall stays raw. */
137
+ average?: PanelSceneSpectrumNumber;
138
+ /** The most times a second the panadapter redraws, 1–60. Default 15. The waterfall keeps its own pace. */
139
+ fps?: PanelSceneSpectrumNumber;
140
+ /** Waterfall rows a second, 0.1–60; each row keeps the loudest reading since the last. Omitted, one per sweep. */
141
+ waterfallSpeed?: PanelSceneSpectrumNumber;
142
+ /** The waterfall's own dB window, like `floor` and `range`. Omitted, the panadapter's. */
143
+ waterfallFloor?: PanelSceneSpectrumNumber;
144
+ waterfallRange?: PanelSceneSpectrumNumber;
145
+ /** Default `classic`. */
146
+ palette?: "classic" | "gray" | "heat";
147
+ /** 2 to 8 stops, lowest level first, in place of `palette`. */
148
+ waterfallColors?: string[];
149
+ traceColor?: string;
150
+ /** 0.25–8. Default 1. */
151
+ traceWidth?: PanelSceneSpectrumNumber;
152
+ /** Under the trace. With `fillColorBottom`, a gradient from the top of the panadapter to its bottom. */
153
+ fillColor?: string;
154
+ fillColorBottom?: string;
155
+ backgroundColor?: string;
156
+ /** The panadapter's dB and frequency lines. */
157
+ gridColor?: string;
158
+ /** The frequency lines carried down through the waterfall. Omitted, none. */
159
+ waterfallGridColor?: string;
160
+ /** A click tunes to the nearest multiple of this many Hz. Omitted, wherever it lands. */
161
+ tuneStep?: PanelSceneSpectrumNumber;
162
+ /** Dashes the panadapter's grid lines. */
163
+ gridDashed?: boolean;
164
+ /** The gap between the panadapter and the waterfall, with frequency labels and ticks. `true` takes the defaults. */
165
+ scale?: boolean | PanelSceneSpectrumScale;
166
+ /** The tuned-frequency line, styled per part; a part it leaves out has none. Omitted, the theme's on both. */
167
+ center?: {
168
+ pan?: PanelSceneSpectrumStyle;
169
+ waterfall?: PanelSceneSpectrumStyle;
170
+ };
171
+ /** Up to 16 lines and areas at Hz offsets — a passband, a CW offset. */
172
+ overlays?: PanelSceneSpectrumOverlay[];
173
+ /** Up to 64 pieces of text at frequencies — a spot, a net, a beacon. */
174
+ annotations?: PanelSceneSpectrumAnnotation[];
175
+ }
176
+ /**
177
+ * The gap sits between the panadapter and the waterfall — above the waterfall with no panadapter,
178
+ * below the panadapter with no waterfall. Labels and ticks are drawn over the whole control, so an
179
+ * `offset` can place them anywhere: it is pixels from the base of the panadapter (the top of the
180
+ * waterfall where there is none), negative rising into the panadapter.
181
+ */
182
+ export interface PanelSceneSpectrumScale {
183
+ /** Pixels, 0–200. Default 20. */
184
+ gap?: PanelSceneSpectrumNumber;
185
+ background?: string;
186
+ borderTop?: {
187
+ color?: string;
188
+ width?: PanelSceneSpectrumNumber;
189
+ };
190
+ borderBottom?: {
191
+ color?: string;
192
+ width?: PanelSceneSpectrumNumber;
193
+ };
194
+ /** On by default; `false` hides them. */
195
+ labels?: boolean | {
196
+ /** Hz between labels. Omitted, the gridlines' step. */
197
+ interval?: PanelSceneSpectrumNumber;
198
+ fontFamily?: string;
199
+ size?: PanelSceneSpectrumNumber;
200
+ /** 100–900. */
201
+ weight?: PanelSceneSpectrumNumber;
202
+ italic?: boolean;
203
+ color?: string;
204
+ /** To the top of the text. Omitted, centered in the gap. */
205
+ offset?: PanelSceneSpectrumNumber;
206
+ };
207
+ /** Off by default. */
208
+ ticks?: boolean | {
209
+ /** Hz between ticks. Omitted, the labels'. */
210
+ interval?: PanelSceneSpectrumNumber;
211
+ color?: string;
212
+ width?: PanelSceneSpectrumNumber;
213
+ /** To the top of the tick. Default 0. */
214
+ offset?: PanelSceneSpectrumNumber;
215
+ /** Default 4. */
216
+ length?: PanelSceneSpectrumNumber;
217
+ };
218
+ }
219
+ /**
220
+ * Text at a frequency, drawn by the host. With an `event`, a click sends `activate` with the
221
+ * frequency as `value` and this `id` as `annotation`, and does not tune; without one, a click
222
+ * on it tunes like anywhere else.
223
+ */
224
+ export interface PanelSceneSpectrumAnnotation {
225
+ /** Unique within the control. */
226
+ id: string;
227
+ /** At most 256 characters. */
228
+ text: string;
229
+ /** A frequency, or an offset from the tuned frequency or the sweep's middle with `anchor`. */
230
+ hz: PanelSceneSpectrumNumber;
231
+ /** Default `absolute`. */
232
+ anchor?: "vfo" | "center" | "absolute";
233
+ /** Default `pan`. */
234
+ part?: "pan" | "waterfall";
235
+ /** Pixels from the panadapter's bottom up to the text's, or from the waterfall's top down to the text's. Default 4. */
236
+ offset?: PanelSceneSpectrumNumber;
237
+ /** Which end of the text sits at `hz`. Default `center`. */
238
+ align?: "start" | "center" | "end";
239
+ /**
240
+ * The point the box makes at `hz`: a drawn-out corner for `start`/`end`, a notch for `center`.
241
+ * With the default `corner`, its tip sits at `offset`; turned the other way, the box's edge
242
+ * does. Shown by default (size 4, pointing toward the panadapter's bottom
243
+ * or the waterfall's top); `false` or `show: false` hides it.
244
+ */
245
+ pointer?: boolean | {
246
+ show?: boolean;
247
+ size?: PanelSceneSpectrumNumber;
248
+ corner?: "top" | "bottom";
249
+ };
250
+ fontFamily?: string;
251
+ size?: PanelSceneSpectrumNumber;
252
+ weight?: PanelSceneSpectrumNumber;
253
+ italic?: boolean;
254
+ color?: string;
255
+ background?: string;
256
+ borderColor?: string;
257
+ borderWidth?: PanelSceneSpectrumNumber;
258
+ /** Default 2. */
259
+ padding?: PanelSceneSpectrumNumber;
260
+ /** Default 2. */
261
+ radius?: PanelSceneSpectrumNumber;
262
+ event?: string;
263
+ }
264
+ /** `color` and `width` draw a line, or an area's edges; `fill` fills an area. */
265
+ export interface PanelSceneSpectrumStyle {
266
+ color?: string;
267
+ width?: PanelSceneSpectrumNumber;
268
+ fill?: string;
269
+ }
270
+ /**
271
+ * A line at `at`, or an area between `from` and `to`, in Hz from the tuned frequency — from the
272
+ * middle of the sweep with `anchor: "center"` (a Flex's panadapter is not centered on its slice),
273
+ * or as frequencies with `anchor: "absolute"` (a band edge, a segment). On the panadapter an area
274
+ * draws behind the trace and a line in front of it unless `behind` — which absolute ones default to.
275
+ * On the waterfall all of it draws over the rows. A part with no style is not drawn on.
276
+ */
277
+ export interface PanelSceneSpectrumOverlay {
278
+ at?: PanelSceneSpectrumNumber;
279
+ from?: PanelSceneSpectrumNumber;
280
+ to?: PanelSceneSpectrumNumber;
281
+ anchor?: "vfo" | "center" | "absolute";
282
+ behind?: boolean;
283
+ pan?: PanelSceneSpectrumStyle;
284
+ waterfall?: PanelSceneSpectrumStyle;
285
+ }
286
+ export interface PanelSceneControl extends PanelSceneSpectrum {
105
287
  id: string;
106
288
  label: string;
107
289
  kind: PanelSceneControlKind;
@@ -177,6 +359,8 @@ export interface PanelSceneEvent {
177
359
  value?: number;
178
360
  /** The control's string, for a control bound to `strings`. */
179
361
  text?: string;
362
+ /** The `id` of the spectrum annotation clicked; `value` is its frequency. */
363
+ annotation?: string;
180
364
  }
181
365
  /** Patch only named scene values and strings. The host ignores stale responses. */
182
366
  export interface PanelSceneEventResult {
@@ -372,6 +556,7 @@ export interface HookContext {
372
556
  getOperation?(uuid: string): Promise<Record<string, JSONValue> | null>;
373
557
  getQsos?(operationUuid: string): Promise<Record<string, JSONValue>[] | null>;
374
558
  getSpotsForCall?(call: string, maxAgeMinutes?: number): Promise<Spot[]>;
559
+ getSpots?(options?: GetSpotsOptions): Promise<BoardSpot[]>;
375
560
  getHistoryForCall?(call: string, options?: HistoryForCallOptions): Promise<Record<string, JSONValue>[]>;
376
561
  }
377
562
  export interface HistoryForCallOptions {
@@ -440,6 +625,24 @@ export interface Spot {
440
625
  sourceInfo?: Record<string, JSONValue>;
441
626
  };
442
627
  }
628
+ export interface GetSpotsOptions {
629
+ filter?: "user" | "all";
630
+ maxAgeMinutes?: number;
631
+ band?: string | string[];
632
+ mode?: string | string[];
633
+ source?: string | string[];
634
+ annotate?: boolean;
635
+ operationUuid?: string;
636
+ }
637
+ export interface BoardSpot extends Spot {
638
+ hookKey?: string;
639
+ annotations?: SpotAnnotations;
640
+ }
641
+ export interface SpotAnnotations {
642
+ worked: boolean;
643
+ isSelf: boolean;
644
+ notices: string[];
645
+ }
443
646
  export interface SpotEligibility {
444
647
  enabled: boolean;
445
648
  icon?: string;
@@ -492,6 +695,7 @@ export interface Ref {
492
695
  program?: string;
493
696
  activationRadiusInMeters?: number;
494
697
  activationRadiusInFeet?: number;
698
+ largeArea?: boolean;
495
699
  retired?: boolean;
496
700
  for?: "operation" | "qso";
497
701
  [key: string]: JSONValue | undefined;
@@ -1544,6 +1748,40 @@ export interface PanelRadioState {
1544
1748
  transmitting: boolean | null;
1545
1749
  meters: Partial<Record<"signalDbm" | "powerOut" | "swr" | "alc" | "supplyVolts", number>>;
1546
1750
  canTune: boolean;
1751
+ /**
1752
+ * The radio's scope, on hosts with the panadapter enabled: absent there otherwise, and null
1753
+ * for a radio without one. The sweeps are never sent — a `spectrum` scene control draws them.
1754
+ */
1755
+ spectrum?: PanelRadioSpectrum | null;
1756
+ }
1757
+ export type PanelRadioSpectrumSpeed = "slow" | "mid" | "fast";
1758
+ export interface PanelRadioSpectrum {
1759
+ /** Why nothing can be drawn, in the operator's language, or null. */
1760
+ problem: string | null;
1761
+ mode: "center" | "fixed" | null;
1762
+ /** Half the width shown while centered — what the radio's menu shows as ±. */
1763
+ spanHz: number | null;
1764
+ /** The edges while fixed. */
1765
+ lowHz: number | null;
1766
+ highHz: number | null;
1767
+ refLevelDb: number | null;
1768
+ speed: PanelRadioSpectrumSpeed | null;
1769
+ /** What `setRadioSpectrum` accepts. A span between two is snapped to the nearer. */
1770
+ spansHz: number[];
1771
+ refMinDb: number;
1772
+ refMaxDb: number;
1773
+ refStepDb: number;
1774
+ modes: ("center" | "fixed")[];
1775
+ speeds: PanelRadioSpectrumSpeed[];
1776
+ }
1777
+ export interface PanelRadioSpectrumSet {
1778
+ /** Reject if this identity no longer matches the selected radio. */
1779
+ id: string;
1780
+ selection?: string;
1781
+ mode?: "center" | "fixed";
1782
+ spanHz?: number;
1783
+ refLevelDb?: number;
1784
+ speed?: PanelRadioSpectrumSpeed;
1547
1785
  }
1548
1786
  export interface PanelRadioTune {
1549
1787
  /** Reject if this identity no longer matches the selected radio. */
@@ -1556,7 +1794,8 @@ export interface PanelRadioTune {
1556
1794
  export interface PanelRadioTuneResult {
1557
1795
  /** Accepted by the host; confirmation still comes from reported state. */
1558
1796
  accepted: boolean;
1559
- reason?: "changed" | "disconnected" | "transmitting" | "invalid";
1797
+ /** `stopped`: a scope change while no display has the scope running — nothing was sent. */
1798
+ reason?: "changed" | "disconnected" | "transmitting" | "invalid" | "stopped";
1560
1799
  state: PanelRadioState | null;
1561
1800
  }
1562
1801
  export interface PanelRenderArgs {
@@ -1650,7 +1889,8 @@ export interface ParsedCallsign {
1650
1889
  indicators?: string[];
1651
1890
  ssid?: string;
1652
1891
  }
1653
- export type CombinedCallInfo = ParsedCallsign & {
1892
+ export type CombinedCallInfo = Omit<ParsedCallsign, "call"> & {
1893
+ call?: string;
1654
1894
  isoPrefix?: string;
1655
1895
  dxccCode?: number;
1656
1896
  regionCode?: string;
@@ -1998,6 +2238,8 @@ export declare const host: {
1998
2238
  connected: boolean;
1999
2239
  }): Promise<PanelRadioTuneResult>;
2000
2240
  tuneRadio(args: PanelRadioTune): Promise<PanelRadioTuneResult>;
2241
+ /** Changes the radio's scope. Needs `requiresRadioWrite` and extension API 6; the result's state reports what the radio took. */
2242
+ setRadioSpectrum(args: PanelRadioSpectrumSet): Promise<PanelRadioTuneResult>;
2001
2243
  fetch(url: string, options?: FetchOptions): Promise<FetchResponse>;
2002
2244
  webSocket(url: string, options?: WebSocketOptions): ExtensionWebSocket;
2003
2245
  kvGet(key: string): Promise<JSONValue | null>;
package/dist/index.js CHANGED
@@ -172,6 +172,10 @@ const host = {
172
172
  async tuneRadio(args) {
173
173
  return await hostCallFn()("tuneRadio", { ...args });
174
174
  },
175
+ /** Changes the radio's scope. Needs `requiresRadioWrite` and extension API 6; the result's state reports what the radio took. */
176
+ async setRadioSpectrum(args) {
177
+ return await hostCallFn()("setRadioSpectrum", { ...args });
178
+ },
175
179
  async fetch(url, options) {
176
180
  return await hostCallFn()("fetch", { url, ...options });
177
181
  },
@@ -262,7 +262,7 @@ It reports every problem at once rather than one per run:
262
262
  h2kext-pack: ./build is not ready to package:
263
263
  manifest.json: missing required field 'name'
264
264
  manifest.json: key 'my-clock' must start with your callsign — 'my' is not one (e.g. ki2d-my-clock)
265
- manifest.json: missing 'api' — declare the extension API this was built against (currently 5)
265
+ manifest.json: missing 'api' — declare the extension API this was built against (currently 6)
266
266
  ```
267
267
 
268
268
  `--force-name` waives the naming convention below. It exists for Ham2K's own
package/docs/hooks.md CHANGED
@@ -70,6 +70,31 @@ hook method receives `(args, ctx)` where `ctx: HookContext` is:
70
70
  reads above it is cheap enough for the lookup path; it resolves `[]`
71
71
  whenever the cache is cold or the host keeps no spots. Used by
72
72
  `spot-history` to pull a hunted park or summit into the QSO being logged.
73
+ * `getSpots?(options?)` — the whole spots board the host already holds,
74
+ for a panel showing spots its own way. Like `getSpotsForCall` it reads the
75
+ cache and never fetches, and resolves `[]` while the cache is cold. By
76
+ default it answers what the operator's Spots Panel shows: their sources,
77
+ enabled bands, band and mode filters (an "auto" filter follows the radio),
78
+ age and continents, merged and ordered as that panel does — so one spot
79
+ can stand for several stations, its `their.call` comma-joined
80
+ (`'K1ABC,W2XYZ'`). `filter: 'all'`
81
+ starts instead from every held spot, one per source report, newest first.
82
+ Either way `maxAgeMinutes`, `band`, `mode` (a mode, or `SSB`/`Phone`/`CW`/
83
+ `Digital` for a family) and `source` (a spot source's hook key, carried on
84
+ each spot as `hookKey`) narrow it further. Spots come back with
85
+ `annotations` — `worked`, `isSelf`, `notices`, judged the way the Spots
86
+ Panel strikes and accents its rows — whenever there is an operation to judge
87
+ them against: `operationUuid`, or the `operation` the calling hook was
88
+ handed, so a panel's render gets them without asking. The verdicts are the
89
+ ones the app already holds for the operation open on screen, so the call
90
+ never waits on a scorer; another operation's spots, and that one's until
91
+ its first verdicts land, come back unannotated rather than guessed.
92
+ `annotate: false` returns the spots as their sources gave them.
93
+ A panel's `'spots'` trigger fires whenever this answer may have moved: a
94
+ refresh, new verdicts, the operator's spot filters or bands, or the radio's
95
+ band or mode. A malformed or unknown option rejects the call. The CLI host
96
+ has no operator: its `'user'` board is every spot handed to it, never
97
+ annotated.
73
98
  * `getHistoryForCall?(call, options?)` — the most recent non-deleted contacts
74
99
  logged with `call` (five unless `limit` says otherwise, never more than
75
100
  50), across every operation, newest first, as raw QSON — the one log read
@@ -247,14 +272,17 @@ interface LookupServiceHook {
247
272
  carried one (`preferStationValues`) — only while the guess's scope is
248
273
  `qth`, since on a portable or prefixed contact a source describes the
249
274
  home QTH. `tz` is a UTC offset with its ordinary sign, `"GMT-5"` being
250
- five hours behind; the SDK's `annotateCallAgainstCountryFile` flips the
251
- country file's west-positive offsets to match.
275
+ five hours behind. lib-country-files states it that way from 1.1; the
276
+ SDK's `annotateCallAgainstCountryFile` answers with the ordinary signs
277
+ whichever version the app ships, flipping an older one's west-positive
278
+ `tz` and `lon`.
252
279
  - **`locSource: 'prefix'` is reserved**, and a `lat`/`lon` merged under it is
253
280
  never read as a coordinate (`locationFromGuess`, halo_core). It marks
254
- lib-country-files' DXCC entity centroid, whose longitude follows cty.dat's
255
- west-positive convention — the opposite of every other coordinate here. A
256
- source reporting a real position must leave `locSource` unset or name
257
- itself; tagging one `'prefix'` gets its coordinate discarded.
281
+ lib-country-files' DXCC entity centroid, which synced records carry signed
282
+ either way — west-positive from versions before 1.1 — with nothing to tell
283
+ the two apart. A source reporting a real position must leave `locSource`
284
+ unset or name itself; tagging one `'prefix'` gets its coordinate
285
+ discarded.
258
286
 
259
287
  Implemented by: `lookups` (key `core`). The merge algorithm itself
260
288
  (`mergeLookupIntoGuess`/`runLookupPipeline` in
@@ -441,7 +469,7 @@ interface RefHandlerHook {
441
469
  geojsonUrlForRef?(args: { ref: Ref }, ctx): Promise<RefGeojson | null>
442
470
  }
443
471
  // Ref: {type, ref?, name?, label?, shortLabel?, grid?, lat?, lon?, location?, program?,
444
- // activationRadiusInMeters?, activationRadiusInFeet?, ...}
472
+ // activationRadiusInMeters?, activationRadiusInFeet?, largeArea?, ...}
445
473
  // TitleSuggestion: {title?, for?, at?, subtitle?, priority?} — a fragment of
446
474
  // the operation's title/subtitle sentence (ham2k/halo-dist#174). Only
447
475
  // return one for the ref type that means "this operation is happening AT/FOR
@@ -558,6 +586,15 @@ and is not offered for an outline report. Programs built on the SDK's
558
586
  `referenceActivity` factory get this from one `activationRadius` config line —
559
587
  a value, or a function of the list row for rules that vary.
560
588
 
589
+ **A reference too large to stand for one place says so.** An operation with
590
+ no location of its own takes the first of its references that has one
591
+ (docs/design/locations.md), which is right for a summit or a park a few
592
+ kilometres across and wrong for a 3,500 km trail. A reference whose point is
593
+ representative rather than where an activator stands — POTA's parks that span
594
+ several states — carries `largeArea: true`, and that reference never places
595
+ the operation. Nothing else reads it: the maps draw it, and the nearby search
596
+ finds it, exactly as before.
597
+
561
598
  `suggestOperationTitle` lets an activity build a natural-language operation
562
599
  title/subtitle from its refs — "at Yosemite NP" (POTA), "for Field Day"
563
600
  (contest programs, when ported) — mirroring app-polo's
@@ -1647,6 +1684,7 @@ the extension. `value` names what it shows and sets.
1647
1684
  | `nativeSwitch`, `nativeCheckbox` | number, 1 or 0 | `commit` | — (shows `label` beside it) |
1648
1685
  | `nativeChip` | number, 1 or 0; or none | `commit`; `activate` without a value | `icon`; needs `event` without a value |
1649
1686
  | `nativeText` | number, string, or none | nothing | `style` (a typography role), `align` (`start`, `center`, `end`) |
1687
+ | `spectrum` | — | `commit` with the frequency clicked; `activate` from an annotation | see *Spectrum* below |
1650
1688
 
1651
1689
  The three drawn kinds are invisible hit areas over artwork the extension paints;
1652
1690
  the native kinds are Material widgets the app paints and themes. Every control
@@ -1758,6 +1796,38 @@ export const spotter: PanelHook = {
1758
1796
  }
1759
1797
  ```
1760
1798
 
1799
+ #### Spectrum
1800
+
1801
+ A `spectrum` control is a radio's panadapter and waterfall, drawn by the host
1802
+ from sweeps that never reach the extension, on a host with the panadapter
1803
+ enabled. It is **extension API 6**, as is `host.setRadioSpectrum`: a bundle
1804
+ using either declares `"api": 6`, and an app speaking less refuses it at install
1805
+ rather than failing to render the scene. It needs `requiresRadioRead`; without it, or on a host or radio
1806
+ without a spectrum, the control says why instead. It is placed like a native
1807
+ control and takes the fields of `PanelSceneSpectrum` in the SDK:
1808
+
1809
+ - `radio` (an id from `listRadios`; omitted, the designated radio), `display`
1810
+ (`both`, `pan`, `waterfall`), `split`, and `flow` — which way the waterfall
1811
+ moves (`down`, `up`, `right`, `left`), the panadapter on the side it comes
1812
+ from.
1813
+ - `floor`, `range`, `average` (0–99, default 40), `fps` (default 15),
1814
+ `waterfallSpeed`, `waterfallFloor`, `waterfallRange`: numbers, or the names
1815
+ of scene values the control reads on every sweep.
1816
+ - Colors (`#RRGGBB` or `#AARRGGBB`): `palette` or `waterfallColors`,
1817
+ `traceColor`/`traceWidth`, `fillColor`/`fillColorBottom`, `backgroundColor`,
1818
+ `gridColor`/`gridDashed`, `waterfallGridColor`.
1819
+ - `center`: the tuned-frequency line, styled per part.
1820
+ - `overlays`: up to 16 lines (`at`) and areas (`from`, `to`) in Hz from the
1821
+ tuned frequency, the sweep's middle, or absolute, each styled per part.
1822
+ - `scale`: the gap between the panadapter and the waterfall, its fill and
1823
+ borders, and frequency `labels` and `ticks`.
1824
+ - `annotations`: up to 64 pieces of text at frequencies, with a pointer to
1825
+ where they sit. One with an `event` sends `activate` when clicked, its
1826
+ frequency as `value` and its `id` as `annotation`, and does not tune.
1827
+ - `hoverHz`, `hoverDb`: scene values the pointer writes, for a text layer.
1828
+ - `tuneStep`: a click elsewhere sends `commit` with the frequency under it,
1829
+ rounded to this many Hz. The extension decides whether to tune.
1830
+
1761
1831
  The callsign comes back upper-cased, and the field shows the extension's
1762
1832
  version once the typing is committed. A bundle carrying this declares
1763
1833
  `"api": 5`. The `k2hrc-panel-scenes` sample in the SDK has every kind, a
@@ -1773,8 +1843,8 @@ repository's dashboard README for building them.
1773
1843
  The radio host calls are manifest capabilities, refused unless declared — the
1774
1844
  same shape as `requiresLocation`, and shown to the operator at install:
1775
1845
  `"requiresRadioRead": true` allows `host.listRadios()` and `host.readRadio()`;
1776
- `"requiresRadioWrite": true` allows `host.tuneRadio()` and
1777
- `host.setRadioConnection()`, and implies read, since a command answers with the
1846
+ `"requiresRadioWrite": true` allows `host.tuneRadio()`,
1847
+ `host.setRadioConnection()` and `host.setRadioSpectrum()`, and implies read, since a command answers with the
1778
1848
  radio's state. An undeclared call rejects rather than answering null.
1779
1849
  A capability belongs to the bundle that declared it, and to the sub-extensions
1780
1850
  that bundle defines under its own key. A separately installed bundle never
@@ -1805,7 +1875,17 @@ reported state confirms it. Drivers retain their own protocol/echo handling.
1805
1875
  `host.setRadioConnection({id, selection?, connected})` starts or ends a local CAT
1806
1876
  connection using the same selection and identity checks. Acceptance starts the
1807
1877
  operation; follow reported connection state for completion. It does not switch
1808
- the hardware's mains power or initiate transmission. The reference panel samples
1878
+ the hardware's mains power or initiate transmission.
1879
+ On a host with the panadapter enabled, `readRadio()` carries `spectrum`: null
1880
+ for a radio without a scope, otherwise its settings as the radio reports them
1881
+ (`mode`, `spanHz`, `lowHz`/`highHz`, `refLevelDb`, `speed`), what it accepts
1882
+ (`spansHz`, `refMinDb`…`refStepDb`, `modes`, `speeds`) and `problem`, why it
1883
+ cannot run, in the operator's language. The settings are known only while a
1884
+ `spectrum` control has the scope running, and null otherwise.
1885
+ `host.setRadioSpectrum({id, selection?, mode?, spanHz?, refLevelDb?, speed?})`
1886
+ changes them; a span between two the radio offers is snapped to the nearer. It
1887
+ answers `stopped` when no control is running the scope: nothing is queued for
1888
+ later. The reference panel samples
1809
1889
  state once per second while visible; knob motion and LCD digit updates run locally,
1810
1890
  and tuning is sent continuously during dragging, paced by the CAT adapter. The
1811
1891
  main readout follows the requested value immediately; confirmation status distinguishes pending requests from the radio
@@ -1920,7 +2000,9 @@ its first render.
1920
2000
 
1921
2001
  The whole log is deliberately absent from `render`'s args. A panel that
1922
2002
  needs it calls `ctx.getQsos(operation.uuid)` — right for a `'qsoLogged'`
1923
- render, wrong for a per-keystroke one.
2003
+ render, wrong for a per-keystroke one. Spots are absent for the same reason:
2004
+ a panel showing them declares `'spots'` and calls `ctx.getSpots()`, which
2005
+ answers the operator's own filtered board, judged against this operation.
1924
2006
 
1925
2007
  ## ✅ `settingsPanel` — extension settings forms
1926
2008
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ham2k/extension-sdk",
3
- "version": "0.12.2",
3
+ "version": "0.12.6",
4
4
  "description": "Write extensions for the Ham2K Logger: typed hook contracts and the host API",
5
5
  "keywords": [
6
6
  "ham2k",