{"version":3,"sources":["../src/index.ts","../src/alarm.ts","../src/carryover.ts","../src/auth.ts","../src/token-cache.ts","../src/credentials.ts","../src/types.ts","../src/convert.ts","../src/firestore.ts","../src/subscribe.ts","../src/client.ts","../src/config.ts","../src/cooling.ts","../src/device-health.ts","../src/format.ts","../src/pasteurization.ts","../src/plan.ts","../src/placement.ts","../src/prediction.ts","../src/replay.ts","../src/season.ts","../src/stability.ts","../src/stall-detection.ts","../src/timeline.ts","../src/wrap-advisor.ts"],"sourcesContent":["export type { AlarmState } from \"./alarm.js\";\nexport { escalateAlarm, getChannelAlarmState, getChannelsAlarmState } from \"./alarm.js\";\nexport type { AuthSessionOptions } from \"./auth.js\";\nexport type { CarryoverInput, CarryoverResult, CarryoverSize } from \"./carryover.js\";\nexport { assessCarryover, carryoverRiseForSize } from \"./carryover.js\";\nexport { ThermoworksCloud } from \"./client.js\";\nexport type { ChannelLabelMap, DeviceEntry, StatuslineConfig } from \"./config.js\";\nexport {\n\tchannelLabelKey,\n\tDEFAULT_STATUSLINE_CONFIG,\n\tisValidChannelLabelMap,\n\tisValidDeviceEntry,\n\tisValidStatuslineConfig,\n\tMAX_CHANNEL_LABEL_LENGTH,\n\tresolveChannelLabel,\n\tsanitizeLabel,\n} from \"./config.js\";\nexport { toCelsius, toFahrenheit } from \"./convert.js\";\nexport type {\n\tCoolingAssessment,\n\tCoolingOptions,\n\tCoolingSample,\n\tCoolingStageResult,\n} from \"./cooling.js\";\nexport {\n\tassessCooling,\n\tFDA_STAGE1_END_F,\n\tFDA_STAGE1_START_F,\n\tFDA_STAGE2_END_F,\n} from \"./cooling.js\";\nexport type { Credentials } from \"./credentials.js\";\nexport {\n\tCREDENTIAL_ACCOUNT,\n\tCREDENTIAL_SERVICE,\n\tLEGACY_ACCOUNT_EMAIL,\n\tLEGACY_ACCOUNT_PASSWORD,\n\tparseCredentialBlob,\n\tresolveEnvCredentials,\n\tserializeCredentials,\n} from \"./credentials.js\";\nexport { assessDeviceHealth, isChannelStale } from \"./device-health.js\";\nexport { formatTimeAgo } from \"./format.js\";\nexport type {\n\tPasteurizationInput,\n\tPasteurizationResult,\n\tPasteurizationTable,\n\tProtein,\n} from \"./pasteurization.js\";\nexport {\n\tassessPasteurization,\n\tgetPasteurizationTable,\n\trequiredHoldMinutes,\n} from \"./pasteurization.js\";\nexport type { ProbePlacement } from \"./placement.js\";\nexport { getProbePlacements, resolveProbePlacement } from \"./placement.js\";\nexport type {\n\tCookPlan,\n\tCookPlanItem,\n\tCookPlanItemInput,\n\tMeatProfile,\n\tPlanCookOptions,\n} from \"./plan.js\";\nexport { getMeatProfiles, planCook, resolveMeatProfile } from \"./plan.js\";\nexport type { PredictionOptions, PredictionResult } from \"./prediction.js\";\nexport { predictDoneTime } from \"./prediction.js\";\nexport type { ReplayFrame, ReplayOptions, ReplayReading } from \"./replay.js\";\nexport {\n\tarchiveReadingToReplay,\n\tbuildReplaySequence,\n\thistoryReadingToReplay,\n\tnextReplayIndex,\n} from \"./replay.js\";\nexport type {\n\tDryBrinePlan,\n\tRubIngredient,\n\tRubPlan,\n\tRubRecipe,\n\tSeasoningIngredientAmount,\n\tSeasoningOptions,\n\tSeasoningPlan,\n\tWetBrinePlan,\n} from \"./season.js\";\nexport { calculateSeasoning, listRubRecipes, resolveRubRecipe } from \"./season.js\";\nexport type {\n\tPitStabilityExcursion,\n\tPitStabilityOptions,\n\tPitStabilityResult,\n} from \"./stability.js\";\nexport { analyzePitStability } from \"./stability.js\";\nexport type {\n\tRapidChangeOptions,\n\tRapidChangeResult,\n\tStallOptions,\n\tStallResult,\n} from \"./stall-detection.js\";\nexport { detectRapidChange, detectStall } from \"./stall-detection.js\";\nexport type {\n\tChannelUpdate,\n\tChannelUpdateCallback,\n\tErrorCallback,\n\tSubscription,\n\tSubscriptionOptions,\n} from \"./subscribe.js\";\nexport { createSubscription } from \"./subscribe.js\";\nexport type {\n\tCookTimeline,\n\tTimelineEvent,\n\tTimelineKind,\n\tTimelineOptions,\n} from \"./timeline.js\";\nexport { buildCookTimeline } from \"./timeline.js\";\nexport type { TokenCacheData } from \"./token-cache.js\";\nexport { invalidateTokenCache, resolveTokenCachePath } from \"./token-cache.js\";\nexport type {\n\tAccount,\n\tAccountInvite,\n\tActionResult,\n\tAlarm,\n\tAlarmSetOptions,\n\tAlarmThresholdOptions,\n\tArchive,\n\tArchiveChannel,\n\tArchiveListOptions,\n\tBigQueryRef,\n\tBillingPlan,\n\tCalibrationPoint,\n\tCalibrationRecord,\n\tDataUsage,\n\tDevice,\n\tDeviceChannel,\n\tDeviceDataUsage,\n\tDeviceEvent,\n\tDeviceFilter,\n\tDeviceGroup,\n\tDeviceHealth,\n\tDeviceHealthIssue,\n\tDeviceHistory,\n\tEventFilter,\n\tFanSettings,\n\tFirmwareInfo,\n\tGatewayInfo,\n\tHistoricalReading,\n\tMinMaxReading,\n\tNotificationSettings,\n\tRetryConfig,\n\tSearchHit,\n\tSearchOptions,\n\tSearchResult,\n\tShareResult,\n\tTemperatureCategory,\n\tTemperatureGuide,\n\tTemperatureReading,\n\tThermoworksConfig,\n\tUser,\n} from \"./types.js\";\nexport { AuthError, NetworkError, NotFoundError } from \"./types.js\";\nexport type { WrapInput, WrapRecommendation, WrapResult } from \"./wrap-advisor.js\";\nexport {\n\tassessWrap,\n\tDEFAULT_SLOW_RATE,\n\tDEFAULT_WRAP_AT_F,\n} from \"./wrap-advisor.js\";\n","import type { DeviceChannel } from \"./types.js\";\n\n/** Alarm severity state for a device or channel. */\nexport type AlarmState = \"none\" | \"low\" | \"high\";\n\n/** Determine the alarm state of a single channel. */\nexport function getChannelAlarmState(channel: DeviceChannel): AlarmState {\n\tif (channel.alarmHigh?.alarming) return \"high\";\n\tif (channel.alarmLow?.alarming) return \"low\";\n\treturn \"none\";\n}\n\n/** Determine the highest alarm state across multiple channels. */\nexport function getChannelsAlarmState(channels: DeviceChannel[]): AlarmState {\n\tfor (const ch of channels) {\n\t\tif (ch.alarmHigh?.alarming) return \"high\";\n\t}\n\tfor (const ch of channels) {\n\t\tif (ch.alarmLow?.alarming) return \"low\";\n\t}\n\treturn \"none\";\n}\n\n/** Escalate to the more severe of two alarm states. */\nexport function escalateAlarm(current: AlarmState, incoming: AlarmState): AlarmState {\n\tif (current === \"high\" || incoming === \"high\") return \"high\";\n\tif (current === \"low\" || incoming === \"low\") return \"low\";\n\treturn \"none\";\n}\n","// ─── Carryover / Pull-Early Prediction ───────────────────────────────────────\n\n/**\n * Relative cut size, used to pick a default carryover rise when the caller does\n * not know the exact number. Small cuts (steaks, chops) hold little residual\n * heat; large cuts (brisket, pork butt, roasts) hold a lot.\n */\nexport type CarryoverSize = \"small\" | \"medium\" | \"large\";\n\n/** Default carryover rise in Fahrenheit for each cut size. */\nconst RISE_BY_SIZE: Record<CarryoverSize, number> = {\n\tsmall: 3,\n\tmedium: 6,\n\tlarge: 10,\n};\n\n/** Input for a carryover assessment. All temperatures are in Fahrenheit. */\nexport interface CarryoverInput {\n\t/** Current probe temperature. */\n\tcurrentTempF: number;\n\t/** Desired final temperature after resting. */\n\ttargetTempF: number;\n\t/** Expected carryover rise while the food rests, in Fahrenheit. */\n\triseF: number;\n}\n\n/** Result of a carryover assessment. All temperatures are in Fahrenheit. */\nexport interface CarryoverResult {\n\tcurrentTempF: number;\n\ttargetTempF: number;\n\triseF: number;\n\t/** Temperature to pull at so the food lands on the target after resting. */\n\tpullTempF: number;\n\t/** Final temperature the food is projected to reach if pulled right now. */\n\tprojectedFinalF: number;\n\t/** Degrees left before reaching the pull temperature. Negative once past it. */\n\tremainingF: number;\n\t/** True when the current temperature is at or above the pull temperature. */\n\tpullNow: boolean;\n\t/** True when pulling now would overshoot the target after resting. */\n\tovershoot: boolean;\n}\n\n/**\n * Return the default carryover rise in Fahrenheit for a cut size. Use this when\n * the caller picks a size preset instead of giving an exact rise.\n */\nexport function carryoverRiseForSize(size: CarryoverSize): number {\n\treturn RISE_BY_SIZE[size];\n}\n\n/**\n * Work out when to pull food off the heat so that carryover cooking lands it on\n * the target temperature after resting. Meat keeps rising after it comes off the\n * heat, so pulling at the target temperature overshoots. This computes the lower\n * pull temperature and how far the current reading is from it.\n *\n * @param input - Current temperature, desired final temperature, and expected rise.\n * @returns A carryover result with the pull temperature and remaining degrees.\n */\nexport function assessCarryover(input: CarryoverInput): CarryoverResult {\n\tconst { currentTempF, targetTempF } = input;\n\tconst riseF = Math.max(0, input.riseF);\n\n\tconst pullTempF = targetTempF - riseF;\n\tconst projectedFinalF = currentTempF + riseF;\n\tconst remainingF = pullTempF - currentTempF;\n\tconst pullNow = currentTempF >= pullTempF;\n\n\treturn {\n\t\tcurrentTempF,\n\t\ttargetTempF,\n\t\triseF,\n\t\tpullTempF,\n\t\tprojectedFinalF,\n\t\tremainingF,\n\t\tpullNow,\n\t\tovershoot: projectedFinalF > targetTempF,\n\t};\n}\n","import { setTimeout as delay } from \"node:timers/promises\";\nimport { Agent, request as undiciRequest } from \"undici\";\nimport {\n\treadTokenCache,\n\tresolveTokenCachePath,\n\ttype TokenCacheData,\n\twriteTokenCache,\n} from \"./token-cache.js\";\nimport { AuthError, NetworkError, type RetryConfig } from \"./types.js\";\n\nconst IDENTITY_HOST = \"https://identitytoolkit.googleapis.com\";\nconst TOKEN_HOST = \"https://securetoken.googleapis.com\";\nconst FIREBASE_HOST = \"https://firebase.googleapis.com\";\nconst FIRESTORE_HOST = \"https://firestore.googleapis.com\";\nconst FUNCTIONS_HOST = \"https://us-central1-thermoworks-cloud-production.cloudfunctions.net\";\n\n// Firebase client-side API key (public identifier, not a secret).\n// Security is enforced by Firebase Security Rules server-side.\nconst DEFAULT_API_KEY = \"AIzaSyCf079iccUFc1k7VHdGXng22zXDy8Y3KEY\";\nconst DEFAULT_APP_ID = \"1:78998049458:web:b41e9d405d8c7de95eefab\";\nconst REFERER = \"https://cloud.thermoworks.com/\";\n\n// Refresh tokens 60 seconds before they actually expire\nconst EXPIRY_BUFFER_MS = 60_000;\n\nconst DEFAULT_MAX_RETRIES = 3;\nconst DEFAULT_BASE_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 30_000;\n\n/** HTTP status codes that indicate transient failures worth retrying. */\nconst RETRYABLE_STATUS_CODES = new Set([429, 503]);\n\ninterface TokenState {\n\taccessToken: string;\n\trefreshToken: string;\n\tuserId: string;\n\texpiresAt: number;\n}\n\ninterface FirebaseWebConfig {\n\tprojectId: string;\n}\n\ninterface HttpResponse {\n\tok: boolean;\n\tstatus: number;\n\theaders: Record<string, string | string[] | undefined>;\n\tjson(): Promise<unknown>;\n\ttext(): Promise<string>;\n}\n\n/**\n * Compute the retry delay using exponential backoff with full jitter.\n * Formula: random(0, min(baseDelay * 2^attempt, maxDelay))\n * If a Retry-After header is present, use that as a floor instead.\n */\nexport function computeRetryDelay(\n\tattempt: number,\n\tbaseDelayMs: number,\n\tmaxDelayMs: number,\n\tretryAfterHeader?: string | string[] | null,\n): number {\n\t// Parse Retry-After header if present (seconds or HTTP-date)\n\tlet retryAfterMs = 0;\n\tif (retryAfterHeader) {\n\t\tconst headerValue = Array.isArray(retryAfterHeader) ? retryAfterHeader[0] : retryAfterHeader;\n\t\tif (headerValue) {\n\t\t\tconst seconds = Number(headerValue);\n\t\t\tif (Number.isFinite(seconds) && seconds > 0) {\n\t\t\t\tretryAfterMs = seconds * 1000;\n\t\t\t} else {\n\t\t\t\t// Try parsing as HTTP-date\n\t\t\t\tconst date = Date.parse(headerValue);\n\t\t\t\tif (!Number.isNaN(date)) {\n\t\t\t\t\tretryAfterMs = Math.max(0, date - Date.now());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Exponential backoff: baseDelay * 2^attempt, capped at maxDelay\n\tconst exponentialDelay = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);\n\n\t// Full jitter on the exponential component: uniform random in [0, exponentialDelay).\n\tconst jitteredDelay = Math.random() * exponentialDelay;\n\n\t// Honor Retry-After as a hard floor: never retry sooner than the server asked,\n\t// even after jitter. Still capped at maxDelay.\n\treturn Math.min(Math.max(jitteredDelay, retryAfterMs), maxDelayMs);\n}\n\nfunction isRetryableStatus(statusCode: number): boolean {\n\treturn RETRYABLE_STATUS_CODES.has(statusCode) || statusCode >= 500;\n}\n\n// Maximum response body size (10 MB) to prevent OOM from malicious/corrupted responses\nconst MAX_RESPONSE_BYTES = 10 * 1024 * 1024;\n\nasync function httpRequest(\n\turl: string,\n\toptions: {\n\t\tmethod?: string;\n\t\theaders?: Record<string, string>;\n\t\tbody?: string;\n\t},\n\tagent: Agent,\n\tretry?: RetryConfig,\n): Promise<HttpResponse> {\n\tconst maxRetries = retry?.maxRetries ?? DEFAULT_MAX_RETRIES;\n\tconst baseDelayMs = retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;\n\tconst maxDelayMs = retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n\n\tlet lastError: Error | undefined;\n\tfor (let attempt = 0; attempt <= maxRetries; attempt++) {\n\t\ttry {\n\t\t\tconst {\n\t\t\t\tstatusCode,\n\t\t\t\theaders: responseHeaders,\n\t\t\t\tbody,\n\t\t\t} = await undiciRequest(url, {\n\t\t\t\tmethod: options.method ?? \"GET\",\n\t\t\t\theaders: options.headers,\n\t\t\t\tbody: options.body,\n\t\t\t\tdispatcher: agent,\n\t\t\t});\n\n\t\t\t// Guard against oversized responses to prevent OOM\n\t\t\tconst contentLength = responseHeaders[\"content-length\"];\n\t\t\tif (contentLength && Number(contentLength) > MAX_RESPONSE_BYTES) {\n\t\t\t\tawait body.dump();\n\t\t\t\tthrow new NetworkError(\"Response too large\");\n\t\t\t}\n\n\t\t\tconst text = await body.text();\n\t\t\tif (text.length > MAX_RESPONSE_BYTES) {\n\t\t\t\tthrow new NetworkError(\"Response too large\");\n\t\t\t}\n\n\t\t\tconst normalizedHeaders: Record<string, string | string[] | undefined> = {};\n\t\t\tif (responseHeaders && typeof responseHeaders === \"object\") {\n\t\t\t\tfor (const [key, value] of Object.entries(responseHeaders)) {\n\t\t\t\t\tnormalizedHeaders[key.toLowerCase()] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst response: HttpResponse = {\n\t\t\t\tok: statusCode >= 200 && statusCode < 300,\n\t\t\t\tstatus: statusCode,\n\t\t\t\theaders: normalizedHeaders,\n\t\t\t\tjson: async () => JSON.parse(text),\n\t\t\t\ttext: async () => text,\n\t\t\t};\n\n\t\t\t// Retry on transient status codes (429, 5xx)\n\t\t\tif (isRetryableStatus(statusCode) && attempt < maxRetries) {\n\t\t\t\tlastError = new NetworkError(`Server error: HTTP ${statusCode}`, statusCode);\n\t\t\t\tconst retryAfter = normalizedHeaders[\"retry-after\"];\n\t\t\t\tconst retryAfterValue = Array.isArray(retryAfter) ? retryAfter[0] : retryAfter;\n\t\t\t\tawait delay(computeRetryDelay(attempt, baseDelayMs, maxDelayMs, retryAfterValue));\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\treturn response;\n\t\t} catch (err) {\n\t\t\tlastError = new NetworkError(err instanceof Error ? err.message : \"Network request failed\");\n\t\t\tif (attempt < maxRetries) {\n\t\t\t\tawait delay(computeRetryDelay(attempt, baseDelayMs, maxDelayMs));\n\t\t\t}\n\t\t}\n\t}\n\tthrow lastError ?? new NetworkError(\"Network request failed\");\n}\n\nfunction parseExpiresIn(value: unknown): number {\n\tconst seconds = Number(value);\n\tif (!Number.isFinite(seconds) || seconds < 0) {\n\t\tthrow new AuthError(\"Invalid expires_in from server\", \"INVALID_TOKEN_RESPONSE\");\n\t}\n\treturn seconds;\n}\n\nasync function safeJsonParse(response: HttpResponse, context: string): Promise<unknown> {\n\ttry {\n\t\treturn await response.json();\n\t} catch {\n\t\tthrow new NetworkError(`Invalid JSON in ${context}`);\n\t}\n}\n\nexport interface AuthSession {\n\trequest(method: string, path: string, body?: unknown): Promise<HttpResponse>;\n\tcallFunction(name: string, data: unknown): Promise<unknown>;\n\tgetUserId(): string;\n\tclose(): void;\n}\n\nexport interface AuthSessionOptions {\n\temail: string;\n\tpassword: string;\n\tapiKey?: string;\n\tappId?: string;\n\t/** Path to token cache file, or true for default path, or false/undefined to disable. */\n\ttokenCachePath?: string | boolean;\n\t/** Retry configuration for transient failures. */\n\tretry?: RetryConfig;\n}\n\nexport async function createAuthSession(\n\temailOrOptions: string | AuthSessionOptions,\n\tpassword?: string,\n\tapiKey?: string,\n\tappId?: string,\n\tretryArg?: RetryConfig,\n): Promise<AuthSession> {\n\t// Support both legacy positional args and new options object\n\tconst opts: AuthSessionOptions =\n\t\ttypeof emailOrOptions === \"string\"\n\t\t\t? { email: emailOrOptions, password: password ?? \"\", apiKey, appId }\n\t\t\t: emailOrOptions;\n\n\tconst retry = opts.retry ?? retryArg;\n\tconst key = opts.apiKey ?? DEFAULT_API_KEY;\n\tconst app = opts.appId ?? DEFAULT_APP_ID;\n\n\tconst agent = new Agent({\n\t\tkeepAliveTimeout: 60_000,\n\t\tconnections: 10,\n\t});\n\n\tlet config: FirebaseWebConfig;\n\tlet token: TokenState;\n\n\tconst cachePath = resolveCacheSetting(opts.tokenCachePath);\n\n\ttry {\n\t\tconst cached = cachePath ? await tryRestoreFromCache(cachePath, key, agent) : null;\n\n\t\tif (cached) {\n\t\t\tconfig = { projectId: cached.projectId };\n\t\t\ttoken = cached.token;\n\t\t} else {\n\t\t\tconfig = await fetchWebConfig(key, app, agent, retry);\n\t\t\ttoken = await login(opts.email, opts.password, key, agent, retry);\n\t\t\tif (cachePath) {\n\t\t\t\tawait persistToCache(cachePath, token, config.projectId);\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tagent.close();\n\t\tthrow err;\n\t}\n\n\tconst baseUrl = `${FIRESTORE_HOST}/v1/projects/${config.projectId}/databases/(default)`;\n\tlet refreshPromise: Promise<TokenState> | null = null;\n\n\tfunction isTokenValid(): boolean {\n\t\treturn Date.now() < token.expiresAt - EXPIRY_BUFFER_MS;\n\t}\n\n\tasync function ensureValidToken(): Promise<string> {\n\t\tif (!isTokenValid()) {\n\t\t\tif (!refreshPromise) {\n\t\t\t\trefreshPromise = (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst t = await refreshAccessToken(token.refreshToken, key, agent, retry);\n\t\t\t\t\t\ttoken = t;\n\t\t\t\t\t\tif (cachePath) {\n\t\t\t\t\t\t\tawait persistToCache(cachePath, t, config.projectId).catch(() => {});\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn t;\n\t\t\t\t\t} finally {\n\t\t\t\t\t\trefreshPromise = null;\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t}\n\t\t\tawait refreshPromise;\n\t\t}\n\t\treturn token.accessToken;\n\t}\n\n\treturn {\n\t\tasync request(method: string, path: string, body?: unknown): Promise<HttpResponse> {\n\t\t\tconst accessToken = await ensureValidToken();\n\t\t\tconst separator = path.includes(\"?\") ? \"&\" : \"?\";\n\t\t\tconst url = `${baseUrl}/${path}${separator}key=${key}`;\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\tauthorization: `Bearer ${accessToken}`,\n\t\t\t};\n\n\t\t\tif (body !== undefined) {\n\t\t\t\theaders[\"content-type\"] = \"application/json\";\n\t\t\t}\n\n\t\t\tconst response = await httpRequest(\n\t\t\t\turl,\n\t\t\t\t{\n\t\t\t\t\tmethod,\n\t\t\t\t\theaders,\n\t\t\t\t\tbody: body !== undefined ? JSON.stringify(body) : undefined,\n\t\t\t\t},\n\t\t\t\tagent,\n\t\t\t\tretry,\n\t\t\t);\n\n\t\t\tif (!response.ok && response.status !== 404) {\n\t\t\t\tthrow new NetworkError(`HTTP ${response.status}`, response.status);\n\t\t\t}\n\n\t\t\treturn response;\n\t\t},\n\n\t\tasync callFunction(name: string, data: unknown): Promise<unknown> {\n\t\t\tif (!name || !/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {\n\t\t\t\tthrow new Error(`Invalid function name: ${name}`);\n\t\t\t}\n\t\t\tconst accessToken = await ensureValidToken();\n\t\t\tconst url = `${FUNCTIONS_HOST}/${name}`;\n\t\t\tconst response = await httpRequest(\n\t\t\t\turl,\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tauthorization: `Bearer ${accessToken}`,\n\t\t\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify({ data }),\n\t\t\t\t},\n\t\t\t\tagent,\n\t\t\t\tretry,\n\t\t\t);\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new NetworkError(\n\t\t\t\t\t`Cloud function call failed: HTTP ${response.status}`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst result = await response.json();\n\t\t\tif (result && typeof result === \"object\" && \"result\" in result) {\n\t\t\t\treturn (result as { result: unknown }).result;\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\n\t\tgetUserId(): string {\n\t\t\treturn token.userId;\n\t\t},\n\n\t\tclose() {\n\t\t\tagent.close();\n\t\t},\n\t};\n}\n\nfunction resolveCacheSetting(setting: string | boolean | undefined): string | null {\n\tif (setting === false || setting === undefined) return null;\n\tif (setting === true) return resolveTokenCachePath();\n\treturn resolveTokenCachePath(setting);\n}\n\nasync function tryRestoreFromCache(\n\tcachePath: string,\n\tapiKey: string,\n\tagent: Agent,\n): Promise<{ token: TokenState; projectId: string } | null> {\n\tconst cached = await readTokenCache(cachePath);\n\tif (!cached) return null;\n\n\tconst expiresAt = Date.parse(cached.expiresAt);\n\tif (Number.isNaN(expiresAt)) return null;\n\n\t// Token is still valid - use directly\n\tif (Date.now() < expiresAt - EXPIRY_BUFFER_MS) {\n\t\treturn {\n\t\t\ttoken: {\n\t\t\t\taccessToken: cached.idToken,\n\t\t\t\trefreshToken: cached.refreshToken,\n\t\t\t\tuserId: cached.userId,\n\t\t\t\texpiresAt,\n\t\t\t},\n\t\t\tprojectId: cached.projectId,\n\t\t};\n\t}\n\n\t// Token expired - try refresh\n\ttry {\n\t\tconst refreshed = await refreshAccessToken(cached.refreshToken, apiKey, agent);\n\t\tawait persistToCache(cachePath, refreshed, cached.projectId);\n\t\treturn { token: refreshed, projectId: cached.projectId };\n\t} catch {\n\t\t// Refresh failed - fall back to full re-auth (caller handles this via null return)\n\t\treturn null;\n\t}\n}\n\nasync function persistToCache(\n\tcachePath: string,\n\ttoken: TokenState,\n\tprojectId: string,\n): Promise<void> {\n\tconst data: TokenCacheData = {\n\t\tidToken: token.accessToken,\n\t\trefreshToken: token.refreshToken,\n\t\tuserId: token.userId,\n\t\texpiresAt: new Date(token.expiresAt).toISOString(),\n\t\tprojectId,\n\t};\n\tawait writeTokenCache(cachePath, data);\n}\n\nasync function fetchWebConfig(\n\tapiKey: string,\n\tappId: string,\n\tagent: Agent,\n\tretry?: RetryConfig,\n): Promise<FirebaseWebConfig> {\n\tconst url = `${FIREBASE_HOST}/v1alpha/projects/-/apps/${appId}/webConfig`;\n\tconst response = await httpRequest(\n\t\turl,\n\t\t{\n\t\t\theaders: {\n\t\t\t\taccept: \"application/json\",\n\t\t\t\t\"x-goog-api-key\": apiKey,\n\t\t\t\treferer: REFERER,\n\t\t\t},\n\t\t},\n\t\tagent,\n\t\tretry,\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new NetworkError(\"Failed to fetch Firebase web config\", response.status);\n\t}\n\n\treturn (await safeJsonParse(response, \"Firebase web config\")) as FirebaseWebConfig;\n}\n\nasync function login(\n\temail: string,\n\tpassword: string,\n\tapiKey: string,\n\tagent: Agent,\n\tretry?: RetryConfig,\n): Promise<TokenState> {\n\tconst url = `${IDENTITY_HOST}/v1/accounts:signInWithPassword?key=${apiKey}`;\n\tconst response = await httpRequest(\n\t\turl,\n\t\t{\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\treferer: REFERER,\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\temail,\n\t\t\t\tpassword,\n\t\t\t\treturnSecureToken: true,\n\t\t\t}),\n\t\t},\n\t\tagent,\n\t\tretry,\n\t);\n\n\tif (!response.ok) {\n\t\tconst errorData = (await response.json().catch(() => null)) as {\n\t\t\terror?: { message?: string };\n\t\t} | null;\n\t\tconst reason = errorData?.error?.message ?? \"UNKNOWN\";\n\t\tthrow new AuthError(`Authentication failed: ${reason}`, reason);\n\t}\n\n\tconst data = (await safeJsonParse(response, \"login response\")) as {\n\t\tidToken: string;\n\t\trefreshToken: string;\n\t\tlocalId: string;\n\t\texpiresIn: string;\n\t};\n\n\treturn {\n\t\taccessToken: data.idToken,\n\t\trefreshToken: data.refreshToken,\n\t\tuserId: data.localId,\n\t\texpiresAt: Date.now() + parseExpiresIn(data.expiresIn) * 1000,\n\t};\n}\n\nasync function refreshAccessToken(\n\trefreshToken: string,\n\tapiKey: string,\n\tagent: Agent,\n\tretry?: RetryConfig,\n): Promise<TokenState> {\n\tconst url = `${TOKEN_HOST}/v1/token?key=${apiKey}`;\n\tconst response = await httpRequest(\n\t\turl,\n\t\t{\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\treferer: REFERER,\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tgrant_type: \"refresh_token\",\n\t\t\t\trefresh_token: refreshToken,\n\t\t\t}),\n\t\t},\n\t\tagent,\n\t\tretry,\n\t);\n\n\tif (!response.ok) {\n\t\tthrow new AuthError(\"Token refresh failed\", \"TOKEN_REFRESH_FAILED\");\n\t}\n\n\tconst data = (await safeJsonParse(response, \"token refresh\")) as {\n\t\tid_token: string;\n\t\trefresh_token: string;\n\t\tuser_id: string;\n\t\texpires_in: string;\n\t};\n\n\treturn {\n\t\taccessToken: data.id_token,\n\t\trefreshToken: data.refresh_token,\n\t\tuserId: data.user_id,\n\t\texpiresAt: Date.now() + parseExpiresIn(data.expires_in) * 1000,\n\t};\n}\n","import { lstat, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { homedir, tmpdir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { CREDENTIAL_SERVICE } from \"./credentials.js\";\n\nconst DEFAULT_CACHE_DIR = join(homedir(), \".thermoworks\");\nconst DEFAULT_CACHE_PATH = join(DEFAULT_CACHE_DIR, \".token-cache.json\");\nconst TOKEN_CACHE_ACCOUNT = \"token-cache\";\n\n/** Cached token data persisted to disk. */\nexport interface TokenCacheData {\n\treadonly idToken: string;\n\treadonly refreshToken: string;\n\treadonly userId: string;\n\t/** ISO 8601 timestamp of token expiry. */\n\treadonly expiresAt: string;\n\t/** Firebase project ID (avoids fetching web config on cache hit). */\n\treadonly projectId: string;\n}\n\ninterface KeytarLike {\n\tgetPassword(service: string, account: string): Promise<string | null>;\n\tsetPassword(service: string, account: string, password: string): Promise<void>;\n\tdeletePassword(service: string, account: string): Promise<boolean>;\n}\n\nlet _keytar: KeytarLike | null | undefined;\n\nasync function getKeytar(): Promise<KeytarLike | null> {\n\tif (_keytar !== undefined) return _keytar;\n\ttry {\n\t\t// Dynamic import - @github/keytar is an optional peer dependency\n\t\t// @ts-expect-error: keytar may not be installed\n\t\tconst mod = await import(\"@github/keytar\");\n\t\t_keytar = mod.default as KeytarLike;\n\t} catch {\n\t\t_keytar = null;\n\t}\n\treturn _keytar;\n}\n\n/** Resolve the token cache file path from user input or default. */\nexport function resolveTokenCachePath(userPath?: string): string {\n\tif (userPath != null) {\n\t\t// Reject paths that could escape the intended directory\n\t\tif (userPath.includes(\"..\") || userPath.startsWith(\"\\\\\\\\\")) {\n\t\t\tthrow new Error(\"tokenCachePath must not contain '..' or UNC paths\");\n\t\t}\n\t\t// Ensure the resolved path is within the user's home directory or temp directory\n\t\tconst resolved = resolve(userPath);\n\t\tconst home = homedir();\n\t\tconst tmp = tmpdir();\n\t\tconst isWithinHome =\n\t\t\tresolved.startsWith(`${home}/`) || resolved.startsWith(`${home}\\\\`) || resolved === home;\n\t\tconst isWithinTmp =\n\t\t\tresolved.startsWith(`${tmp}/`) || resolved.startsWith(`${tmp}\\\\`) || resolved === tmp;\n\t\tif (!isWithinHome && !isWithinTmp) {\n\t\t\tthrow new Error(\"tokenCachePath must be within the user home directory\");\n\t\t}\n\t}\n\treturn userPath ?? DEFAULT_CACHE_PATH;\n}\n\n/**\n * Read cached tokens. Tries OS keychain first, falls back to file.\n * Returns null if cache is missing, corrupt, or malformed.\n */\nexport async function readTokenCache(cachePath: string): Promise<TokenCacheData | null> {\n\t// Try OS keychain first (secure storage)\n\ttry {\n\t\tconst keytar = await getKeytar();\n\t\tif (keytar) {\n\t\t\tconst blob = await keytar.getPassword(CREDENTIAL_SERVICE, TOKEN_CACHE_ACCOUNT);\n\t\t\tif (blob) {\n\t\t\t\tconst parsed: unknown = JSON.parse(blob);\n\t\t\t\tif (isValidCacheData(parsed)) return parsed;\n\t\t\t}\n\t\t}\n\t} catch (err) {\n\t\tprocess.emitWarning(\n\t\t\t`Keychain read failed: ${err instanceof Error ? err.message : \"unknown error\"}`,\n\t\t\t\"ThermoWorksSecurityWarning\",\n\t\t);\n\t}\n\n\t// File fallback for headless/CI environments\n\ttry {\n\t\tconst raw = await readFile(cachePath, \"utf8\");\n\t\tconst parsed: unknown = JSON.parse(raw);\n\t\tif (!isValidCacheData(parsed)) {\n\t\t\treturn null;\n\t\t}\n\t\treturn parsed;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Write token data to cache. Stores in OS keychain when available,\n * falls back to file with restricted permissions.\n */\nexport async function writeTokenCache(cachePath: string, data: TokenCacheData): Promise<void> {\n\tconst blob = JSON.stringify(data);\n\n\t// Try OS keychain first (secure storage)\n\ttry {\n\t\tconst keytar = await getKeytar();\n\t\tif (keytar) {\n\t\t\tawait keytar.setPassword(CREDENTIAL_SERVICE, TOKEN_CACHE_ACCOUNT, blob);\n\t\t\t// Remove any stale file cache when keychain is available\n\t\t\ttry {\n\t\t\t\tawait rm(cachePath, { force: true });\n\t\t\t} catch {\n\t\t\t\t// Non-fatal\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t} catch (err) {\n\t\tprocess.emitWarning(\n\t\t\t`Keychain unavailable, using plaintext file cache at ${cachePath}. ` +\n\t\t\t\t`Tokens are protected by file permissions (0o600) but not encrypted at rest. ` +\n\t\t\t\t`Install @github/keytar for secure OS keychain storage. ` +\n\t\t\t\t`(${err instanceof Error ? err.message : \"unknown error\"})`,\n\t\t\t\"ThermoWorksSecurityWarning\",\n\t\t);\n\t}\n\n\t// File fallback with restricted permissions (plaintext - see warning above)\n\ttry {\n\t\ttry {\n\t\t\tconst stat = await lstat(cachePath);\n\t\t\tif (stat.isSymbolicLink()) {\n\t\t\t\tprocess.emitWarning(\n\t\t\t\t\t\"Token cache path is a symlink, skipping write for security\",\n\t\t\t\t\t\"ThermoWorksSecurityWarning\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\t// File doesn't exist yet, safe to create\n\t\t}\n\t\tconst dir = dirname(cachePath);\n\t\tawait mkdir(dir, { recursive: true, mode: 0o700 });\n\t\tawait writeFile(cachePath, `${blob}\\n`, {\n\t\t\tencoding: \"utf8\",\n\t\t\tmode: 0o600,\n\t\t});\n\t} catch (err) {\n\t\tprocess.emitWarning(\n\t\t\t`Token cache write failed: ${err instanceof Error ? err.message : \"unknown error\"}`,\n\t\t\t\"ThermoWorksSecurityWarning\",\n\t\t);\n\t}\n}\n\n/** Delete the token cache from all storage backends. */\nexport async function invalidateTokenCache(cachePath?: string): Promise<void> {\n\t// Clear from OS keychain\n\ttry {\n\t\tconst keytar = await getKeytar();\n\t\tif (keytar) {\n\t\t\tawait keytar.deletePassword(CREDENTIAL_SERVICE, TOKEN_CACHE_ACCOUNT);\n\t\t}\n\t} catch {\n\t\t// Non-fatal\n\t}\n\n\t// Clear file cache\n\tconst resolved = resolveTokenCachePath(cachePath);\n\ttry {\n\t\tawait rm(resolved, { force: true });\n\t} catch {\n\t\t// Non-fatal\n\t}\n}\n\nfunction isValidCacheData(value: unknown): value is TokenCacheData {\n\tif (typeof value !== \"object\" || value === null) return false;\n\tconst obj = value as Record<string, unknown>;\n\treturn (\n\t\ttypeof obj.idToken === \"string\" &&\n\t\ttypeof obj.refreshToken === \"string\" &&\n\t\ttypeof obj.userId === \"string\" &&\n\t\ttypeof obj.expiresAt === \"string\" &&\n\t\ttypeof obj.projectId === \"string\" &&\n\t\tobj.idToken.length > 0 &&\n\t\tobj.refreshToken.length > 0 &&\n\t\tobj.userId.length > 0 &&\n\t\tobj.projectId.length > 0 &&\n\t\t!Number.isNaN(Date.parse(obj.expiresAt))\n\t);\n}\n","export const CREDENTIAL_SERVICE = \"thermoworks\";\nexport const CREDENTIAL_ACCOUNT = \"credentials\";\nexport const LEGACY_ACCOUNT_EMAIL = \"email\";\nexport const LEGACY_ACCOUNT_PASSWORD = \"password\";\n\nexport interface Credentials {\n\treadonly email: string;\n\treadonly password: string;\n}\n\nexport function parseCredentialBlob(blob: string): Credentials | null {\n\ttry {\n\t\tconst parsed = JSON.parse(blob) as { email?: string; password?: string };\n\t\tif (parsed.email && parsed.password) {\n\t\t\treturn { email: parsed.email, password: parsed.password };\n\t\t}\n\t} catch {\n\t\t// Corrupted blob\n\t}\n\treturn null;\n}\n\nexport function serializeCredentials(email: string, password: string): string {\n\treturn JSON.stringify({ email, password });\n}\n\nexport function resolveEnvCredentials(): Credentials | null {\n\tconst email = process.env.THERMOWORKS_EMAIL;\n\tconst password = process.env.THERMOWORKS_PASSWORD;\n\treturn email && password ? { email, password } : null;\n}\n","/** Configuration for automatic retry with exponential backoff. */\nexport interface RetryConfig {\n\t/** Maximum number of retry attempts (default 3). */\n\tmaxRetries?: number;\n\t/** Base delay in milliseconds before exponential increase (default 1000). */\n\tbaseDelayMs?: number;\n\t/** Maximum delay in milliseconds between retries (default 30000). */\n\tmaxDelayMs?: number;\n}\n\n/** Configuration for the ThermoWorks Cloud client. */\nexport interface ThermoworksConfig {\n\t/** ThermoWorks Cloud account email address. */\n\temail: string;\n\t/** ThermoWorks Cloud account password. */\n\tpassword: string;\n\t/** Override the default Firebase API key. */\n\tapiKey?: string;\n\t/** Override the default Firebase app ID. */\n\tappId?: string;\n\t/** Retry configuration for transient failures (429, 503, network errors). */\n\tretry?: RetryConfig;\n\t/**\n\t * Path to the token cache file for persisting auth tokens across sessions.\n\t * Reduces authentication overhead by reusing valid tokens instead of\n\t * performing a full sign-in on every client instantiation.\n\t *\n\t * Defaults to `~/.thermoworks/.token-cache.json` if set to `true` or a string path.\n\t * Set to `false` or omit to disable token caching.\n\t */\n\ttokenCachePath?: string | boolean;\n}\n\n// ─── Device ──────────────────────────────────────────────────────────────────\n\n/** A ThermoWorks device (Node, Smoke, Signals, RFX Gateway, etc). */\nexport interface Device {\n\treadonly serial: string;\n\treadonly deviceId: string | null;\n\treadonly label: string | null;\n\treadonly type: string | null;\n\treadonly device: string | null;\n\treadonly status: string | null;\n\treadonly battery: number | null;\n\treadonly batteryState: string | null;\n\treadonly wifiStrength: number | null;\n\treadonly firmware: string | null;\n\treadonly color: string | null;\n\treadonly thumbnail: string | null;\n\treadonly deviceDisplayUnits: string | null;\n\treadonly iotDeviceId: string | null;\n\treadonly iotCoreDeviceBlocked: boolean | null;\n\treadonly recordingIntervalInSeconds: number | null;\n\treadonly transmitIntervalInSeconds: number | null;\n\treadonly readInterval: number | null;\n\treadonly heartbeatInterval: number | null;\n\treadonly temperatureDeltaTrigger: number | null;\n\treadonly pendingLoad: boolean | null;\n\treadonly batteryAlertSent: boolean | null;\n\treadonly lastSeen: Date | null;\n\treadonly lastTelemetrySaved: Date | null;\n\treadonly latestReading: Date | null;\n\treadonly lastWifiConnection: Date | null;\n\treadonly lastBluetoothConnection: Date | null;\n\treadonly sessionStart: Date | null;\n\treadonly sessionLabel: string | null;\n\treadonly lastArchive: Date | null;\n\treadonly lastPurged: Date | null;\n\treadonly assignedToAccountOn: Date | null;\n\treadonly accountId: string | null;\n\treadonly notes: string | null;\n\treadonly public: boolean | null;\n\treadonly publicLink: string | null;\n\treadonly searModeEnabled: boolean | null;\n\treadonly showSensorChannels: boolean | null;\n\treadonly ringColors: string[] | null;\n\treadonly gateway: GatewayInfo | null;\n\treadonly fan: FanSettings | null;\n\treadonly bigQuery: BigQueryRef | null;\n}\n\n/** Gateway connection info for RFX wireless devices. */\nexport interface GatewayInfo {\n\treadonly gatewayId: string | null;\n\treadonly rssi: number | null;\n\treadonly lastSeen: Date | null;\n\treadonly switchedAt: Date | null;\n\treadonly lastPacketId: number | null;\n}\n\n/** BBQ fan/blower controller settings. */\nexport interface FanSettings {\n\treadonly connected: boolean;\n\treadonly connection: boolean;\n\treadonly setTemp: number | null;\n\treadonly fanChannel: string | null;\n\treadonly state: number | null;\n}\n\n/** Reference to historical data in BigQuery. */\nexport interface BigQueryRef {\n\treadonly datasetId: string;\n\treadonly tableId: string;\n}\n\n// ─── Channel ─────────────────────────────────────────────────────────────────\n\n/** A channel reading from a device (temperature or humidity sensor). */\nexport interface DeviceChannel {\n\treadonly value: number | null;\n\treadonly units: string | null;\n\treadonly label: string | null;\n\treadonly status: string | null;\n\treadonly type: string | null;\n\treadonly number: string | null;\n\treadonly enabled: boolean | null;\n\treadonly color: string | null;\n\treadonly lastSeen: Date | null;\n\treadonly lastTelemetrySaved: Date | null;\n\treadonly lastEventId: string | null;\n\treadonly showAvgTemp: boolean | null;\n\treadonly estimatedAlarmStatus: string | null;\n\treadonly rateOfChange: number | null;\n\treadonly rateOfChangeUnit: string | null;\n\treadonly alarmHigh: Alarm | null;\n\treadonly alarmLow: Alarm | null;\n\treadonly minimum: MinMaxReading | null;\n\treadonly maximum: MinMaxReading | null;\n}\n\n/** An alarm threshold on a device channel. */\nexport interface Alarm {\n\treadonly enabled: boolean;\n\treadonly alarming: boolean;\n\treadonly muted: boolean | null;\n\treadonly value: number | null;\n\treadonly units: string | null;\n\treadonly lastNotified: Date | null;\n}\n\n/** A minimum or maximum reading record. */\nexport interface MinMaxReading {\n\treadonly value: number | null;\n\treadonly units: string | null;\n\treadonly date: Date | null;\n}\n\n// ─── User ────────────────────────────────────────────────────────────────────\n\n/** User account information. */\nexport interface User {\n\treadonly userId: string;\n\treadonly accountId: string | null;\n\treadonly email: string | null;\n\treadonly displayName: string | null;\n\treadonly timeZone: string | null;\n\treadonly preferredUnits: string | null;\n\treadonly locale: string | null;\n\treadonly photoUrl: string | null;\n\treadonly use24Time: boolean | null;\n\treadonly lastLogin: Date | null;\n\treadonly appVersion: string | null;\n\treadonly accountRoles: Record<string, boolean> | null;\n\treadonly roles: Record<string, boolean> | null;\n\treadonly notificationSettings: NotificationSettings | null;\n}\n\n/** User notification preferences. */\nexport interface NotificationSettings {\n\treadonly enabled: boolean;\n\treadonly continuousAlerts: boolean;\n\treadonly emailNotification: boolean;\n\treadonly smsNotification: boolean;\n\treadonly deviceNotification: boolean;\n}\n\n// ─── Account ─────────────────────────────────────────────────────────────────\n\n/** Account metadata. */\nexport interface Account {\n\treadonly accountId: string;\n\treadonly name: string | null;\n\treadonly type: string | null;\n\treadonly createdOn: Date | null;\n\treadonly exportVersion: number | null;\n}\n\n// ─── Events ──────────────────────────────────────────────────────────────────\n\n/** A device event (alarm, status change, alert). */\nexport interface DeviceEvent {\n\treadonly id: string;\n\treadonly eventType: string;\n\treadonly severity: number;\n\treadonly eventTime: Date;\n\treadonly deviceId: string;\n\treadonly channelId: string | null;\n\treadonly accountId: string;\n\treadonly valueBefore: string | null;\n\treadonly valueAfter: string | null;\n\treadonly groups: string[] | null;\n}\n\n/** Options for filtering events. */\nexport interface EventFilter {\n\t/** Filter by device serial. */\n\tdeviceId?: string;\n\t/** Filter by event type (e.g., \"Low Battery Alert\"). */\n\teventType?: string;\n\t/** Include events at or after this time. */\n\tstartTime?: Date;\n\t/** Include events at or before this time. */\n\tendTime?: Date;\n\t/** Maximum number of events to return (default 50). */\n\tlimit?: number;\n}\n\n// ─── Archives ────────────────────────────────────────────────────────────────\n\n/** A historical session archive. */\nexport interface Archive {\n\treadonly id: string;\n\treadonly start: Date | null;\n\treadonly end: Date | null;\n\treadonly count: number | null;\n\treadonly type: string | null;\n\treadonly label: string | null;\n\treadonly deviceLabel: string | null;\n\treadonly notes: string | null;\n\treadonly createdOn: Date | null;\n\treadonly public: boolean | null;\n\treadonly publicLink: string | null;\n\treadonly filename: string | null;\n\treadonly channels: ArchiveChannel[] | null;\n}\n\n/** A channel snapshot within an archive. */\nexport interface ArchiveChannel {\n\treadonly number: string | null;\n\treadonly label: string | null;\n\treadonly units: string | null;\n\treadonly value: number | null;\n\treadonly status: string | null;\n\treadonly enabled: boolean | null;\n\treadonly color: string | null;\n\treadonly type: string | null;\n\treadonly alarmHigh: Alarm | null;\n\treadonly alarmLow: Alarm | null;\n\treadonly minimum: MinMaxReading | null;\n\treadonly maximum: MinMaxReading | null;\n\treadonly recentReadings: TemperatureReading[];\n}\n\n/** A single temperature reading from an archive. */\nexport interface TemperatureReading {\n\treadonly value: number;\n\treadonly timestamp: Date;\n\treadonly units: string;\n}\n\n/** Options for listing archives. */\nexport interface ArchiveListOptions {\n\t/** Maximum number of archives to return (default 20). */\n\tlimit?: number;\n\t/** Cursor for pagination (archive document ID to start after). */\n\tstartAfter?: string;\n}\n\n// ─── Calibration ─────────────────────────────────────────────────────────────\n\n/** A factory calibration record. */\nexport interface CalibrationRecord {\n\treadonly calibrationId: string;\n\treadonly calibrationDate: Date | null;\n\treadonly deviceId: string;\n\treadonly sessionId: string | null;\n\treadonly performedBy: string | null;\n\treadonly manager: string | null;\n\treadonly referenceDetail: string | null;\n\treadonly statedAccuracy: string | null;\n\treadonly ambientTemperature: string | null;\n\treadonly ambientHumidity: string | null;\n\treadonly result: string | null;\n\treadonly lowPointAdjustments: CalibrationPoint[];\n\treadonly highPointReference: CalibrationPoint[];\n}\n\n/** A single calibration measurement point. */\nexport interface CalibrationPoint {\n\treadonly channel: number;\n\treadonly value: number;\n\treadonly units: string;\n\treadonly referenceValue: number;\n\treadonly deviation: number;\n\treadonly trimValue: number | null;\n\treadonly result: string;\n}\n\n// ─── Firmware ────────────────────────────────────────────────────────────────\n\n/** Firmware information for a device type. */\nexport interface FirmwareInfo {\n\treadonly name: string;\n\treadonly version: string;\n\treadonly location: string;\n\treadonly md5: string;\n}\n\n// ─── Content ─────────────────────────────────────────────────────────────────\n\n/** Cooking temperature guide. */\nexport interface TemperatureGuide {\n\treadonly categories: TemperatureCategory[];\n}\n\n/** A category in the temperature guide (e.g., Beef, Pork). */\nexport interface TemperatureCategory {\n\treadonly label: string;\n\treadonly icon: string;\n\treadonly pullWarning: string | null;\n\treadonly warning: string | null;\n}\n\n// ─── Search ──────────────────────────────────────────────────────────────────\n\n/** Search result from Typesense. */\nexport interface SearchResult {\n\treadonly hits: SearchHit[];\n\treadonly totalHits: number;\n\treadonly page: number;\n}\n\n/** A single search hit. */\nexport interface SearchHit {\n\treadonly id: string;\n\treadonly score: number;\n\treadonly document: Record<string, unknown>;\n}\n\n/** Options for search queries. */\nexport interface SearchOptions {\n\t/** Search collection: \"device\", \"accounts\", or \"users\". */\n\tcollection: \"device\" | \"accounts\" | \"users\";\n\t/** Page number (1-indexed). */\n\tpage?: number;\n\t/** Results per page. */\n\tpageSize?: number;\n}\n\n// ─── Sharing ─────────────────────────────────────────────────────────────────\n\n/** Result from a public sharing operation. */\nexport interface ShareResult {\n\treadonly success: boolean;\n\treadonly publicLink?: string;\n}\n\n// ─── Alarm Configuration ─────────────────────────────────────────────────────\n\n/** Options for configuring a single alarm threshold. */\nexport interface AlarmThresholdOptions {\n\t/** Alarm trigger value (temperature or humidity). */\n\tvalue: number;\n\t/** Units for the alarm value (e.g., \"F\", \"C\"). Defaults to the device's current units if omitted. */\n\tunits?: string;\n\t/** Whether this alarm threshold is enabled. */\n\tenabled?: boolean;\n\t/** Whether this alarm is muted (suppresses notifications). */\n\tmuted?: boolean;\n}\n\n/** Options for setting alarm thresholds on a device channel. At least one of `high` or `low` must be provided. */\nexport interface AlarmSetOptions {\n\t/** High alarm threshold configuration. */\n\thigh?: AlarmThresholdOptions;\n\t/** Low alarm threshold configuration. */\n\tlow?: AlarmThresholdOptions;\n}\n\n// ─── Billing & Storage ───────────────────────────────────────────────────────\n\n/** Total data storage usage for an account. */\nexport interface DataUsage {\n\treadonly totalBytes: number;\n\treadonly formattedSize: string;\n}\n\n/** Data storage usage for a single device. */\nexport interface DeviceDataUsage {\n\treadonly deviceId: string;\n\treadonly bytes: number;\n\treadonly formattedSize: string;\n}\n\n/** Billing plan details. */\nexport interface BillingPlan {\n\treadonly id: string;\n\treadonly name: string;\n\treadonly description: string;\n\treadonly monthlyAmount: number;\n\treadonly deviceCount: number;\n\treadonly isDefault: boolean;\n}\n\n// ─── Account Invites ─────────────────────────────────────────────────────────\n\n/** A pending invitation to join an account. */\nexport interface AccountInvite {\n\treadonly id: string;\n\treadonly accountId: string;\n\treadonly email?: string;\n\treadonly status?: string;\n\treadonly createdAt?: string;\n}\n\n// ─── Actions (Callable Functions) ────────────────────────────────────────────\n\n/** Result from a callable function invocation. */\nexport interface ActionResult<T = unknown> {\n\treadonly success: boolean;\n\treadonly data: T | null;\n\treadonly error: string | null;\n}\n\n// ─── History ─────────────────────────────────────────────────────────────────\n\n/** A single historical temperature reading from BigQuery time-series data. */\nexport interface HistoricalReading {\n\treadonly value: number;\n\treadonly timestamp: string;\n\treadonly units: string;\n}\n\n/** Historical readings for a device retrieved from BigQuery. */\nexport interface DeviceHistory {\n\treadonly deviceId: string;\n\treadonly readings: HistoricalReading[];\n}\n\n// ─── Device Groups ───────────────────────────────────────────────────────────\n\n/** A device group organizing multiple devices together. */\nexport interface DeviceGroup {\n\treadonly id: string;\n\treadonly name: string;\n\treadonly devices: string[];\n}\n\n// ─── Filters ─────────────────────────────────────────────────────────────────\n\n/** Options for filtering devices. */\nexport interface DeviceFilter {\n\t/** Filter by device serial number(s). */\n\tserial?: string | string[];\n\t/** Filter by device type (e.g., \"node\", \"smoke\", \"signals\"). */\n\ttype?: string | string[];\n\t/** Filter by device label (exact match). */\n\tlabel?: string | string[];\n\t/** Filter by device status (e.g., \"online\", \"offline\"). */\n\tstatus?: string | string[];\n\t/** Only include devices seen within this many minutes. */\n\tactiveWithinMinutes?: number;\n}\n\n// ─── Device Health ───────────────────────────────────────────────────────────\n\n/** Overall health assessment for a device. */\nexport interface DeviceHealth {\n\treadonly overall: \"good\" | \"warning\" | \"critical\";\n\treadonly issues: DeviceHealthIssue[];\n}\n\n/** A single health issue detected on a device. */\nexport interface DeviceHealthIssue {\n\treadonly code:\n\t\t| \"stale_reading\"\n\t\t| \"low_battery\"\n\t\t| \"weak_wifi_signal\"\n\t\t| \"offline\"\n\t\t| \"firmware_outdated\";\n\treadonly severity: \"warning\" | \"critical\";\n\treadonly message: string;\n\treadonly detail?: string;\n}\n\n/** Error thrown when authentication fails. */\nexport class AuthError extends Error {\n\treadonly reason: string;\n\n\tconstructor(message: string, reason: string) {\n\t\tsuper(message);\n\t\tthis.name = \"AuthError\";\n\t\tthis.reason = reason;\n\t}\n}\n\n/** Error thrown when a requested resource is not found. */\nexport class NotFoundError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"NotFoundError\";\n\t}\n}\n\n/** Error thrown on network/HTTP failures. */\nexport class NetworkError extends Error {\n\treadonly statusCode: number | null;\n\n\tconstructor(message: string, statusCode: number | null = null) {\n\t\tsuper(message);\n\t\tthis.name = \"NetworkError\";\n\t\tthis.statusCode = statusCode;\n\t}\n}\n","/** Convert a temperature from Celsius to Fahrenheit, rounded to 1 decimal place. */\nexport function toFahrenheit(celsius: number): number {\n\treturn Math.round(((celsius * 9) / 5 + 32) * 10) / 10;\n}\n\n/** Convert a temperature from Fahrenheit to Celsius, rounded to 1 decimal place. */\nexport function toCelsius(fahrenheit: number): number {\n\treturn Math.round((((fahrenheit - 32) * 5) / 9) * 10) / 10;\n}\n","/**\n * Utilities for parsing Google Firestore REST API document fields\n * into plain JavaScript values.\n */\n\ntype FirestoreValue =\n\t| { stringValue: string }\n\t| { integerValue: string }\n\t| { doubleValue: number }\n\t| { booleanValue: boolean }\n\t| { timestampValue: string }\n\t| { nullValue: null }\n\t| { mapValue: { fields?: Record<string, FirestoreValue> } }\n\t| { arrayValue: { values?: FirestoreValue[] } };\n\nexport type { FirestoreValue };\nexport type FirestoreFields = Record<string, FirestoreValue>;\n\n/** Extract a string from a Firestore field. */\nexport function getString(fields: FirestoreFields, key: string): string | null {\n\tconst field = fields[key];\n\tif (!field) return null;\n\tif (\"stringValue\" in field) return field.stringValue;\n\treturn null;\n}\n\n/** Extract a number from a Firestore field (handles both integerValue and doubleValue). */\nexport function getNumber(fields: FirestoreFields, key: string): number | null {\n\tconst field = fields[key];\n\tif (!field) return null;\n\tif (\"doubleValue\" in field) return field.doubleValue;\n\tif (\"integerValue\" in field) return Number(field.integerValue);\n\treturn null;\n}\n\n/** Extract a boolean from a Firestore field. */\nexport function getBoolean(fields: FirestoreFields, key: string): boolean | null {\n\tconst field = fields[key];\n\tif (!field) return null;\n\tif (\"booleanValue\" in field) return field.booleanValue;\n\treturn null;\n}\n\n/** Extract a Date from a Firestore timestamp field. */\nexport function getTimestamp(fields: FirestoreFields, key: string): Date | null {\n\tconst field = fields[key];\n\tif (!field) return null;\n\tif (\"timestampValue\" in field) {\n\t\tconst date = new Date(field.timestampValue);\n\t\treturn Number.isNaN(date.getTime()) ? null : date;\n\t}\n\treturn null;\n}\n\n/** Extract nested map fields from a Firestore field. */\nexport function getMapFields(fields: FirestoreFields, key: string): FirestoreFields | null {\n\tconst field = fields[key];\n\tif (!field) return null;\n\tif (\"mapValue\" in field) return field.mapValue.fields ?? null;\n\treturn null;\n}\n\n/** Extract an array of values from a Firestore field. */\nexport function getArray(fields: FirestoreFields, key: string): FirestoreValue[] | null {\n\tconst field = fields[key];\n\tif (!field) return null;\n\tif (\"arrayValue\" in field) return field.arrayValue.values ?? null;\n\treturn null;\n}\n\n/** Extract string values from an array field. */\nexport function getStringArray(fields: FirestoreFields, key: string): string[] | null {\n\tconst values = getArray(fields, key);\n\tif (!values) return null;\n\tconst result: string[] = [];\n\tfor (const v of values) {\n\t\tif (\"stringValue\" in v) result.push(v.stringValue);\n\t}\n\treturn result.length > 0 ? result : null;\n}\n","import type { DeviceChannel } from \"./types.js\";\n\n/** Options for configuring polling-based subscriptions. */\nexport interface SubscriptionOptions {\n\t/** Polling interval in milliseconds (default 10000 = 10 seconds). */\n\tintervalMs?: number;\n}\n\n/** A snapshot of a single channel's current state, emitted on change. */\nexport interface ChannelUpdate {\n\tserial: string;\n\tchannel: number;\n\tvalue: number | null;\n\tunits: string | null;\n\tstatus: string | null;\n\ttimestamp: string | null;\n}\n\n/** Handle returned by subscribe() to stop receiving updates. */\nexport interface Subscription {\n\t/** Stop polling and release resources. */\n\tunsubscribe(): void;\n}\n\n/** Callback invoked with each channel update. */\nexport type ChannelUpdateCallback = (update: ChannelUpdate) => void;\n\n/** Callback invoked when a polling cycle encounters an error. */\nexport type ErrorCallback = (error: Error) => void;\n\nconst DEFAULT_INTERVAL_MS = 10_000;\n\n/**\n * Build a fingerprint string for deduplication.\n * Only fires callback when value/units/status actually change.\n */\nfunction channelFingerprint(ch: DeviceChannel): string {\n\treturn `${ch.value}|${ch.units}|${ch.status}`;\n}\n\n/** Convert a DeviceChannel to a ChannelUpdate for the callback. */\nfunction toChannelUpdate(serial: string, ch: DeviceChannel): ChannelUpdate {\n\tconst channelNum = ch.number != null ? Number.parseInt(ch.number, 10) : 0;\n\treturn {\n\t\tserial,\n\t\tchannel: channelNum,\n\t\tvalue: ch.value,\n\t\tunits: ch.units,\n\t\tstatus: ch.status,\n\t\ttimestamp: ch.lastSeen?.toISOString() ?? null,\n\t};\n}\n\n/** Dependency: a function that fetches all channels for a device serial. */\nexport type ChannelFetcher = (serial: string, signal?: AbortSignal) => Promise<DeviceChannel[]>;\n\n/**\n * Create a polling subscription that monitors a device's channels for changes.\n *\n * @param serial - Device serial to monitor\n * @param fetchChannels - Function to retrieve current channel state (injected for testability)\n * @param callback - Called with each channel update when state changes\n * @param options - Polling configuration and error handler\n * @returns Subscription handle with unsubscribe()\n */\nexport function createSubscription(\n\tserial: string,\n\tfetchChannels: ChannelFetcher,\n\tcallback: ChannelUpdateCallback,\n\toptions?: SubscriptionOptions & { onError?: ErrorCallback },\n): Subscription {\n\tconst MIN_INTERVAL_MS = 5_000;\n\tconst intervalMs = Math.max(MIN_INTERVAL_MS, options?.intervalMs ?? DEFAULT_INTERVAL_MS);\n\tconst onError = options?.onError;\n\n\t// Track last-known state per channel number for deduplication\n\tconst lastState = new Map<number, string>();\n\tlet timer: ReturnType<typeof setTimeout> | null = null;\n\tlet stopped = false;\n\tlet abortController: AbortController | null = null;\n\n\tasync function poll(): Promise<void> {\n\t\tif (stopped) return;\n\t\tabortController = new AbortController();\n\t\ttry {\n\t\t\tconst channels = await fetchChannels(serial, abortController.signal);\n\t\t\tif (stopped) return;\n\n\t\t\tfor (const ch of channels) {\n\t\t\t\tif (ch.number == null) continue;\n\t\t\t\tconst channelNum = Number.parseInt(ch.number, 10);\n\t\t\t\tif (Number.isNaN(channelNum)) continue;\n\n\t\t\t\tconst fingerprint = channelFingerprint(ch);\n\t\t\t\tconst previous = lastState.get(channelNum);\n\n\t\t\t\tif (previous !== fingerprint) {\n\t\t\t\t\tlastState.set(channelNum, fingerprint);\n\t\t\t\t\tcallback(toChannelUpdate(serial, ch));\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err: unknown) {\n\t\t\tif (stopped) return;\n\t\t\tif (onError) {\n\t\t\t\ttry {\n\t\t\t\t\tonError(err instanceof Error ? err : new Error(String(err)));\n\t\t\t\t} catch {\n\t\t\t\t\t// A throwing consumer onError callback must not reject this\n\t\t\t\t\t// floating poll() promise and crash the host process.\n\t\t\t\t}\n\t\t\t}\n\t\t} finally {\n\t\t\tabortController = null;\n\t\t}\n\t\t// Self-reschedule: next poll only starts after current one completes,\n\t\t// preventing overlap when fetchChannels takes longer than intervalMs\n\t\tif (!stopped) {\n\t\t\ttimer = setTimeout(() => void poll(), intervalMs);\n\t\t}\n\t}\n\n\t// Fire immediately (self-reschedule handles subsequent polls)\n\tvoid poll();\n\n\treturn {\n\t\tunsubscribe(): void {\n\t\t\tif (stopped) return;\n\t\t\tstopped = true;\n\t\t\tif (timer != null) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\ttimer = null;\n\t\t\t}\n\t\t\t// Abort any in-flight fetch to prevent resource leaks\n\t\t\tabortController?.abort();\n\t\t\tabortController = null;\n\t\t\tlastState.clear();\n\t\t},\n\t};\n}\n","import { type AuthSession, createAuthSession } from \"./auth.js\";\nimport { toFahrenheit } from \"./convert.js\";\nimport {\n\ttype FirestoreFields,\n\ttype FirestoreValue,\n\tgetArray,\n\tgetBoolean,\n\tgetMapFields,\n\tgetNumber,\n\tgetString,\n\tgetStringArray,\n\tgetTimestamp,\n} from \"./firestore.js\";\nimport {\n\ttype ChannelUpdateCallback,\n\tcreateSubscription,\n\ttype ErrorCallback,\n\ttype Subscription,\n\ttype SubscriptionOptions,\n} from \"./subscribe.js\";\nimport {\n\ttype Account,\n\ttype AccountInvite,\n\ttype ActionResult,\n\ttype Alarm,\n\ttype AlarmSetOptions,\n\ttype AlarmThresholdOptions,\n\ttype Archive,\n\ttype ArchiveChannel,\n\ttype ArchiveListOptions,\n\ttype BillingPlan,\n\ttype CalibrationPoint,\n\ttype CalibrationRecord,\n\ttype DataUsage,\n\ttype Device,\n\ttype DeviceChannel,\n\ttype DeviceDataUsage,\n\ttype DeviceEvent,\n\ttype DeviceFilter,\n\ttype DeviceGroup,\n\ttype DeviceHistory,\n\ttype EventFilter,\n\ttype FanSettings,\n\ttype FirmwareInfo,\n\ttype HistoricalReading,\n\ttype MinMaxReading,\n\tNetworkError,\n\tNotFoundError,\n\ttype NotificationSettings,\n\ttype SearchHit,\n\ttype SearchOptions,\n\ttype SearchResult,\n\ttype ShareResult,\n\ttype TemperatureCategory,\n\ttype TemperatureGuide,\n\ttype TemperatureReading,\n\ttype ThermoworksConfig,\n\ttype User,\n} from \"./types.js\";\n\nconst SERIAL_PATTERN = /^[A-Za-z0-9:_-]+$/;\n\nfunction validateSerial(serial: string): void {\n\tif (!serial || !SERIAL_PATTERN.test(serial)) {\n\t\tthrow new Error(`Invalid device serial: ${serial}`);\n\t}\n}\n\nfunction validateChannel(channel: number): void {\n\tif (!Number.isInteger(channel) || channel < 1 || channel > 9) {\n\t\tthrow new Error(`Invalid channel number: ${channel} (must be integer 1-9)`);\n\t}\n}\n\n/** Strip ANSI escape sequences and control characters from a string. */\nfunction sanitizeLabel(value: string | null | undefined): string | null {\n\tif (value == null) return null;\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional - stripping control chars\n\treturn value.replace(/[\\x00-\\x1f\\x7f-\\x9f](\\[[0-9;]*[A-Za-z])?/g, \"\");\n}\n\n/**\n * Sanitize a units string: strip control/escape chars like a label, then cap the\n * length so units stays a short token (for example F, C, %RH) and cannot smuggle\n * a prompt-injection sentence into a terminal or an LLM tool result. This keeps\n * units a genuinely constrained field even though it is written from the cloud.\n */\nfunction sanitizeUnits(value: string | null | undefined): string | null {\n\tconst cleaned = sanitizeLabel(value);\n\treturn cleaned == null ? cleaned : cleaned.slice(0, 8);\n}\n\n/**\n * Client for the ThermoWorks Cloud service.\n *\n * @example\n * ```ts\n * const client = new ThermoworksCloud({\n *   email: process.env.THERMOWORKS_EMAIL!,\n *   password: process.env.THERMOWORKS_PASSWORD!,\n * });\n *\n * const devices = await client.getDevices();\n * for (const device of devices) {\n *   const channel = await client.getDeviceChannel(device.serial, 1);\n *   console.log(`${device.label}: ${channel.value}°${channel.units}`);\n * }\n *\n * client.close();\n * ```\n */\nexport class ThermoworksCloud {\n\tprivate readonly config: ThermoworksConfig;\n\tprivate session: AuthSession | null = null;\n\tprivate sessionPromise: Promise<AuthSession> | null = null;\n\tprivate cachedAccountId: string | null = null;\n\tprivate closed = false;\n\tprivate readonly activeSubscriptions = new Set<Subscription>();\n\n\tconstructor(config: ThermoworksConfig) {\n\t\tthis.config = config;\n\t}\n\n\t/** Get the authenticated user's information. */\n\tasync getUser(): Promise<User> {\n\t\tconst session = await this.ensureSession();\n\t\tconst userId = session.getUserId();\n\t\tconst response = await session.request(\"GET\", `documents/users/${userId}`);\n\n\t\tif (response.status === 404) {\n\t\t\tawait response.text().catch(() => {});\n\t\t\tthrow new NotFoundError(\"User not found\");\n\t\t}\n\n\t\tconst doc = (await response.json()) as { fields?: FirestoreFields };\n\t\tconst fields = doc.fields ?? {};\n\n\t\treturn {\n\t\t\tuserId,\n\t\t\taccountId: getString(fields, \"accountId\"),\n\t\t\temail: getString(fields, \"email\"),\n\t\t\tdisplayName: getString(fields, \"displayName\"),\n\t\t\ttimeZone: getString(fields, \"timeZone\"),\n\t\t\tpreferredUnits: getString(fields, \"preferredUnits\"),\n\t\t\tlocale: getString(fields, \"locale\"),\n\t\t\tphotoUrl: getString(fields, \"photoURL\"),\n\t\t\tuse24Time: getBoolean(fields, \"use24Time\"),\n\t\t\tlastLogin: getTimestamp(fields, \"lastLogin\"),\n\t\t\tappVersion: getString(fields, \"appVersion\"),\n\t\t\taccountRoles: parseStringBooleanMap(getMapFields(fields, \"accountRoles\")),\n\t\t\troles: parseStringBooleanMap(getMapFields(fields, \"roles\")),\n\t\t\tnotificationSettings: parseNotificationSettings(getMapFields(fields, \"notificationSettings\")),\n\t\t};\n\t}\n\n\t/** Get the authenticated user's notification preferences. */\n\tasync getNotificationSettings(): Promise<NotificationSettings> {\n\t\tconst user = await this.getUser();\n\t\treturn (\n\t\t\tuser.notificationSettings ?? {\n\t\t\t\tenabled: false,\n\t\t\t\tcontinuousAlerts: false,\n\t\t\t\temailNotification: false,\n\t\t\t\tsmsNotification: false,\n\t\t\t\tdeviceNotification: false,\n\t\t\t}\n\t\t);\n\t}\n\n\t/**\n\t * Update the authenticated user's notification preferences.\n\t *\n\t * Performs a read-merge-write to ensure unspecified fields retain\n\t * their current values (Firestore updateMask replaces the entire map).\n\t */\n\tasync updateNotificationSettings(settings: Partial<NotificationSettings>): Promise<void> {\n\t\tconst current = await this.getNotificationSettings();\n\t\tconst merged: NotificationSettings = { ...current, ...settings };\n\n\t\tconst session = await this.ensureSession();\n\t\tconst userId = session.getUserId();\n\t\tconst path = `documents/users/${userId}?updateMask.fieldPaths=notificationSettings`;\n\t\tconst body = {\n\t\t\tfields: {\n\t\t\t\tnotificationSettings: {\n\t\t\t\t\tmapValue: {\n\t\t\t\t\t\tfields: {\n\t\t\t\t\t\t\tenabled: { booleanValue: merged.enabled },\n\t\t\t\t\t\t\tcontinuousAlerts: { booleanValue: merged.continuousAlerts },\n\t\t\t\t\t\t\temailNotification: { booleanValue: merged.emailNotification },\n\t\t\t\t\t\t\tsmsNotification: { booleanValue: merged.smsNotification },\n\t\t\t\t\t\t\tdeviceNotification: { booleanValue: merged.deviceNotification },\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\n\t\tconst response = await session.request(\"PATCH\", path, body);\n\t\tif (!response.ok) {\n\t\t\tthrow new NetworkError(\n\t\t\t\t`Failed to update notification settings: HTTP ${response.status}`,\n\t\t\t\tresponse.status,\n\t\t\t);\n\t\t}\n\t\t// Consume the response body to release the connection\n\t\tawait response.text().catch(() => {});\n\t}\n\n\t/** Get all devices for the authenticated user, with optional filtering. */\n\tasync getDevices(filter?: DeviceFilter): Promise<Device[]> {\n\t\tlet accountId = this.cachedAccountId;\n\t\tif (!accountId) {\n\t\t\tconst user = await this.getUser();\n\t\t\tif (!user.accountId) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\taccountId = user.accountId;\n\t\t\tthis.cachedAccountId = accountId;\n\t\t}\n\n\t\tconst session = await this.ensureSession();\n\t\tconst queryBody = {\n\t\t\tstructuredQuery: {\n\t\t\t\tfrom: [{ collectionId: \"devices\" }],\n\t\t\t\twhere: {\n\t\t\t\t\tfieldFilter: {\n\t\t\t\t\t\tfield: { fieldPath: \"accountId\" },\n\t\t\t\t\t\top: \"EQUAL\",\n\t\t\t\t\t\tvalue: { stringValue: accountId },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\torderBy: [{ field: { fieldPath: \"__name__\" }, direction: \"ASCENDING\" }],\n\t\t\t},\n\t\t};\n\n\t\tconst response = await session.request(\"POST\", \"documents:runQuery\", queryBody);\n\t\tconst rawResults = await response.json();\n\t\tif (!Array.isArray(rawResults)) {\n\t\t\tconst maybeError = rawResults as { error?: { message?: string } } | null;\n\t\t\tif (maybeError?.error) {\n\t\t\t\tthrow new NetworkError(maybeError.error.message ?? \"Query failed\");\n\t\t\t}\n\t\t\treturn [];\n\t\t}\n\t\tconst results = rawResults as Array<{ document?: { fields?: FirestoreFields } }>;\n\n\t\tlet devices: Device[] = [];\n\t\tfor (const result of results) {\n\t\t\tif (result.document?.fields) {\n\t\t\t\tdevices.push(parseDevice(result.document.fields));\n\t\t\t}\n\t\t}\n\n\t\tif (filter) {\n\t\t\tdevices = applyDeviceFilter(devices, filter);\n\t\t}\n\n\t\treturn devices;\n\t}\n\n\t/** Get a single device by serial number. */\n\tasync getDevice(serial: string): Promise<Device> {\n\t\tvalidateSerial(serial);\n\t\tconst fields = await this.fetchDocFields(\n\t\t\t`documents/devices/${encodeURIComponent(serial)}`,\n\t\t\t`Device with serial '${serial}' not found`,\n\t\t);\n\t\tif (!fields || Object.keys(fields).length === 0) {\n\t\t\tthrow new NetworkError(\"Invalid response: missing fields\");\n\t\t}\n\t\treturn parseDevice(fields);\n\t}\n\n\t/** Get a channel reading for a device. Channels are 1-indexed. */\n\tasync getDeviceChannel(serial: string, channel: number): Promise<DeviceChannel> {\n\t\tvalidateSerial(serial);\n\t\tvalidateChannel(channel);\n\t\tconst fields = await this.fetchDocFields(\n\t\t\t`documents/devices/${encodeURIComponent(serial)}/channels/${channel}`,\n\t\t\t`Channel ${channel} not found for device '${serial}'`,\n\t\t);\n\t\treturn parseDeviceChannel(fields);\n\t}\n\n\t/**\n\t * Get all channels for a device. Probes all channels 1-9, skipping any gaps.\n\t */\n\tasync getAllDeviceChannels(serial: string): Promise<DeviceChannel[]> {\n\t\tconst results = await Promise.allSettled(\n\t\t\tArray.from({ length: 9 }, (_, i) => this.getDeviceChannel(serial, i + 1)),\n\t\t);\n\t\tconst channels: DeviceChannel[] = [];\n\t\tfor (const result of results) {\n\t\t\tif (result.status === \"fulfilled\") {\n\t\t\t\tchannels.push(result.value);\n\t\t\t} else if (!(result.reason instanceof NotFoundError)) {\n\t\t\t\tthrow result.reason;\n\t\t\t}\n\t\t}\n\t\treturn channels;\n\t}\n\n\t/**\n\t * Compute the average temperature across all temperature channels for a device.\n\t * Excludes humidity channels (units \"H\") and channels with no reading.\n\t * Returns null if no temperature channels have readings.\n\t */\n\tasync getAverageTemperature(serial: string): Promise<{ value: number; units: string } | null> {\n\t\tconst channels = await this.getAllDeviceChannels(serial);\n\t\tconst temps = channels.filter(\n\t\t\t(ch): ch is DeviceChannel & { value: number; units: string } =>\n\t\t\t\tch.value != null && ch.units != null && ch.units !== \"H\",\n\t\t);\n\n\t\tconst first = temps[0];\n\t\tif (!first) return null;\n\n\t\t// Normalize all values to the first channel's unit system before averaging\n\t\tconst targetUnits = first.units;\n\t\tconst normalized = temps.map((ch) => {\n\t\t\tif (ch.units === targetUnits) return ch.value;\n\t\t\t// Convert between F and C as needed\n\t\t\treturn targetUnits === \"F\" ? toFahrenheit(ch.value) : (ch.value - 32) * (5 / 9);\n\t\t});\n\n\t\tconst sum = normalized.reduce((acc, v) => acc + v, 0);\n\t\treturn {\n\t\t\tvalue: Math.round((sum / normalized.length) * 10) / 10,\n\t\t\tunits: targetUnits,\n\t\t};\n\t}\n\n\t/**\n\t * Set alarm thresholds on a device channel.\n\t *\n\t * Performs a partial update - only the provided fields are modified.\n\t * At least one of `high` or `low` must be specified.\n\t *\n\t * @example\n\t * ```ts\n\t * // Set a high alarm at 275°F\n\t * await client.setAlarm(\"ABC123\", 1, {\n\t *   high: { value: 275, units: \"F\", enabled: true },\n\t * });\n\t *\n\t * // Set both high and low alarms\n\t * await client.setAlarm(\"ABC123\", 1, {\n\t *   high: { value: 275, units: \"F\", enabled: true },\n\t *   low: { value: 32, units: \"F\", enabled: true },\n\t * });\n\t * ```\n\t */\n\tasync setAlarm(serial: string, channel: number, config: AlarmSetOptions): Promise<void> {\n\t\tvalidateSerial(serial);\n\t\tvalidateChannel(channel);\n\n\t\tif (!config.high && !config.low) {\n\t\t\tthrow new Error(\"At least one of 'high' or 'low' must be provided\");\n\t\t}\n\n\t\tconst fieldPaths: string[] = [];\n\t\tconst fields: Record<string, FirestoreValue> = {};\n\n\t\tif (config.high) {\n\t\t\tfieldPaths.push(\"alarmHigh\");\n\t\t\tfields.alarmHigh = buildAlarmMapValue(config.high);\n\t\t}\n\n\t\tif (config.low) {\n\t\t\tfieldPaths.push(\"alarmLow\");\n\t\t\tfields.alarmLow = buildAlarmMapValue(config.low);\n\t\t}\n\n\t\tconst updateMask = fieldPaths.map((fp) => `updateMask.fieldPaths=${fp}`).join(\"&\");\n\n\t\tconst path = `documents/devices/${encodeURIComponent(serial)}/channels/${channel}?${updateMask}`;\n\t\tconst body = { fields };\n\n\t\tconst session = await this.ensureSession();\n\t\tawait session.request(\"PATCH\", path, body);\n\t}\n\n\t/** Get account metadata. */\n\tasync getAccount(): Promise<Account> {\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst fields = await this.fetchDocFields(\n\t\t\t`documents/accounts/${encodeURIComponent(accountId)}`,\n\t\t\t\"Account not found\",\n\t\t);\n\n\t\treturn {\n\t\t\taccountId,\n\t\t\tname: getString(fields, \"name\"),\n\t\t\ttype: getString(fields, \"type\"),\n\t\t\tcreatedOn: getTimestamp(fields, \"createdOn\"),\n\t\t\texportVersion: getNumber(fields, \"exportVersion\"),\n\t\t};\n\t}\n\n\t/** Get total data storage usage for the authenticated user's account. */\n\tasync getDataUsage(): Promise<DataUsage> {\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\t\tconst result = await session.callFunction(\"accountDataStorageSize\", { accountId });\n\t\tconst data = result as { totalBytes?: number } | null;\n\t\tconst totalBytes = data?.totalBytes ?? 0;\n\t\treturn {\n\t\t\ttotalBytes,\n\t\t\tformattedSize: formatBytes(totalBytes),\n\t\t};\n\t}\n\n\t/** Get per-device data storage usage for the authenticated user's account. */\n\tasync getDataUsageByDevice(): Promise<DeviceDataUsage[]> {\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\t\tconst result = await session.callFunction(\"accountDataStorageSizeByTable\", { accountId });\n\t\tconst data = result as Array<{ deviceId?: string; bytes?: number }> | null;\n\t\tif (!Array.isArray(data)) return [];\n\t\treturn data.map((entry) => {\n\t\t\tconst bytes = entry.bytes ?? 0;\n\t\t\treturn {\n\t\t\t\tdeviceId: entry.deviceId ?? \"\",\n\t\t\t\tbytes,\n\t\t\t\tformattedSize: formatBytes(bytes),\n\t\t\t};\n\t\t});\n\t}\n\n\t/** Get the billing plan for the authenticated user's account. */\n\tasync getBillingPlan(): Promise<BillingPlan | null> {\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\t\tconst accountResponse = await session.request(\n\t\t\t\"GET\",\n\t\t\t`documents/accounts/${encodeURIComponent(accountId)}`,\n\t\t);\n\n\t\tif (accountResponse.status === 404) {\n\t\t\tawait accountResponse.text().catch(() => {});\n\t\t\treturn null;\n\t\t}\n\n\t\tconst accountDoc = (await accountResponse.json()) as { fields?: FirestoreFields };\n\t\tconst accountFields = accountDoc.fields ?? {};\n\t\tconst planId = getString(accountFields, \"billingPlanId\");\n\t\tif (!planId) return null;\n\n\t\tconst planResponse = await session.request(\n\t\t\t\"GET\",\n\t\t\t`documents/system/billingPlans/plans/${encodeURIComponent(planId)}`,\n\t\t);\n\n\t\tif (planResponse.status === 404) {\n\t\t\tawait planResponse.text().catch(() => {});\n\t\t\treturn null;\n\t\t}\n\n\t\tconst planDoc = (await planResponse.json()) as { fields?: FirestoreFields };\n\t\tconst fields = planDoc.fields ?? {};\n\t\treturn {\n\t\t\tid: planId,\n\t\t\tname: getString(fields, \"name\") ?? \"\",\n\t\t\tdescription: getString(fields, \"description\") ?? \"\",\n\t\t\tmonthlyAmount: getNumber(fields, \"monthlyAmount\") ?? 0,\n\t\t\tdeviceCount: getNumber(fields, \"deviceCount\") ?? 0,\n\t\t\tisDefault: getBoolean(fields, \"isDefault\") ?? false,\n\t\t};\n\t}\n\n\t/** Get pending invitations for the authenticated user's account. */\n\tasync getInvites(): Promise<AccountInvite[]> {\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\n\t\tconst queryBody = {\n\t\t\tstructuredQuery: {\n\t\t\t\tfrom: [{ collectionId: \"usersInvites\" }],\n\t\t\t\twhere: {\n\t\t\t\t\tfieldFilter: {\n\t\t\t\t\t\tfield: { fieldPath: \"accountId\" },\n\t\t\t\t\t\top: \"EQUAL\",\n\t\t\t\t\t\tvalue: { stringValue: accountId },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\torderBy: [{ field: { fieldPath: \"__name__\" }, direction: \"ASCENDING\" }],\n\t\t\t},\n\t\t};\n\n\t\tconst response = await session.request(\"POST\", \"documents:runQuery\", queryBody);\n\t\tconst rawResults = await response.json();\n\t\tif (!Array.isArray(rawResults)) {\n\t\t\tconst maybeError = rawResults as { error?: { message?: string } } | null;\n\t\t\tif (maybeError?.error) {\n\t\t\t\tthrow new NetworkError(maybeError.error.message ?? \"Invites query failed\");\n\t\t\t}\n\t\t\treturn [];\n\t\t}\n\n\t\tconst results = rawResults as Array<{ document?: { fields?: FirestoreFields; name?: string } }>;\n\t\tconst invites: AccountInvite[] = [];\n\t\tfor (const result of results) {\n\t\t\tif (result.document?.fields) {\n\t\t\t\tconst f = result.document.fields;\n\t\t\t\tinvites.push({\n\t\t\t\t\tid: extractDocId(result.document.name),\n\t\t\t\t\taccountId: getString(f, \"accountId\") ?? accountId,\n\t\t\t\t\temail: getString(f, \"email\") ?? undefined,\n\t\t\t\t\tstatus: getString(f, \"status\") ?? undefined,\n\t\t\t\t\tcreatedAt: getString(f, \"createdAt\") ?? undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\treturn invites;\n\t}\n\n\t/** Remove a user from the authenticated user's account. */\n\tasync removeUser(userId: string): Promise<ActionResult> {\n\t\tif (!userId) {\n\t\t\tthrow new Error(\"userId is required\");\n\t\t}\n\t\tconst accountId = await this.resolveAccountId();\n\t\treturn this.callAction(\"userRemoteFromAccount\", { userId, accountId });\n\t}\n\n\t/** Get events for the authenticated user's account. */\n\tasync getEvents(filter?: EventFilter): Promise<DeviceEvent[]> {\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\t\tconst limit = Math.min(Math.max(1, filter?.limit ?? 50), 500);\n\n\t\tconst filters: Array<{\n\t\t\tfieldFilter: {\n\t\t\t\tfield: { fieldPath: string };\n\t\t\t\top: string;\n\t\t\t\tvalue: { stringValue: string } | { timestampValue: string };\n\t\t\t};\n\t\t}> = [\n\t\t\t{\n\t\t\t\tfieldFilter: {\n\t\t\t\t\tfield: { fieldPath: \"accountId\" },\n\t\t\t\t\top: \"EQUAL\",\n\t\t\t\t\tvalue: { stringValue: accountId },\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\n\t\tif (filter?.deviceId) {\n\t\t\tfilters.push({\n\t\t\t\tfieldFilter: {\n\t\t\t\t\tfield: { fieldPath: \"deviceId\" },\n\t\t\t\t\top: \"EQUAL\",\n\t\t\t\t\tvalue: { stringValue: filter.deviceId },\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tif (filter?.eventType) {\n\t\t\tfilters.push({\n\t\t\t\tfieldFilter: {\n\t\t\t\t\tfield: { fieldPath: \"EventType\" },\n\t\t\t\t\top: \"EQUAL\",\n\t\t\t\t\tvalue: { stringValue: filter.eventType },\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tif (filter?.startTime) {\n\t\t\tfilters.push({\n\t\t\t\tfieldFilter: {\n\t\t\t\t\tfield: { fieldPath: \"EventTime\" },\n\t\t\t\t\top: \"GREATER_THAN_OR_EQUAL\",\n\t\t\t\t\tvalue: { timestampValue: filter.startTime.toISOString() },\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tif (filter?.endTime) {\n\t\t\tfilters.push({\n\t\t\t\tfieldFilter: {\n\t\t\t\t\tfield: { fieldPath: \"EventTime\" },\n\t\t\t\t\top: \"LESS_THAN_OR_EQUAL\",\n\t\t\t\t\tvalue: { timestampValue: filter.endTime.toISOString() },\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\tconst where =\n\t\t\tfilters.length === 1 ? filters[0] : { compositeFilter: { op: \"AND\", filters: filters } };\n\n\t\tconst queryBody = {\n\t\t\tstructuredQuery: {\n\t\t\t\tfrom: [{ collectionId: \"events\" }],\n\t\t\t\twhere,\n\t\t\t\torderBy: [{ field: { fieldPath: \"EventTime\" }, direction: \"DESCENDING\" }],\n\t\t\t\tlimit,\n\t\t\t},\n\t\t};\n\n\t\tconst response = await session.request(\"POST\", \"documents:runQuery\", queryBody);\n\t\tconst rawResults = await response.json();\n\t\tif (!Array.isArray(rawResults)) {\n\t\t\tconst maybeError = rawResults as { error?: { message?: string } } | null;\n\t\t\tif (maybeError?.error) {\n\t\t\t\tthrow new NetworkError(maybeError.error.message ?? \"Event query failed\");\n\t\t\t}\n\t\t\treturn [];\n\t\t}\n\n\t\tconst results = rawResults as Array<{ document?: { fields?: FirestoreFields; name?: string } }>;\n\t\tconst events: DeviceEvent[] = [];\n\t\tfor (const result of results) {\n\t\t\tif (result.document?.fields) {\n\t\t\t\tevents.push(parseDeviceEvent(result.document.fields, extractDocId(result.document.name)));\n\t\t\t}\n\t\t}\n\t\treturn events;\n\t}\n\n\t/** Get events for a specific device. */\n\tasync getDeviceEvents(serial: string, limit?: number): Promise<DeviceEvent[]> {\n\t\tvalidateSerial(serial);\n\t\treturn this.getEvents({ deviceId: serial, limit: limit ?? 50 });\n\t}\n\n\t/** Get archived sessions for a device. */\n\tasync getArchives(serial: string, options?: ArchiveListOptions): Promise<Archive[]> {\n\t\tvalidateSerial(serial);\n\t\tconst session = await this.ensureSession();\n\t\tconst limit = Math.min(Math.max(1, options?.limit ?? 20), 500);\n\t\tlet path = `documents/devices/${encodeURIComponent(serial)}/archive?pageSize=${limit}&orderBy=createdOn%20desc`;\n\t\tif (options?.startAfter) {\n\t\t\tpath += `&pageToken=${encodeURIComponent(options.startAfter)}`;\n\t\t}\n\n\t\tconst response = await session.request(\"GET\", path);\n\t\tconst data = (await response.json()) as {\n\t\t\tdocuments?: Array<{ fields?: FirestoreFields; name?: string }>;\n\t\t\terror?: { message?: string };\n\t\t};\n\t\tif (data.error) {\n\t\t\tthrow new NetworkError(data.error.message ?? \"Failed to list archives\");\n\t\t}\n\t\tif (!data.documents) return [];\n\n\t\treturn data.documents.map((doc) => parseArchive(doc.fields ?? {}, extractDocId(doc.name)));\n\t}\n\n\t/** Get a specific archive by ID. */\n\tasync getArchive(serial: string, archiveId: string): Promise<Archive> {\n\t\tvalidateSerial(serial);\n\t\tconst session = await this.ensureSession();\n\t\tconst response = await session.request(\n\t\t\t\"GET\",\n\t\t\t`documents/devices/${encodeURIComponent(serial)}/archive/${encodeURIComponent(archiveId)}`,\n\t\t);\n\n\t\tif (response.status === 404) {\n\t\t\tawait response.text().catch(() => {});\n\t\t\tthrow new NotFoundError(`Archive '${archiveId}' not found for device '${serial}'`);\n\t\t}\n\n\t\tconst doc = (await response.json()) as { fields?: FirestoreFields; name?: string };\n\t\treturn parseArchive(doc.fields ?? {}, archiveId);\n\t}\n\n\t/** Get calibration records for a device. */\n\tasync getCalibration(serial: string): Promise<CalibrationRecord[]> {\n\t\tvalidateSerial(serial);\n\t\tconst session = await this.ensureSession();\n\t\tconst response = await session.request(\n\t\t\t\"GET\",\n\t\t\t`documents/devices/${encodeURIComponent(serial)}/calibration`,\n\t\t);\n\n\t\tconst data = (await response.json()) as {\n\t\t\tdocuments?: Array<{ fields?: FirestoreFields; name?: string }>;\n\t\t};\n\t\tif (!data.documents) return [];\n\n\t\treturn data.documents.map((doc) =>\n\t\t\tparseCalibrationRecord(doc.fields ?? {}, extractDocId(doc.name)),\n\t\t);\n\t}\n\n\t/** Get firmware info for a device type. */\n\tasync getFirmwareInfo(deviceType: string): Promise<FirmwareInfo> {\n\t\tconst fields = await this.fetchDocFields(\n\t\t\t`documents/firmware/${encodeURIComponent(deviceType)}`,\n\t\t\t`Firmware info not found for type '${deviceType}'`,\n\t\t);\n\n\t\treturn {\n\t\t\tname: getString(fields, \"name\") ?? deviceType,\n\t\t\tversion: getString(fields, \"version\") ?? \"\",\n\t\t\tlocation: getString(fields, \"location\") ?? \"\",\n\t\t\tmd5: getString(fields, \"md5\") ?? \"\",\n\t\t};\n\t}\n\n\t/** Get the cooking temperature guide. */\n\tasync getTemperatureGuide(): Promise<TemperatureGuide> {\n\t\tconst session = await this.ensureSession();\n\t\tconst response = await session.request(\"GET\", \"documents/content/temperatureGuide\");\n\n\t\tif (response.status === 404) {\n\t\t\tawait response.text().catch(() => {});\n\t\t\tthrow new NotFoundError(\"Temperature guide not found\");\n\t\t}\n\n\t\tconst doc = (await response.json()) as { fields?: FirestoreFields };\n\t\tconst fields = doc.fields ?? {};\n\t\tconst categoriesRaw = getArray(fields, \"categories\");\n\t\tconst categories: TemperatureCategory[] = [];\n\n\t\tif (categoriesRaw) {\n\t\t\tfor (const item of categoriesRaw) {\n\t\t\t\tif (\"mapValue\" in item && item.mapValue.fields) {\n\t\t\t\t\tconst f = item.mapValue.fields;\n\t\t\t\t\tcategories.push({\n\t\t\t\t\t\tlabel: getString(f, \"label\") ?? \"\",\n\t\t\t\t\t\ticon: getString(f, \"icon\") ?? \"\",\n\t\t\t\t\t\tpullWarning: getString(f, \"pullWarning\"),\n\t\t\t\t\t\twarning: getString(f, \"warning\"),\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { categories };\n\t}\n\n\t/** Search across devices, accounts, or users via Typesense. */\n\tasync search(query: string, options: SearchOptions): Promise<SearchResult> {\n\t\tconst allowedCollections = new Set([\"device\", \"accounts\", \"users\"]);\n\t\tif (!allowedCollections.has(options.collection)) {\n\t\t\tthrow new Error(`Invalid search collection: ${options.collection}`);\n\t\t}\n\t\tif (query.length > 500) {\n\t\t\tthrow new Error(\"Search query exceeds maximum length of 500 characters\");\n\t\t}\n\t\tconst session = await this.ensureSession();\n\t\tconst result = await session.callFunction(\"typesense_search\", {\n\t\t\tquery,\n\t\t\tcollection: options.collection,\n\t\t\tpage: Math.max(1, options.page ?? 1),\n\t\t\tpageSize: Math.min(Math.max(1, options.pageSize ?? 20), 100),\n\t\t});\n\n\t\tconst data = result as {\n\t\t\thits?: Array<{ id?: string; score?: number; document?: Record<string, unknown> }>;\n\t\t\ttotalHits?: number;\n\t\t\tpage?: number;\n\t\t} | null;\n\t\tconst hits: SearchHit[] = [];\n\t\tif (data?.hits) {\n\t\t\tfor (const hit of data.hits) {\n\t\t\t\thits.push({\n\t\t\t\t\tid: hit.id ?? \"\",\n\t\t\t\t\tscore: hit.score ?? 0,\n\t\t\t\t\tdocument: hit.document ?? {},\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\thits,\n\t\t\ttotalHits: data?.totalHits ?? 0,\n\t\t\tpage: data?.page ?? 1,\n\t\t};\n\t}\n\n\t/** Start a monitoring session for the given device. */\n\tasync startSession(serial: string, label?: string): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\tif (label != null && label.length > 200) {\n\t\t\tthrow new Error(\"label exceeds maximum length of 200 characters\");\n\t\t}\n\t\tconst data: Record<string, unknown> = { deviceId: serial };\n\t\tif (label != null) data.label = sanitizeLabel(label) ?? label;\n\t\treturn this.callAction(\"newSessionRequest\", data);\n\t}\n\n\t/** End an active monitoring session for the given device. */\n\tasync endSession(serial: string): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\treturn this.callAction(\"endSessionRequest\", { deviceId: serial });\n\t}\n\n\t/** Clear session data for the given device. */\n\tasync clearSession(serial: string): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\treturn this.callAction(\"clearSessionRequest\", { deviceId: serial });\n\t}\n\n\t/** Reset the min/max readings for a specific device channel. */\n\tasync resetMinMax(serial: string, channel: number): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\tvalidateChannel(channel);\n\t\treturn this.callAction(\"telemetryDeviceChannelResetMinMax\", {\n\t\t\tdeviceId: serial,\n\t\t\tchannelId: channel,\n\t\t});\n\t}\n\n\t/** Clear all events for the given device. */\n\tasync clearEvents(serial: string): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\treturn this.callAction(\"deviceClearEvents\", { deviceId: serial });\n\t}\n\n\t/** Update device state/settings via a Cloud Function. */\n\tasync updateDeviceState(serial: string, state: Record<string, unknown>): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\tif (!state || typeof state !== \"object\" || Array.isArray(state)) {\n\t\t\tthrow new Error(\"state must be a non-null object\");\n\t\t}\n\t\treturn this.callAction(\"deviceStateUpdate\", { deviceId: serial, state });\n\t}\n\n\t/** Rename a device. */\n\tasync renameDevice(serial: string, name: string): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\tif (typeof name !== \"string\" || name.trim().length === 0) {\n\t\t\tthrow new Error(\"name must be a non-empty string\");\n\t\t}\n\t\tif (name.length > 200) {\n\t\t\tthrow new Error(\"name exceeds maximum length of 200 characters\");\n\t\t}\n\t\treturn this.callAction(\"setInstrumentName\", { deviceId: serial, name });\n\t}\n\n\t/** Factory reset a device. */\n\tasync factoryReset(serial: string): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\treturn this.callAction(\"deviceFactoryReset\", { deviceId: serial });\n\t}\n\n\t/** Share a device's live state publicly via a shareable link. */\n\tasync shareDevice(serial: string): Promise<ShareResult> {\n\t\tvalidateSerial(serial);\n\t\tconst session = await this.ensureSession();\n\t\tconst result = await session.callFunction(\"publicShareDeviceState\", { deviceId: serial });\n\t\treturn toShareResult(result);\n\t}\n\n\t/** Share an archive publicly via a shareable link. */\n\tasync shareArchive(serial: string, archiveId: string): Promise<ShareResult> {\n\t\tvalidateSerial(serial);\n\t\tif (!archiveId) {\n\t\t\tthrow new Error(\"archiveId is required\");\n\t\t}\n\t\tconst session = await this.ensureSession();\n\t\tconst result = await session.callFunction(\"publicShareArchive\", {\n\t\t\tarchiveId,\n\t\t\tdeviceId: serial,\n\t\t});\n\t\treturn toShareResult(result);\n\t}\n\n\t/** Retrieve full historical temperature time-series data from BigQuery. */\n\tasync getHistory(serial: string): Promise<DeviceHistory> {\n\t\tvalidateSerial(serial);\n\t\tconst session = await this.ensureSession();\n\t\tconst result = await session.callFunction(\"requestRetrieveInstrumentHistory\", {\n\t\t\tdeviceId: serial,\n\t\t});\n\t\treturn parseDeviceHistory(serial, result);\n\t}\n\n\t// ─── Device Groups ─────────────────────────────────────────────────────────\n\n\t/**\n\t * Get device groups for the authenticated user.\n\t *\n\t * Reads the user profile's `deviceGroupOrDeviceOrder` field to identify\n\t * group references, then fetches each group's details from the account's\n\t * `deviceGroups` subcollection.\n\t */\n\tasync getDeviceGroups(): Promise<DeviceGroup[]> {\n\t\tconst session = await this.ensureSession();\n\t\tconst userId = session.getUserId();\n\t\tconst response = await session.request(\"GET\", `documents/users/${userId}`);\n\n\t\tif (response.status === 404) {\n\t\t\tawait response.text().catch(() => {});\n\t\t\tthrow new NotFoundError(\"User not found\");\n\t\t}\n\n\t\tconst doc = (await response.json()) as { fields?: FirestoreFields };\n\t\tconst fields = doc.fields ?? {};\n\n\t\tconst accountId = getString(fields, \"accountId\");\n\t\tif (!accountId) return [];\n\n\t\t// Parse deviceGroupOrDeviceOrder: { accountId: { \"0\": { id, type }, ... } }\n\t\tconst orderMap = getMapFields(fields, \"deviceGroupOrDeviceOrder\");\n\t\tif (!orderMap) return [];\n\n\t\tconst accountOrderMap = getMapFields(orderMap, accountId);\n\t\tif (!accountOrderMap) return [];\n\n\t\t// Collect group IDs from entries with type \"deviceGroup\"\n\t\tconst groupIds: string[] = [];\n\t\tfor (const entry of Object.values(accountOrderMap)) {\n\t\t\tif (\"mapValue\" in entry) {\n\t\t\t\tconst entryFields = entry.mapValue.fields ?? {};\n\t\t\t\tconst id =\n\t\t\t\t\tentryFields.id && \"stringValue\" in entryFields.id ? entryFields.id.stringValue : null;\n\t\t\t\tconst type =\n\t\t\t\t\tentryFields.type && \"stringValue\" in entryFields.type\n\t\t\t\t\t\t? entryFields.type.stringValue\n\t\t\t\t\t\t: null;\n\t\t\t\tif (id && type === \"deviceGroup\") {\n\t\t\t\t\tgroupIds.push(id);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (groupIds.length === 0) return [];\n\n\t\t// Fetch each group document from the account's deviceGroups subcollection.\n\t\t// The requests are independent, so run them in parallel to avoid N sequential\n\t\t// round trips; Promise.all preserves groupIds order in the result.\n\t\tconst groups: DeviceGroup[] = await Promise.all(\n\t\t\tgroupIds.map(async (groupId): Promise<DeviceGroup> => {\n\t\t\t\tconst groupPath = `documents/accounts/${encodeURIComponent(accountId)}/deviceGroups/${encodeURIComponent(groupId)}`;\n\t\t\t\tconst groupResponse = await session.request(\"GET\", groupPath);\n\n\t\t\t\tif (groupResponse.status === 404) {\n\t\t\t\t\tawait groupResponse.text().catch(() => {});\n\t\t\t\t\t// Group referenced but document missing - include with minimal info\n\t\t\t\t\treturn { id: groupId, name: \"\", devices: [] };\n\t\t\t\t}\n\n\t\t\t\tconst groupDoc = (await groupResponse.json()) as { fields?: FirestoreFields };\n\t\t\t\tconst groupFields = groupDoc.fields ?? {};\n\t\t\t\treturn {\n\t\t\t\t\tid: groupId,\n\t\t\t\t\tname: getString(groupFields, \"name\") ?? \"\",\n\t\t\t\t\tdevices: getStringArray(groupFields, \"devices\") ?? [],\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\n\t\treturn groups;\n\t}\n\n\t/**\n\t * Create a new device group in the account's deviceGroups subcollection.\n\t */\n\tasync createDeviceGroup(name: string, devices: string[]): Promise<DeviceGroup> {\n\t\tif (typeof name !== \"string\" || name.trim().length === 0) {\n\t\t\tthrow new Error(\"Group name must be a non-empty string\");\n\t\t}\n\t\tif (name.length > 200) {\n\t\t\tthrow new Error(\"Group name exceeds maximum length of 200 characters\");\n\t\t}\n\t\tfor (const d of devices) {\n\t\t\tvalidateSerial(d);\n\t\t}\n\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\t\tconst path = `documents/accounts/${encodeURIComponent(accountId)}/deviceGroups`;\n\t\tconst body = {\n\t\t\tfields: {\n\t\t\t\tname: { stringValue: name },\n\t\t\t\tdevices: {\n\t\t\t\t\tarrayValue: {\n\t\t\t\t\t\tvalues: devices.map((d) => ({ stringValue: d })),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tconst response = await session.request(\"POST\", path, body);\n\t\tif (!response.ok) {\n\t\t\tawait response.text().catch(() => {});\n\t\t\tthrow new Error(`Failed to create group: ${response.status}`);\n\t\t}\n\t\tconst doc = (await response.json()) as { name?: string; fields?: FirestoreFields };\n\t\tconst docName = doc.name ?? \"\";\n\t\tconst id = docName.split(\"/\").pop() ?? \"\";\n\t\treturn { id, name, devices };\n\t}\n\n\t/**\n\t * Delete a device group.\n\t *\n\t * @throws Error - This method is not yet supported as the full device group\n\t * API is not documented.\n\t */\n\tasync deleteDeviceGroup(_groupId: string): Promise<void> {\n\t\tthrow new Error(\n\t\t\t\"deleteDeviceGroup is not yet supported: the device group write API is not fully documented\",\n\t\t);\n\t}\n\n\t/**\n\t * Add a device to an existing group by updating the group's devices array.\n\t */\n\tasync addDeviceToGroup(groupId: string, serial: string): Promise<void> {\n\t\tif (!groupId || typeof groupId !== \"string\") {\n\t\t\tthrow new Error(\"groupId is required\");\n\t\t}\n\t\tvalidateSerial(serial);\n\n\t\tconst groups = await this.getDeviceGroups();\n\t\tconst group = groups.find((g) => g.id === groupId);\n\t\tif (!group) throw new NotFoundError(`Group ${groupId} not found`);\n\t\tif (group.devices.includes(serial)) return;\n\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\t\tconst newDevices = [...group.devices, serial];\n\t\tconst path = `documents/accounts/${encodeURIComponent(accountId)}/deviceGroups/${encodeURIComponent(groupId)}?updateMask.fieldPaths=devices`;\n\t\tconst body = {\n\t\t\tfields: {\n\t\t\t\tdevices: {\n\t\t\t\t\tarrayValue: {\n\t\t\t\t\t\tvalues: newDevices.map((d) => ({ stringValue: d })),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tconst response = await session.request(\"PATCH\", path, body);\n\t\tawait response.text().catch(() => {});\n\t}\n\n\t/**\n\t * Remove a device from a group by updating the group's devices array.\n\t */\n\tasync removeDeviceFromGroup(groupId: string, serial: string): Promise<void> {\n\t\tif (!groupId || typeof groupId !== \"string\") {\n\t\t\tthrow new Error(\"groupId is required\");\n\t\t}\n\t\tvalidateSerial(serial);\n\n\t\tconst groups = await this.getDeviceGroups();\n\t\tconst group = groups.find((g) => g.id === groupId);\n\t\tif (!group) throw new NotFoundError(`Group ${groupId} not found`);\n\t\tif (!group.devices.includes(serial)) return;\n\n\t\tconst accountId = await this.resolveAccountId();\n\t\tconst session = await this.ensureSession();\n\t\tconst newDevices = group.devices.filter((d) => d !== serial);\n\t\tconst path = `documents/accounts/${encodeURIComponent(accountId)}/deviceGroups/${encodeURIComponent(groupId)}?updateMask.fieldPaths=devices`;\n\t\tconst body = {\n\t\t\tfields: {\n\t\t\t\tdevices: {\n\t\t\t\t\tarrayValue: {\n\t\t\t\t\t\tvalues: newDevices.map((d) => ({ stringValue: d })),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tconst response = await session.request(\"PATCH\", path, body);\n\t\tawait response.text().catch(() => {});\n\t}\n\n\t// ─── Fan Controller ──────────────────────────────────────────────────────────\n\n\t/** Get the current fan/blower controller state for a device. Returns `null` if the device has no fan. */\n\tasync getFanState(serial: string): Promise<FanSettings | null> {\n\t\tconst device = await this.getDevice(serial);\n\t\treturn device.fan;\n\t}\n\n\t/** Set the fan controller target temperature. */\n\tasync setFanTarget(serial: string, targetTemp: number): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\tif (!Number.isFinite(targetTemp)) {\n\t\t\tthrow new Error(\"targetTemp must be a finite number\");\n\t\t}\n\t\treturn this.callAction(\"deviceStateUpdate\", {\n\t\t\tdeviceId: serial,\n\t\t\tfan: { setTemp: targetTemp },\n\t\t});\n\t}\n\n\t/** Enable or disable the fan controller connection. */\n\tasync setFanEnabled(serial: string, enabled: boolean): Promise<ActionResult> {\n\t\tvalidateSerial(serial);\n\t\treturn this.callAction(\"deviceStateUpdate\", {\n\t\t\tdeviceId: serial,\n\t\t\tfan: { connection: enabled },\n\t\t});\n\t}\n\n\t/**\n\t * @deprecated Use the top-level methods `startSession`, `endSession`,\n\t * `clearSession`, `resetMinMax`, and `clearEvents` instead.\n\t */\n\treadonly actions = {\n\t\tstartSession: (serial: string, label?: string): Promise<ActionResult> =>\n\t\t\tthis.startSession(serial, label),\n\t\tendSession: (serial: string): Promise<ActionResult> => this.endSession(serial),\n\t\tclearSession: (serial: string): Promise<ActionResult> => this.clearSession(serial),\n\t\tresetMinMax: (serial: string, channel: number): Promise<ActionResult> =>\n\t\t\tthis.resetMinMax(serial, channel),\n\t\tclearEvents: (serial: string): Promise<ActionResult> => this.clearEvents(serial),\n\t};\n\n\t/**\n\t * Subscribe to real-time channel updates for a device via polling.\n\t *\n\t * Immediately fetches the current state, then polls at the configured\n\t * interval. The callback is only invoked when a channel's value, units,\n\t * or status actually changes (deduplication).\n\t *\n\t * @example\n\t * ```ts\n\t * const sub = client.subscribe(\"ABC123\", (update) => {\n\t *   console.log(`Ch${update.channel}: ${update.value}°${update.units}`);\n\t * }, { intervalMs: 5000, onError: console.error });\n\t *\n\t * // Later: stop polling\n\t * sub.unsubscribe();\n\t * ```\n\t */\n\tsubscribe(\n\t\tserial: string,\n\t\tcallback: ChannelUpdateCallback,\n\t\toptions?: SubscriptionOptions & { onError?: ErrorCallback },\n\t): Subscription {\n\t\tif (this.closed) {\n\t\t\tthrow new Error(\"Client is closed\");\n\t\t}\n\t\tvalidateSerial(serial);\n\t\tconst sub = createSubscription(serial, (s) => this.getAllDeviceChannels(s), callback, options);\n\t\tconst tracked: Subscription = {\n\t\t\tunsubscribe: () => {\n\t\t\t\tsub.unsubscribe();\n\t\t\t\tthis.activeSubscriptions.delete(tracked);\n\t\t\t},\n\t\t};\n\t\tthis.activeSubscriptions.add(tracked);\n\t\treturn tracked;\n\t}\n\n\t/** Close the client and release resources. Stops all active subscriptions. */\n\tclose(): void {\n\t\tthis.closed = true;\n\t\tfor (const sub of this.activeSubscriptions) sub.unsubscribe();\n\t\tthis.activeSubscriptions.clear();\n\t\tthis.session?.close();\n\t\tthis.session = null;\n\t\tthis.sessionPromise = null;\n\t\tthis.cachedAccountId = null;\n\t}\n\n\tprivate async ensureSession(): Promise<AuthSession> {\n\t\tif (this.closed) {\n\t\t\tthrow new Error(\"Client is closed\");\n\t\t}\n\t\tif (this.session) return this.session;\n\t\tif (!this.sessionPromise) {\n\t\t\tthis.sessionPromise = createAuthSession({\n\t\t\t\temail: this.config.email,\n\t\t\t\tpassword: this.config.password,\n\t\t\t\tapiKey: this.config.apiKey,\n\t\t\t\tappId: this.config.appId,\n\t\t\t\ttokenCachePath: this.config.tokenCachePath,\n\t\t\t\tretry: this.config.retry,\n\t\t\t})\n\t\t\t\t.then((s) => {\n\t\t\t\t\tif (this.closed) {\n\t\t\t\t\t\ts.close();\n\t\t\t\t\t\treturn s;\n\t\t\t\t\t}\n\t\t\t\t\tthis.session = s;\n\t\t\t\t\treturn s;\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tthis.sessionPromise = null;\n\t\t\t\t\tthrow err;\n\t\t\t\t});\n\t\t}\n\t\treturn this.sessionPromise;\n\t}\n\n\tprivate async resolveAccountId(): Promise<string> {\n\t\tif (this.cachedAccountId) return this.cachedAccountId;\n\t\tconst user = await this.getUser();\n\t\tif (!user.accountId) {\n\t\t\tthrow new NotFoundError(\"User has no associated account\");\n\t\t}\n\t\tthis.cachedAccountId = user.accountId;\n\t\treturn this.cachedAccountId;\n\t}\n\n\t/** Fetch a Firestore document and return its fields, or throw NotFoundError. */\n\tprivate async fetchDocFields(path: string, notFoundMsg: string): Promise<FirestoreFields> {\n\t\tconst session = await this.ensureSession();\n\t\tconst response = await session.request(\"GET\", path);\n\t\tif (response.status === 404) {\n\t\t\tawait response.text().catch(() => {});\n\t\t\tthrow new NotFoundError(notFoundMsg);\n\t\t}\n\t\tconst doc = (await response.json()) as { fields?: FirestoreFields };\n\t\treturn doc.fields ?? {};\n\t}\n\n\t/** Call a Cloud Function and return the result as an ActionResult. */\n\tprivate async callAction(\n\t\tfunctionName: string,\n\t\tdata: Record<string, unknown>,\n\t): Promise<ActionResult> {\n\t\tconst session = await this.ensureSession();\n\t\tconst result = await session.callFunction(functionName, data);\n\t\treturn toActionResult(result);\n\t}\n}\n\nfunction parseDevice(fields: FirestoreFields): Device {\n\tconst gatewayId = getString(fields, \"gatewayId\");\n\tconst gatewayFields = gatewayId != null ? fields : null;\n\n\tconst fanMap = getMapFields(fields, \"fan\");\n\tconst bigQueryMap = getMapFields(fields, \"bigQuery\");\n\n\treturn {\n\t\tserial: getString(fields, \"serial\") ?? \"\",\n\t\tdeviceId: getString(fields, \"deviceId\"),\n\t\tlabel: sanitizeLabel(getString(fields, \"label\")),\n\t\ttype: sanitizeLabel(getString(fields, \"type\")),\n\t\tdevice: getString(fields, \"device\"),\n\t\tstatus: sanitizeLabel(getString(fields, \"status\")),\n\t\tbattery: getNumber(fields, \"battery\"),\n\t\tbatteryState: getString(fields, \"battery_state\") ?? getString(fields, \"batteryState\"),\n\t\twifiStrength: getNumber(fields, \"wifi_stength\") ?? getNumber(fields, \"wifiStrength\"),\n\t\tfirmware: sanitizeLabel(getString(fields, \"firmware\")),\n\t\tcolor: getString(fields, \"color\"),\n\t\tthumbnail: getString(fields, \"thumbnail\"),\n\t\tdeviceDisplayUnits: getString(fields, \"deviceDisplayUnits\"),\n\t\tiotDeviceId: getString(fields, \"iotDeviceId\"),\n\t\tiotCoreDeviceBlocked: getBoolean(fields, \"iotCoreDeviceBlocked\"),\n\t\trecordingIntervalInSeconds: getNumber(fields, \"recordingIntervalInSeconds\"),\n\t\ttransmitIntervalInSeconds: getNumber(fields, \"transmitIntervalInSeconds\"),\n\t\treadInterval: getNumber(fields, \"readInterval\"),\n\t\theartbeatInterval: getNumber(fields, \"heartbeatInterval\"),\n\t\ttemperatureDeltaTrigger: getNumber(fields, \"temperatureDeltaTrigger\"),\n\t\tpendingLoad: getBoolean(fields, \"pendingLoad\"),\n\t\tbatteryAlertSent: getBoolean(fields, \"batteryAlertSent\"),\n\t\tlastSeen: getTimestamp(fields, \"last_seen\") ?? getTimestamp(fields, \"lastSeen\"),\n\t\tlastTelemetrySaved: getTimestamp(fields, \"lastTelemetrySaved\"),\n\t\tlatestReading: getTimestamp(fields, \"latestReading\"),\n\t\tlastWifiConnection: getTimestamp(fields, \"lastWifiConnection\"),\n\t\tlastBluetoothConnection: getTimestamp(fields, \"lastBluetoothConnection\"),\n\t\tsessionStart: getTimestamp(fields, \"sessionStart\"),\n\t\tsessionLabel: sanitizeLabel(getString(fields, \"sessionLabel\")),\n\t\tlastArchive: getTimestamp(fields, \"lastArchive\"),\n\t\tlastPurged: getTimestamp(fields, \"lastPurged\"),\n\t\tassignedToAccountOn: getTimestamp(fields, \"assignedToAccountOn\"),\n\t\taccountId: getString(fields, \"accountId\"),\n\t\tnotes: sanitizeLabel(getString(fields, \"notes\")),\n\t\tpublic: getBoolean(fields, \"public\"),\n\t\tpublicLink: getString(fields, \"publicLink\"),\n\t\tsearModeEnabled: getBoolean(fields, \"searModeEnabled\"),\n\t\tshowSensorChannels: getBoolean(fields, \"showSensorChannels\"),\n\t\tringColors: getStringArray(fields, \"ringColors\"),\n\t\tgateway: gatewayFields\n\t\t\t? {\n\t\t\t\t\tgatewayId,\n\t\t\t\t\trssi: getNumber(gatewayFields, \"gatewayRSSI\"),\n\t\t\t\t\tlastSeen: getTimestamp(gatewayFields, \"gatewayLastSeen\"),\n\t\t\t\t\tswitchedAt: getTimestamp(gatewayFields, \"gatewaySwitchLastAt\"),\n\t\t\t\t\tlastPacketId: getNumber(gatewayFields, \"lastPacketId\"),\n\t\t\t\t}\n\t\t\t: null,\n\t\tfan: fanMap\n\t\t\t? {\n\t\t\t\t\tconnected: getBoolean(fanMap, \"connected\") ?? false,\n\t\t\t\t\tconnection: getBoolean(fanMap, \"connection\") ?? false,\n\t\t\t\t\tsetTemp: getNumber(fanMap, \"setTemp\"),\n\t\t\t\t\tfanChannel: getString(fanMap, \"fan_channel\"),\n\t\t\t\t\tstate: getNumber(fanMap, \"state\"),\n\t\t\t\t}\n\t\t\t: null,\n\t\tbigQuery: bigQueryMap\n\t\t\t? {\n\t\t\t\t\tdatasetId: getString(bigQueryMap, \"datasetId\") ?? \"\",\n\t\t\t\t\ttableId: getString(bigQueryMap, \"tableId\") ?? \"\",\n\t\t\t\t}\n\t\t\t: null,\n\t};\n}\n\nfunction parseDeviceChannel(fields: FirestoreFields): DeviceChannel {\n\treturn {\n\t\tvalue: getNumber(fields, \"value\"),\n\t\tunits: sanitizeUnits(getString(fields, \"units\")),\n\t\tlabel: sanitizeLabel(getString(fields, \"label\")),\n\t\tstatus: sanitizeLabel(getString(fields, \"status\")),\n\t\ttype: sanitizeLabel(getString(fields, \"type\")),\n\t\tnumber: getString(fields, \"number\"),\n\t\tenabled: getBoolean(fields, \"enabled\"),\n\t\tcolor: getString(fields, \"color\"),\n\t\tlastSeen: getTimestamp(fields, \"last_seen\") ?? getTimestamp(fields, \"lastSeen\"),\n\t\tlastTelemetrySaved: getTimestamp(fields, \"lastTelemetrySaved\"),\n\t\tlastEventId: getString(fields, \"lastEventId\"),\n\t\tshowAvgTemp: getBoolean(fields, \"showAvgTemp\"),\n\t\testimatedAlarmStatus: getString(fields, \"estimatedAlarmStatus\"),\n\t\trateOfChange: getNumber(fields, \"rateOfChange\"),\n\t\trateOfChangeUnit: getString(fields, \"rateOfChangeUnit\"),\n\t\talarmHigh: parseAlarm(getMapFields(fields, \"alarmHigh\")),\n\t\talarmLow: parseAlarm(getMapFields(fields, \"alarmLow\")),\n\t\tminimum: parseMinMaxReading(getMapFields(fields, \"minimum\")),\n\t\tmaximum: parseMinMaxReading(getMapFields(fields, \"maximum\")),\n\t};\n}\n\nfunction parseAlarm(fields: FirestoreFields | null): Alarm | null {\n\tif (!fields) return null;\n\treturn {\n\t\tenabled: getBoolean(fields, \"enabled\") ?? false,\n\t\talarming: getBoolean(fields, \"alarming\") ?? false,\n\t\tmuted: getBoolean(fields, \"muted\"),\n\t\tvalue: getNumber(fields, \"value\"),\n\t\tunits: sanitizeUnits(getString(fields, \"units\")),\n\t\tlastNotified: getTimestamp(fields, \"lastNotified\"),\n\t};\n}\n\nfunction parseMinMaxReading(fields: FirestoreFields | null): MinMaxReading | null {\n\tif (!fields) return null;\n\tconst readingFields = getMapFields(fields, \"reading\");\n\treturn {\n\t\tvalue: readingFields ? getNumber(readingFields, \"value\") : null,\n\t\tunits: readingFields ? sanitizeUnits(getString(readingFields, \"units\")) : null,\n\t\tdate: getTimestamp(fields, \"dateReading\"),\n\t};\n}\n\nfunction toArray(value: string | string[] | undefined): string[] | undefined {\n\tif (value === undefined) return undefined;\n\treturn Array.isArray(value) ? value : [value];\n}\n\nfunction applyDeviceFilter(devices: Device[], filter: DeviceFilter): Device[] {\n\tconst serials = toArray(filter.serial);\n\tconst types = toArray(filter.type);\n\tconst labels = toArray(filter.label);\n\tconst statuses = toArray(filter.status);\n\tconst now = Date.now();\n\n\treturn devices.filter((device) => {\n\t\tif (serials && !serials.includes(device.serial)) return false;\n\t\tif (types && (!device.type || !types.includes(device.type))) return false;\n\t\tif (labels && (!device.label || !labels.includes(device.label))) return false;\n\t\tif (statuses && (!device.status || !statuses.includes(device.status))) return false;\n\t\tif (filter.activeWithinMinutes != null) {\n\t\t\tif (!device.lastSeen) return false;\n\t\t\tconst ageMs = now - device.lastSeen.getTime();\n\t\t\tif (ageMs > filter.activeWithinMinutes * 60_000) return false;\n\t\t}\n\t\treturn true;\n\t});\n}\n\nfunction extractDocId(name: string | undefined): string {\n\tif (!name) return \"\";\n\tconst parts = name.split(\"/\");\n\treturn parts[parts.length - 1] ?? \"\";\n}\n\nfunction parseStringBooleanMap(fields: FirestoreFields | null): Record<string, boolean> | null {\n\tif (!fields) return null;\n\tconst result: Record<string, boolean> = {};\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (\"booleanValue\" in value) {\n\t\t\tresult[key] = value.booleanValue;\n\t\t}\n\t}\n\treturn Object.keys(result).length > 0 ? result : null;\n}\n\nfunction parseNotificationSettings(fields: FirestoreFields | null): NotificationSettings | null {\n\tif (!fields) return null;\n\treturn {\n\t\tenabled: getBoolean(fields, \"enabled\") ?? false,\n\t\tcontinuousAlerts: getBoolean(fields, \"continuousAlerts\") ?? false,\n\t\temailNotification: getBoolean(fields, \"emailNotification\") ?? false,\n\t\tsmsNotification: getBoolean(fields, \"smsNotification\") ?? false,\n\t\tdeviceNotification: getBoolean(fields, \"deviceNotification\") ?? false,\n\t};\n}\n\nfunction parseDeviceEvent(fields: FirestoreFields, id: string): DeviceEvent {\n\treturn {\n\t\tid,\n\t\teventType:\n\t\t\tsanitizeLabel(getString(fields, \"EventType\") ?? getString(fields, \"eventType\")) ?? \"\",\n\t\tseverity: getNumber(fields, \"Severity\") ?? getNumber(fields, \"severity\") ?? 0,\n\t\teventTime:\n\t\t\tgetTimestamp(fields, \"EventTime\") ??\n\t\t\tgetTimestamp(fields, \"eventTime\") ??\n\t\t\tgetTimestampFromInt(fields, \"EventTime\") ??\n\t\t\tgetTimestampFromInt(fields, \"eventTime\") ??\n\t\t\tnew Date(0),\n\t\tdeviceId: getString(fields, \"deviceId\") ?? \"\",\n\t\tchannelId: getString(fields, \"channelId\"),\n\t\taccountId: getString(fields, \"accountId\") ?? \"\",\n\t\tvalueBefore: sanitizeLabel(\n\t\t\tgetString(fields, \"ValueBefore\") ?? getString(fields, \"valueBefore\"),\n\t\t),\n\t\tvalueAfter: sanitizeLabel(getString(fields, \"ValueAfter\") ?? getString(fields, \"valueAfter\")),\n\t\tgroups: getStringArray(fields, \"groups\"),\n\t};\n}\n\n/** Try to parse a timestamp stored as integerValue (epoch millis or seconds). */\nfunction getTimestampFromInt(fields: FirestoreFields, key: string): Date | null {\n\tconst n = getNumber(fields, key);\n\tif (n == null || n < 0) return null;\n\t// 1e12 ms ≈ Sep 2001; legitimate epoch-second timestamps stay below 1e10 until ~2286\n\tconst ms = n < 1e12 ? n * 1000 : n;\n\tconst date = new Date(ms);\n\treturn Number.isNaN(date.getTime()) ? null : date;\n}\n\nfunction parseArchive(fields: FirestoreFields, id: string): Archive {\n\tconst channelsRaw = getArray(fields, \"channels\");\n\tlet channels: ArchiveChannel[] | null = null;\n\n\tif (channelsRaw) {\n\t\tchannels = [];\n\t\tfor (const item of channelsRaw) {\n\t\t\tif (\"mapValue\" in item && item.mapValue.fields) {\n\t\t\t\tchannels.push(parseArchiveChannel(item.mapValue.fields));\n\t\t\t}\n\t\t}\n\t\tif (channels.length === 0) channels = null;\n\t}\n\n\treturn {\n\t\tid,\n\t\tstart: getTimestamp(fields, \"start\"),\n\t\tend: getTimestamp(fields, \"end\"),\n\t\tcount: getNumber(fields, \"count\"),\n\t\ttype: sanitizeLabel(getString(fields, \"type\")),\n\t\tlabel: sanitizeLabel(getString(fields, \"label\")),\n\t\tdeviceLabel: sanitizeLabel(getString(fields, \"deviceLabel\")),\n\t\tnotes: sanitizeLabel(getString(fields, \"notes\")),\n\t\tcreatedOn: getTimestamp(fields, \"createdOn\"),\n\t\tpublic: getBoolean(fields, \"public\"),\n\t\tpublicLink: getString(fields, \"publicLink\"),\n\t\tfilename: getString(fields, \"filename\"),\n\t\tchannels,\n\t};\n}\n\nfunction parseArchiveChannel(fields: FirestoreFields): ArchiveChannel {\n\tconst readingsRaw = getArray(fields, \"recentReadings\");\n\tconst recentReadings: TemperatureReading[] = [];\n\n\tif (readingsRaw) {\n\t\tfor (const item of readingsRaw) {\n\t\t\tif (\"mapValue\" in item && item.mapValue.fields) {\n\t\t\t\tconst rf = item.mapValue.fields;\n\t\t\t\t// ThermoWorks uses short field names: v (value), ts (timestamp), u (units)\n\t\t\t\tconst rawValue =\n\t\t\t\t\tgetNumber(rf, \"value\") ?? getNumber(rf, \"v\") ?? parseFloat(getString(rf, \"v\") ?? \"\");\n\t\t\t\tconst timestamp = getTimestamp(rf, \"timestamp\") ?? getTimestamp(rf, \"ts\");\n\t\t\t\tconst units = sanitizeUnits(getString(rf, \"units\") ?? getString(rf, \"u\"));\n\t\t\t\tif (rawValue != null && !Number.isNaN(rawValue) && timestamp != null && units != null) {\n\t\t\t\t\trecentReadings.push({ value: rawValue, timestamp, units });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tnumber: getString(fields, \"number\"),\n\t\tlabel: sanitizeLabel(getString(fields, \"label\")),\n\t\tunits: sanitizeUnits(getString(fields, \"units\")),\n\t\tvalue: getNumber(fields, \"value\"),\n\t\tstatus: sanitizeLabel(getString(fields, \"status\")),\n\t\tenabled: getBoolean(fields, \"enabled\"),\n\t\tcolor: getString(fields, \"color\"),\n\t\ttype: sanitizeLabel(getString(fields, \"type\")),\n\t\talarmHigh: parseAlarm(getMapFields(fields, \"alarmHigh\")),\n\t\talarmLow: parseAlarm(getMapFields(fields, \"alarmLow\")),\n\t\tminimum: parseMinMaxReading(getMapFields(fields, \"minimum\")),\n\t\tmaximum: parseMinMaxReading(getMapFields(fields, \"maximum\")),\n\t\trecentReadings,\n\t};\n}\n\nfunction parseCalibrationRecord(fields: FirestoreFields, id: string): CalibrationRecord {\n\tconst lowPoints = getArray(fields, \"lowPointAdjustments\");\n\tconst highPoints = getArray(fields, \"highPointReference\");\n\n\treturn {\n\t\tcalibrationId: id,\n\t\tcalibrationDate: getTimestamp(fields, \"calibrationDate\"),\n\t\tdeviceId: getString(fields, \"deviceId\") ?? \"\",\n\t\tsessionId: getString(fields, \"sessionId\"),\n\t\tperformedBy: getString(fields, \"performedBy\"),\n\t\tmanager: getString(fields, \"manager\"),\n\t\treferenceDetail: getString(fields, \"referenceDetail\"),\n\t\tstatedAccuracy: getString(fields, \"statedAccuracy\"),\n\t\tambientTemperature: getString(fields, \"ambientTemperature\"),\n\t\tambientHumidity: getString(fields, \"ambientHumidity\"),\n\t\tresult: getString(fields, \"result\"),\n\t\tlowPointAdjustments: parseCalibrationPoints(lowPoints),\n\t\thighPointReference: parseCalibrationPoints(highPoints),\n\t};\n}\n\nfunction parseCalibrationPoints(values: FirestoreValue[] | null): CalibrationPoint[] {\n\tif (!values) return [];\n\tconst points: CalibrationPoint[] = [];\n\tfor (const item of values) {\n\t\tif (\"mapValue\" in item && item.mapValue.fields) {\n\t\t\tconst f = item.mapValue.fields;\n\t\t\tpoints.push({\n\t\t\t\tchannel: getNumber(f, \"channel\") ?? 0,\n\t\t\t\tvalue: getNumber(f, \"value\") ?? 0,\n\t\t\t\tunits: sanitizeUnits(getString(f, \"units\")) ?? \"\",\n\t\t\t\treferenceValue: getNumber(f, \"referenceValue\") ?? 0,\n\t\t\t\tdeviation: getNumber(f, \"deviation\") ?? 0,\n\t\t\t\ttrimValue: getNumber(f, \"trimValue\"),\n\t\t\t\tresult: getString(f, \"result\") ?? \"\",\n\t\t\t});\n\t\t}\n\t}\n\treturn points;\n}\n\nfunction toActionResult(result: unknown): ActionResult {\n\tconst data = result as {\n\t\tsuccess?: boolean;\n\t\terror?: string;\n\t\tstatus?: string;\n\t\tmessage?: string;\n\t} | null;\n\tif (data && typeof data === \"object\") {\n\t\t// Detect error envelope: { status: \"error\", message: \"...\" }\n\t\tif (data.status === \"error\" || data.error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\tdata: null,\n\t\t\t\terror: data.error ?? data.message ?? \"Action failed\",\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: data.success !== false,\n\t\t\tdata: data,\n\t\t\terror: null,\n\t\t};\n\t}\n\treturn { success: true, data: result ?? null, error: null };\n}\n\nfunction toShareResult(result: unknown): ShareResult {\n\tconst data = result as {\n\t\tsuccess?: boolean;\n\t\tpublicLink?: string;\n\t\terror?: string;\n\t\tstatus?: string;\n\t\tmessage?: string;\n\t} | null;\n\tif (data && typeof data === \"object\") {\n\t\tif (data.status === \"error\" || data.error) {\n\t\t\treturn { success: false };\n\t\t}\n\t\treturn {\n\t\t\tsuccess: data.success !== false,\n\t\t\tpublicLink: data.publicLink ?? undefined,\n\t\t};\n\t}\n\treturn { success: true };\n}\n\nfunction parseDeviceHistory(serial: string, result: unknown): DeviceHistory {\n\tconst readings: HistoricalReading[] = [];\n\tif (result && typeof result === \"object\" && \"readings\" in result) {\n\t\tconst raw = (result as { readings?: unknown }).readings;\n\t\tif (Array.isArray(raw)) {\n\t\t\tfor (const entry of raw) {\n\t\t\t\tif (entry && typeof entry === \"object\") {\n\t\t\t\t\tconst r = entry as { v?: string; ts?: string; u?: string };\n\t\t\t\t\tconst value = r.v != null ? Number(r.v) : Number.NaN;\n\t\t\t\t\tif (!Number.isNaN(value) && r.ts && r.u) {\n\t\t\t\t\t\treadings.push({ value, timestamp: r.ts, units: r.u });\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn { deviceId: serial, readings };\n}\n\nfunction buildAlarmMapValue(opts: AlarmThresholdOptions): FirestoreValue {\n\tconst mapFields: Record<string, FirestoreValue> = {\n\t\tvalue: { doubleValue: opts.value },\n\t};\n\tif (opts.units !== undefined) {\n\t\tmapFields.units = { stringValue: opts.units };\n\t}\n\tif (opts.enabled !== undefined) {\n\t\tmapFields.enabled = { booleanValue: opts.enabled };\n\t}\n\tif (opts.muted !== undefined) {\n\t\tmapFields.muted = { booleanValue: opts.muted };\n\t}\n\treturn { mapValue: { fields: mapFields } };\n}\n\nconst BYTE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"] as const;\n\nfunction formatBytes(bytes: number): string {\n\tif (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n\tconst exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);\n\tconst value = bytes / 1024 ** exponent;\n\tconst formatted = exponent === 0 ? value.toString() : value.toFixed(2);\n\treturn `${formatted} ${BYTE_UNITS[exponent]}`;\n}\n","/** A device entry in the ThermoWorks statusline configuration. */\nexport interface DeviceEntry {\n\tserial: string;\n\tlabel: string;\n\t/** Channel numbers to display, or \"avg\" for average temperature. */\n\tchannels: number[] | \"avg\";\n}\n\n/**\n * Persistent local channel labels keyed by `\"serial:channelNumber\"`.\n *\n * Example: `{ \"ABC123:1\": \"Pit\", \"ABC123:2\": \"Brisket\" }`\n */\nexport type ChannelLabelMap = Record<string, string>;\n\n/** Shared configuration for ThermoWorks CLI and VS Code statusline. */\nexport interface StatuslineConfig {\n\t/** Devices to show in statusline. */\n\tdevices: DeviceEntry[];\n\t/** API cache duration in seconds. */\n\trefreshSeconds: number;\n\t/** Persistent local channel labels, keyed by `\"serial:channelNumber\"`. */\n\tchannelLabels?: ChannelLabelMap;\n}\n\n/** Maximum length for a user-provided channel label. */\nexport const MAX_CHANNEL_LABEL_LENGTH = 50;\n\n/** Default configuration values. */\nexport const DEFAULT_STATUSLINE_CONFIG: StatuslineConfig = {\n\tdevices: [],\n\trefreshSeconds: 30,\n};\n\n/**\n * Strip ANSI escape sequences and control characters from a label string.\n *\n * Labels reach the terminal, the DOM, and MCP/LLM output, so they must be\n * treated as untrusted input. This function is the single sanitization point\n * shared by all surfaces.\n */\nexport function sanitizeLabel(value: string | null | undefined): string | null {\n\tif (value == null) return null;\n\t// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional - stripping ANSI/control chars from untrusted labels\n\tconst stripped = value.replace(/[\\x00-\\x1f\\x7f-\\x9f](\\[[0-9;]*[A-Za-z])?/g, \"\");\n\treturn stripped.slice(0, MAX_CHANNEL_LABEL_LENGTH);\n}\n\n/**\n * Build the config map key for a channel label.\n *\n * @param serial - Device serial number.\n * @param channelNumber - 1-indexed channel number (string or number).\n */\nexport function channelLabelKey(serial: string, channelNumber: string | number): string {\n\treturn `${serial}:${channelNumber}`;\n}\n\n/**\n * Resolve the display name for a channel, applying the three-tier fallback:\n *\n * 1. Local custom label (from `channelLabels` config map)\n * 2. Cloud channel label (`channel.label` from the API)\n * 3. Default `\"Ch N\"` derived from the channel number\n *\n * @param serial - Device serial number.\n * @param channel - The channel object from the SDK.\n * @param channelLabels - The persisted label map (may be undefined).\n * @param index - Zero-based index, used when `channel.number` is null.\n * @returns The resolved display name.\n */\nexport function resolveChannelLabel(\n\tserial: string,\n\tchannel: { label?: string | null; number?: string | null },\n\tchannelLabels: ChannelLabelMap | undefined,\n\tindex: number,\n): string {\n\tconst chNum = channel.number ?? String(index + 1);\n\tconst key = channelLabelKey(serial, chNum);\n\tconst custom = channelLabels?.[key];\n\tif (custom) return custom;\n\tif (channel.label) return channel.label;\n\treturn `Ch ${chNum}`;\n}\n\n/** Validate a raw object as a partial StatuslineConfig. */\nexport function isValidStatuslineConfig(raw: unknown): raw is Partial<StatuslineConfig> {\n\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return false;\n\tconst obj = raw as Record<string, unknown>;\n\tif (\n\t\tobj.refreshSeconds !== undefined &&\n\t\t(typeof obj.refreshSeconds !== \"number\" || obj.refreshSeconds < 1)\n\t)\n\t\treturn false;\n\tif (obj.devices !== undefined && !Array.isArray(obj.devices)) return false;\n\tif (obj.channelLabels !== undefined && !isValidChannelLabelMap(obj.channelLabels)) return false;\n\treturn true;\n}\n\n/** Validate a single device entry. */\nexport function isValidDeviceEntry(entry: unknown): entry is DeviceEntry {\n\tif (typeof entry !== \"object\" || entry === null) return false;\n\tconst obj = entry as Record<string, unknown>;\n\tif (typeof obj.serial !== \"string\" || !obj.serial) return false;\n\tif (typeof obj.label !== \"string\") return false;\n\tif (obj.channels === \"avg\") return true;\n\tif (\n\t\tArray.isArray(obj.channels) &&\n\t\tobj.channels.length > 0 &&\n\t\tobj.channels.every((ch) => typeof ch === \"number\")\n\t)\n\t\treturn true;\n\treturn false;\n}\n\n/** Validate a channel label map: must be a plain object with string values. */\nexport function isValidChannelLabelMap(raw: unknown): raw is ChannelLabelMap {\n\tif (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) return false;\n\treturn Object.values(raw as Record<string, unknown>).every((v) => typeof v === \"string\");\n}\n","// ─── Cooling Safety (FDA two-stage rule) ─────────────────────────────────────\n\n/**\n * The FDA Food Code two-stage cooling rule. Cooked food must drop from\n * {@link FDA_STAGE1_START_F} to {@link FDA_STAGE1_END_F} within two hours, and\n * all the way to {@link FDA_STAGE2_END_F} within six hours total, both measured\n * from the moment the food first drops into the danger zone.\n */\nexport const FDA_STAGE1_START_F = 135;\nexport const FDA_STAGE1_END_F = 70;\nexport const FDA_STAGE2_END_F = 41;\nconst DEFAULT_STAGE1_LIMIT_HOURS = 2;\nconst DEFAULT_STAGE2_LIMIT_HOURS = 6;\n\n/** A single cooling data point: a Fahrenheit reading at an elapsed minute mark. */\nexport interface CoolingSample {\n\t/** Temperature in Fahrenheit. */\n\ttempF: number;\n\t/** Minutes elapsed on a shared, non-decreasing timeline (>= 0). */\n\tminutes: number;\n}\n\n/** Options for {@link assessCooling}. */\nexport interface CoolingOptions {\n\t/** Stage one deadline in hours (135F to 70F). Default: 2. */\n\tstage1LimitHours?: number;\n\t/** Total deadline in hours (135F to 41F). Default: 6. */\n\tstage2LimitHours?: number;\n}\n\n/** Outcome for one stage of the cooling rule. */\nexport interface CoolingStageResult {\n\t/** Target temperature in Fahrenheit the food must reach for this stage. */\n\ttargetF: number;\n\t/** Deadline in minutes, measured from danger-zone entry. */\n\tlimitMinutes: number;\n\t/** Whether the target was reached within the supplied data. */\n\treached: boolean;\n\t/** Minutes from entry to reaching the target, or null if not reached. */\n\telapsedMinutes: number | null;\n\t/** Whether the target was reached at or before the deadline. */\n\twithinLimit: boolean;\n\t/**\n\t * Deadline minus elapsed minutes. Positive means time to spare, negative\n\t * means over the deadline. Null if the target was not reached.\n\t */\n\tmarginMinutes: number | null;\n}\n\n/** Result of a two-stage cooling assessment. */\nexport interface CoolingAssessment {\n\t/** Whether any reading was at or below the danger-zone entry temperature. */\n\tentered: boolean;\n\t/** Minute mark of the entry reading, or null if the food never entered. */\n\tentryMinutes: number | null;\n\t/** Temperature at entry in Fahrenheit, or null if the food never entered. */\n\tentryTempF: number | null;\n\t/**\n\t * True when the first reading was already in the danger zone, so the real\n\t * entry time may be earlier than the data shows.\n\t */\n\tentryUncertain: boolean;\n\t/** Stage one: 135F down to 70F. */\n\tstage1: CoolingStageResult;\n\t/** Stage two: 135F down to 41F. */\n\tstage2: CoolingStageResult;\n\t/** True only when both stages were reached within their deadlines. */\n\tsafe: boolean;\n}\n\nfunction missedStage(targetF: number, limitMinutes: number): CoolingStageResult {\n\treturn {\n\t\ttargetF,\n\t\tlimitMinutes,\n\t\treached: false,\n\t\telapsedMinutes: null,\n\t\twithinLimit: false,\n\t\tmarginMinutes: null,\n\t};\n}\n\n/**\n * Find the first sample at or after `fromIndex` whose temperature has fallen to\n * or below `targetF`, and describe how it fared against the deadline.\n */\nfunction evaluateStage(\n\tsamples: CoolingSample[],\n\tfromIndex: number,\n\tentryMinutes: number,\n\ttargetF: number,\n\tlimitMinutes: number,\n): CoolingStageResult {\n\tfor (let i = fromIndex; i < samples.length; i++) {\n\t\tconst sample = samples[i];\n\t\tif (!sample) continue;\n\t\tif (sample.tempF <= targetF) {\n\t\t\tconst elapsed = Math.round(sample.minutes - entryMinutes);\n\t\t\treturn {\n\t\t\t\ttargetF,\n\t\t\t\tlimitMinutes,\n\t\t\t\treached: true,\n\t\t\t\telapsedMinutes: elapsed,\n\t\t\t\twithinLimit: elapsed <= limitMinutes,\n\t\t\t\tmarginMinutes: limitMinutes - elapsed,\n\t\t\t};\n\t\t}\n\t}\n\treturn missedStage(targetF, limitMinutes);\n}\n\n/**\n * Assess a cooling curve against the FDA two-stage rule.\n *\n * The clock starts at the first reading that has dropped to or below\n * {@link FDA_STAGE1_START_F} (the danger-zone entry). Stage one measures the\n * time to reach {@link FDA_STAGE1_END_F}; stage two measures the time to reach\n * {@link FDA_STAGE2_END_F}. Readings must be in chronological order (oldest\n * first) and use Fahrenheit.\n *\n * @throws RangeError when no samples are provided or a limit is not positive.\n */\nexport function assessCooling(\n\tsamples: CoolingSample[],\n\toptions?: CoolingOptions,\n): CoolingAssessment {\n\tif (samples.length === 0) {\n\t\tthrow new RangeError(\"cooling assessment needs at least one reading\");\n\t}\n\n\tconst stage1LimitHours = options?.stage1LimitHours ?? DEFAULT_STAGE1_LIMIT_HOURS;\n\tconst stage2LimitHours = options?.stage2LimitHours ?? DEFAULT_STAGE2_LIMIT_HOURS;\n\tif (stage1LimitHours <= 0) {\n\t\tthrow new RangeError(`stage1LimitHours must be positive, got ${stage1LimitHours}`);\n\t}\n\tif (stage2LimitHours <= 0) {\n\t\tthrow new RangeError(`stage2LimitHours must be positive, got ${stage2LimitHours}`);\n\t}\n\n\tconst stage1Limit = Math.round(stage1LimitHours * 60);\n\tconst stage2Limit = Math.round(stage2LimitHours * 60);\n\n\tlet entryIndex = -1;\n\tfor (let i = 0; i < samples.length; i++) {\n\t\tconst sample = samples[i];\n\t\tif (sample && sample.tempF <= FDA_STAGE1_START_F) {\n\t\t\tentryIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (entryIndex === -1) {\n\t\treturn {\n\t\t\tentered: false,\n\t\t\tentryMinutes: null,\n\t\t\tentryTempF: null,\n\t\t\tentryUncertain: false,\n\t\t\tstage1: missedStage(FDA_STAGE1_END_F, stage1Limit),\n\t\t\tstage2: missedStage(FDA_STAGE2_END_F, stage2Limit),\n\t\t\tsafe: false,\n\t\t};\n\t}\n\n\tconst entry = samples[entryIndex];\n\tif (!entry) {\n\t\tthrow new RangeError(\"cooling assessment could not resolve the entry reading\");\n\t}\n\n\tconst stage1 = evaluateStage(samples, entryIndex, entry.minutes, FDA_STAGE1_END_F, stage1Limit);\n\tconst stage2 = evaluateStage(samples, entryIndex, entry.minutes, FDA_STAGE2_END_F, stage2Limit);\n\n\treturn {\n\t\tentered: true,\n\t\tentryMinutes: entry.minutes,\n\t\tentryTempF: entry.tempF,\n\t\tentryUncertain: entryIndex === 0,\n\t\tstage1,\n\t\tstage2,\n\t\tsafe: stage1.withinLimit && stage2.withinLimit,\n\t};\n}\n","import type { Device, DeviceChannel, DeviceHealth, DeviceHealthIssue } from \"./types.js\";\n\n/** Threshold in milliseconds: 5 minutes for warning-level staleness. */\nconst STALE_WARNING_MS = 5 * 60 * 1000;\n/** Threshold in milliseconds: 30 minutes for critical-level staleness. */\nconst STALE_CRITICAL_MS = 30 * 60 * 1000;\n/** Battery percentage below which a warning is issued. */\nconst BATTERY_WARNING_THRESHOLD = 20;\n/** Battery percentage below which a critical issue is issued. */\nconst BATTERY_CRITICAL_THRESHOLD = 5;\n/** RSSI at or below this dBm is warning-level Wi-Fi health. */\nconst WIFI_WARNING_DBM = -75;\n/** RSSI at or below this dBm is critical Wi-Fi health. */\nconst WIFI_CRITICAL_DBM = -85;\n/** Percentage at or below this value is warning-level Wi-Fi health. */\nconst WIFI_WARNING_PERCENT = 30;\n/** Percentage at or below this value is critical Wi-Fi health. */\nconst WIFI_CRITICAL_PERCENT = 10;\n\nfunction assessWifiStrength(strength: number): DeviceHealthIssue | null {\n\tconst isRssi = strength < 0;\n\tconst detail = isRssi ? `RSSI ${strength} dBm` : `Signal ${strength}%`;\n\tconst critical = isRssi ? strength <= WIFI_CRITICAL_DBM : strength <= WIFI_CRITICAL_PERCENT;\n\tif (critical) {\n\t\treturn {\n\t\t\tcode: \"weak_wifi_signal\",\n\t\t\tseverity: \"critical\",\n\t\t\tmessage: \"Wi-Fi signal critically weak\",\n\t\t\tdetail,\n\t\t};\n\t}\n\n\tconst warning = isRssi ? strength <= WIFI_WARNING_DBM : strength <= WIFI_WARNING_PERCENT;\n\tif (warning) {\n\t\treturn {\n\t\t\tcode: \"weak_wifi_signal\",\n\t\t\tseverity: \"warning\",\n\t\t\tmessage: \"Wi-Fi signal weak\",\n\t\t\tdetail,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Assess the health of a device based on its state and channel data.\n *\n * Checks for stale readings, low battery, and offline status, returning\n * a summary with the worst severity as the overall health level.\n *\n * @param device - The device to assess.\n * @param channels - The device's channel readings (used for timestamp comparison).\n * @param now - Optional current time for testability (defaults to `new Date()`).\n */\nexport function assessDeviceHealth(\n\tdevice: Device,\n\tchannels: DeviceChannel[],\n\tnow: Date = new Date(),\n): DeviceHealth {\n\tconst issues: DeviceHealthIssue[] = [];\n\tconst nowMs = now.getTime();\n\n\t// Determine the most recent reading timestamp across channels and device-level lastSeen.\n\tconst timestamps: number[] = [];\n\tif (device.lastSeen) {\n\t\ttimestamps.push(device.lastSeen.getTime());\n\t}\n\tfor (const ch of channels) {\n\t\tif (ch.lastSeen) {\n\t\t\ttimestamps.push(ch.lastSeen.getTime());\n\t\t}\n\t}\n\n\tif (timestamps.length > 0) {\n\t\tconst mostRecent = Math.max(...timestamps);\n\t\tconst ageMs = nowMs - mostRecent;\n\n\t\tif (ageMs >= STALE_CRITICAL_MS) {\n\t\t\tconst minutes = Math.round(ageMs / 60_000);\n\t\t\tissues.push({\n\t\t\t\tcode: \"stale_reading\",\n\t\t\t\tseverity: \"critical\",\n\t\t\t\tmessage: \"Reading is critically stale\",\n\t\t\t\tdetail: `Last reading was ${minutes} minutes ago`,\n\t\t\t});\n\t\t} else if (ageMs >= STALE_WARNING_MS) {\n\t\t\tconst minutes = Math.round(ageMs / 60_000);\n\t\t\tissues.push({\n\t\t\t\tcode: \"stale_reading\",\n\t\t\t\tseverity: \"warning\",\n\t\t\t\tmessage: \"Reading may be stale\",\n\t\t\t\tdetail: `Last reading was ${minutes} minutes ago`,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Battery check\n\tif (device.battery != null) {\n\t\tif (device.battery < BATTERY_CRITICAL_THRESHOLD) {\n\t\t\tissues.push({\n\t\t\t\tcode: \"low_battery\",\n\t\t\t\tseverity: \"critical\",\n\t\t\t\tmessage: \"Battery critically low\",\n\t\t\t\tdetail: `Battery at ${device.battery}%`,\n\t\t\t});\n\t\t} else if (device.battery < BATTERY_WARNING_THRESHOLD) {\n\t\t\tissues.push({\n\t\t\t\tcode: \"low_battery\",\n\t\t\t\tseverity: \"warning\",\n\t\t\t\tmessage: \"Battery low\",\n\t\t\t\tdetail: `Battery at ${device.battery}%`,\n\t\t\t});\n\t\t}\n\t}\n\n\tif (device.wifiStrength != null) {\n\t\tconst wifiIssue = assessWifiStrength(device.wifiStrength);\n\t\tif (wifiIssue) issues.push(wifiIssue);\n\t}\n\n\t// Offline check\n\tif (device.status !== \"online\") {\n\t\tissues.push({\n\t\t\tcode: \"offline\",\n\t\t\tseverity: \"warning\",\n\t\t\tmessage: \"Device is offline\",\n\t\t\tdetail: device.status ? `Status: ${device.status}` : undefined,\n\t\t});\n\t}\n\n\t// Derive overall status from worst issue severity\n\tlet overall: DeviceHealth[\"overall\"] = \"good\";\n\tfor (const issue of issues) {\n\t\tif (issue.severity === \"critical\") {\n\t\t\toverall = \"critical\";\n\t\t\tbreak;\n\t\t}\n\t\tif (issue.severity === \"warning\") {\n\t\t\toverall = \"warning\";\n\t\t}\n\t}\n\n\treturn { overall, issues };\n}\n\n/**\n * Check whether a channel reading is considered stale.\n *\n * @param channel - The channel to check.\n * @param now - Optional current time for testability.\n * @returns `true` if the channel's last reading is older than 5 minutes.\n */\nexport function isChannelStale(channel: DeviceChannel, now: Date = new Date()): boolean {\n\tif (!channel.lastSeen) return false;\n\tconst ageMs = now.getTime() - channel.lastSeen.getTime();\n\treturn ageMs >= STALE_WARNING_MS;\n}\n","/** Format a date as a human-readable relative time string. */\nexport function formatTimeAgo(date: Date | null): string {\n\tif (!date) return \"Never\";\n\tconst seconds = Math.floor((Date.now() - date.getTime()) / 1000);\n\tif (seconds < 0) return \"Just now\";\n\tif (seconds < 60) return \"Just now\";\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) return `${minutes}m ago`;\n\tconst hours = Math.floor(minutes / 60);\n\tif (hours < 24) return `${hours}h ago`;\n\tconst days = Math.floor(hours / 24);\n\treturn `${days}d ago`;\n}\n","// ─── Food-Safety Pasteurization ──────────────────────────────────────────────\n//\n// Estimates when meat has been held long enough at its core temperature to be\n// pasteurized, in addition to the usual instant-safe target temperature.\n//\n// Data is adapted from USDA FSIS Appendix A time-at-temperature lethality\n// tables: 7.0-log10 Salmonella reduction for poultry and 6.5-log10 Salmonella\n// reduction for beef and pork. Values are interpolated between published\n// points. These are estimates for planning, not a replacement for official\n// food-safety guidance. Hold times assume the core is held at or above the\n// listed temperature for the full duration.\n\n/** Supported protein tables. */\nexport type Protein = \"poultry\" | \"beef\" | \"pork\";\n\n/** A pasteurization lookup table for one protein. */\nexport interface PasteurizationTable {\n\tprotein: Protein;\n\t/** Core temperature (F) below which holding does not pasteurize in a practical time. */\n\tminTempF: number;\n\t/** Core temperature (F) at or above which lethality is treated as instant. */\n\tinstantTempF: number;\n\t/** Sorted ascending [tempF, holdMinutes] points for the required hold time. */\n\tpoints: ReadonlyArray<readonly [number, number]>;\n}\n\n// Poultry: USDA FSIS Appendix A, 7.0-log10 Salmonella reduction.\nconst POULTRY_TABLE: PasteurizationTable = {\n\tprotein: \"poultry\",\n\tminTempF: 130,\n\tinstantTempF: 165,\n\tpoints: [\n\t\t[130, 86.5],\n\t\t[134, 35.9],\n\t\t[136, 24.0],\n\t\t[138, 15.9],\n\t\t[140, 10.5],\n\t\t[142, 7.0],\n\t\t[144, 4.7],\n\t\t[146, 3.1],\n\t\t[148, 2.1],\n\t\t[150, 1.4],\n\t\t[154, 0.6],\n\t\t[158, 0.3],\n\t\t[162, 0.1],\n\t],\n};\n\n// Beef and pork: USDA FSIS Appendix A, 6.5-log10 Salmonella reduction.\nconst BEEF_PORK_POINTS: ReadonlyArray<readonly [number, number]> = [\n\t[130, 112.0],\n\t[133, 56.0],\n\t[135, 36.0],\n\t[138, 18.0],\n\t[140, 12.0],\n\t[142, 8.0],\n\t[145, 4.0],\n\t[147, 2.7],\n\t[150, 1.0],\n\t[153, 0.6],\n\t[155, 0.4],\n];\n\nconst BEEF_TABLE: PasteurizationTable = {\n\tprotein: \"beef\",\n\tminTempF: 130,\n\tinstantTempF: 160,\n\tpoints: BEEF_PORK_POINTS,\n};\n\nconst PORK_TABLE: PasteurizationTable = {\n\tprotein: \"pork\",\n\tminTempF: 130,\n\tinstantTempF: 160,\n\tpoints: BEEF_PORK_POINTS,\n};\n\nconst TABLES: Record<Protein, PasteurizationTable> = {\n\tpoultry: POULTRY_TABLE,\n\tbeef: BEEF_TABLE,\n\tpork: PORK_TABLE,\n};\n\n/** Return the pasteurization table for a protein. Defaults to poultry. */\nexport function getPasteurizationTable(protein: Protein = \"poultry\"): PasteurizationTable {\n\treturn TABLES[protein];\n}\n\n/**\n * Required hold time (minutes) at a core temperature for the protein.\n *\n * - Returns 0 when the temperature is at or above the instant-safe target.\n * - Returns null when the temperature is below the table minimum, meaning\n *   holding will not pasteurize in a practical time.\n * - Otherwise interpolates between published points. Above the highest listed\n *   point (but below the instant target) the smallest listed hold time is used.\n */\nexport function requiredHoldMinutes(\n\ttemperatureF: number,\n\tprotein: Protein = \"poultry\",\n): number | null {\n\tconst table = getPasteurizationTable(protein);\n\tif (temperatureF >= table.instantTempF) return 0;\n\tif (temperatureF < table.minTempF) return null;\n\n\tconst points = table.points;\n\t// biome-ignore lint/style/noNonNullAssertion: points is a non-empty constant\n\tconst first = points[0]!;\n\t// biome-ignore lint/style/noNonNullAssertion: points is a non-empty constant\n\tconst last = points[points.length - 1]!;\n\tif (temperatureF <= first[0]) return first[1];\n\tif (temperatureF >= last[0]) return last[1];\n\n\tfor (let i = 0; i < points.length - 1; i++) {\n\t\t// biome-ignore lint/style/noNonNullAssertion: index guarded by loop bound\n\t\tconst [t0, m0] = points[i]!;\n\t\t// biome-ignore lint/style/noNonNullAssertion: index guarded by loop bound\n\t\tconst [t1, m1] = points[i + 1]!;\n\t\tif (temperatureF >= t0 && temperatureF <= t1) {\n\t\t\tconst frac = (temperatureF - t0) / (t1 - t0);\n\t\t\treturn m0 + frac * (m1 - m0);\n\t\t}\n\t}\n\treturn last[1];\n}\n\n/** Input for a pasteurization assessment. */\nexport interface PasteurizationInput {\n\t/** Current core temperature in Fahrenheit. */\n\ttemperatureF: number;\n\t/** Minutes the core has been held at or above temperatureF. */\n\tholdMinutes: number;\n\t/** Protein table to use. Default: poultry. */\n\tprotein?: Protein;\n}\n\n/** Result of a pasteurization assessment. */\nexport interface PasteurizationResult {\n\tprotein: Protein;\n\ttemperatureF: number;\n\t/** Required hold minutes at this temperature, or null when too low to pasteurize. */\n\trequiredMinutes: number | null;\n\t/** Minutes held so far, clamped to be non-negative. */\n\theldMinutes: number;\n\t/** Minutes remaining until safe. 0 when safe now, null when too low to pasteurize. */\n\tremainingMinutes: number | null;\n\t/** True when the food is pasteurized (instant target reached or hold met). */\n\tsafe: boolean;\n\t/** True when at or above the instant-safe temperature. */\n\tinstant: boolean;\n\t/** Instant-safe target temperature (F) for this protein. */\n\tinstantTempF: number;\n}\n\n/**\n * Assess pasteurization for a core temperature and how long it has been held.\n *\n * The model is intentionally simple: it compares the time held at or above the\n * current core temperature against the required hold time for that temperature.\n * It does not integrate accumulated lethality across a changing temperature.\n */\nexport function assessPasteurization(input: PasteurizationInput): PasteurizationResult {\n\tconst protein = input.protein ?? \"poultry\";\n\tconst table = getPasteurizationTable(protein);\n\tconst temperatureF = input.temperatureF;\n\tconst heldMinutes = Math.max(0, input.holdMinutes);\n\tconst instant = temperatureF >= table.instantTempF;\n\tconst requiredMinutes = requiredHoldMinutes(temperatureF, protein);\n\n\tif (instant) {\n\t\treturn {\n\t\t\tprotein,\n\t\t\ttemperatureF,\n\t\t\trequiredMinutes: 0,\n\t\t\theldMinutes,\n\t\t\tremainingMinutes: 0,\n\t\t\tsafe: true,\n\t\t\tinstant: true,\n\t\t\tinstantTempF: table.instantTempF,\n\t\t};\n\t}\n\n\tif (requiredMinutes === null) {\n\t\treturn {\n\t\t\tprotein,\n\t\t\ttemperatureF,\n\t\t\trequiredMinutes: null,\n\t\t\theldMinutes,\n\t\t\tremainingMinutes: null,\n\t\t\tsafe: false,\n\t\t\tinstant: false,\n\t\t\tinstantTempF: table.instantTempF,\n\t\t};\n\t}\n\n\tconst remainingMinutes = Math.max(0, requiredMinutes - heldMinutes);\n\treturn {\n\t\tprotein,\n\t\ttemperatureF,\n\t\trequiredMinutes,\n\t\theldMinutes,\n\t\tremainingMinutes,\n\t\tsafe: remainingMinutes <= 0,\n\t\tinstant: false,\n\t\tinstantTempF: table.instantTempF,\n\t};\n}\n","/**\n * Cook planner: back-calculate when to start each item of a cook so that they\n * all finish at a shared serve time. Pure functions with no network access, so\n * they can be reused by the CLI, MCP, and web surfaces and unit tested directly.\n */\n\n/** A built-in cooking-time profile for a common cut. */\nexport interface MeatProfile {\n\t/** Canonical display name. */\n\treadonly name: string;\n\t/** Cook time per pound in hours, or null for cuts planned by fixed time. */\n\treadonly hoursPerPound: number | null;\n\t/** Fixed cook time in hours when weight is not the driver, or null. */\n\treadonly fixedHours: number | null;\n\t/** Recommended rest time in minutes after the cook. */\n\treadonly restMinutes: number;\n\t/** Reference smoker/pit temperature in Fahrenheit. */\n\treadonly pitTempF: number;\n\t/**\n\t * Recommended internal temperature in Fahrenheit to pull the cut at, or\n\t * null for cuts judged by feel (for example ribs by the bend test).\n\t */\n\treadonly targetTempF: number | null;\n\t/** Short note describing how to tell the cut is done. */\n\treadonly doneness: string;\n}\n\n/** Input describing one item to plan. */\nexport interface CookPlanItemInput {\n\t/** Meat profile key or alias (e.g. \"brisket\", \"pork butt\"). */\n\treadonly meat?: string;\n\t/** Explicit cook time in hours, overriding any profile estimate. */\n\treadonly hours?: number;\n\t/** Weight in pounds, used with per-pound profiles. */\n\treadonly weightLb?: number;\n\t/** Rest time in minutes, overriding the profile default. */\n\treadonly restMinutes?: number;\n\t/** Display label; defaults to the meat name or \"item\". */\n\treadonly label?: string;\n}\n\n/** A fully resolved item in a cook plan. */\nexport interface CookPlanItem {\n\treadonly label: string;\n\treadonly meat: string | null;\n\treadonly cookMinutes: number;\n\treadonly restMinutes: number;\n\t/** When to put the item on. */\n\treadonly startAt: Date;\n\t/** When to pull the item off (ready minus rest). */\n\treadonly removeAt: Date;\n\t/** Shared serve time. */\n\treadonly readyAt: Date;\n}\n\n/** A complete cook plan. */\nexport interface CookPlan {\n\treadonly readyAt: Date;\n\t/** Items sorted by earliest start time first. */\n\treadonly items: CookPlanItem[];\n}\n\n/** Options for {@link planCook}. */\nexport interface PlanCookOptions {\n\t/** The shared time everything should be ready to serve. */\n\treadonly readyAt: Date;\n}\n\nconst PROFILES: MeatProfile[] = [\n\t{\n\t\tname: \"Brisket\",\n\t\thoursPerPound: 1.25,\n\t\tfixedHours: null,\n\t\trestMinutes: 60,\n\t\tpitTempF: 250,\n\t\ttargetTempF: 203,\n\t\tdoneness: \"Probe-tender, around 203\\u00B0F in the flat\",\n\t},\n\t{\n\t\tname: \"Pork Butt\",\n\t\thoursPerPound: 1.5,\n\t\tfixedHours: null,\n\t\trestMinutes: 45,\n\t\tpitTempF: 250,\n\t\ttargetTempF: 203,\n\t\tdoneness: \"Pull-apart tender, 200-205\\u00B0F\",\n\t},\n\t{\n\t\tname: \"Pork Ribs\",\n\t\thoursPerPound: null,\n\t\tfixedHours: 5.5,\n\t\trestMinutes: 15,\n\t\tpitTempF: 250,\n\t\ttargetTempF: null,\n\t\tdoneness: \"Bend test; bones start to pull, around 200-203\\u00B0F\",\n\t},\n\t{\n\t\tname: \"Baby Back Ribs\",\n\t\thoursPerPound: null,\n\t\tfixedHours: 5,\n\t\trestMinutes: 15,\n\t\tpitTempF: 250,\n\t\ttargetTempF: null,\n\t\tdoneness: \"Bend test; tender at around 200-203\\u00B0F\",\n\t},\n\t{\n\t\tname: \"Whole Chicken\",\n\t\thoursPerPound: null,\n\t\tfixedHours: 3.5,\n\t\trestMinutes: 15,\n\t\tpitTempF: 275,\n\t\ttargetTempF: 165,\n\t\tdoneness: \"165\\u00B0F in the breast, 175\\u00B0F in the thigh\",\n\t},\n\t{\n\t\tname: \"Chicken Wings\",\n\t\thoursPerPound: null,\n\t\tfixedHours: 1.5,\n\t\trestMinutes: 5,\n\t\tpitTempF: 375,\n\t\ttargetTempF: 175,\n\t\tdoneness: \"175\\u00B0F for rendered, crisp skin\",\n\t},\n\t{\n\t\tname: \"Turkey\",\n\t\thoursPerPound: 0.5,\n\t\tfixedHours: null,\n\t\trestMinutes: 30,\n\t\tpitTempF: 275,\n\t\ttargetTempF: 165,\n\t\tdoneness: \"165\\u00B0F in the breast, 175\\u00B0F in the thigh\",\n\t},\n\t{\n\t\tname: \"Tri-Tip\",\n\t\thoursPerPound: null,\n\t\tfixedHours: 1.5,\n\t\trestMinutes: 15,\n\t\tpitTempF: 250,\n\t\ttargetTempF: 130,\n\t\tdoneness: \"130-135\\u00B0F for medium-rare, then rest\",\n\t},\n\t{\n\t\tname: \"Salmon\",\n\t\thoursPerPound: null,\n\t\tfixedHours: 1,\n\t\trestMinutes: 5,\n\t\tpitTempF: 225,\n\t\ttargetTempF: 125,\n\t\tdoneness: \"125-130\\u00B0F for moist, flaky fillets\",\n\t},\n\t{\n\t\tname: \"Beef Short Ribs\",\n\t\thoursPerPound: 1.5,\n\t\tfixedHours: null,\n\t\trestMinutes: 30,\n\t\tpitTempF: 250,\n\t\ttargetTempF: 203,\n\t\tdoneness: \"Probe-tender, around 203\\u00B0F\",\n\t},\n];\n\n/** Aliases mapping user input to a canonical profile name. */\nconst ALIASES: Record<string, string> = {\n\tbrisket: \"Brisket\",\n\t\"pork butt\": \"Pork Butt\",\n\t\"pork-butt\": \"Pork Butt\",\n\tporkbutt: \"Pork Butt\",\n\t\"pork shoulder\": \"Pork Butt\",\n\t\"pulled pork\": \"Pork Butt\",\n\tribs: \"Pork Ribs\",\n\t\"pork ribs\": \"Pork Ribs\",\n\t\"spare ribs\": \"Pork Ribs\",\n\t\"st louis ribs\": \"Pork Ribs\",\n\t\"baby back ribs\": \"Baby Back Ribs\",\n\t\"baby back\": \"Baby Back Ribs\",\n\tbabyback: \"Baby Back Ribs\",\n\tchicken: \"Whole Chicken\",\n\t\"whole chicken\": \"Whole Chicken\",\n\twings: \"Chicken Wings\",\n\t\"chicken wings\": \"Chicken Wings\",\n\tturkey: \"Turkey\",\n\t\"tri tip\": \"Tri-Tip\",\n\t\"tri-tip\": \"Tri-Tip\",\n\ttritip: \"Tri-Tip\",\n\tsalmon: \"Salmon\",\n\t\"short ribs\": \"Beef Short Ribs\",\n\t\"beef short ribs\": \"Beef Short Ribs\",\n};\n\n/** Return the built-in meat profiles. */\nexport function getMeatProfiles(): MeatProfile[] {\n\treturn PROFILES.map((p) => ({ ...p }));\n}\n\n/** Resolve a meat name or alias to a profile, or null if unknown. */\nexport function resolveMeatProfile(name: string): MeatProfile | null {\n\tconst key = name.trim().toLowerCase();\n\tconst canonical = ALIASES[key];\n\tif (canonical) {\n\t\treturn PROFILES.find((p) => p.name === canonical) ?? null;\n\t}\n\treturn PROFILES.find((p) => p.name.toLowerCase() === key) ?? null;\n}\n\nfunction cookHoursFor(item: CookPlanItemInput): {\n\thours: number;\n\trestMinutes: number;\n\tmeat: string | null;\n} {\n\tif (typeof item.hours === \"number\") {\n\t\tif (!Number.isFinite(item.hours) || item.hours <= 0) {\n\t\t\tthrow new Error(`Cook time must be a positive number of hours, got: ${item.hours}`);\n\t\t}\n\t\treturn {\n\t\t\thours: item.hours,\n\t\t\trestMinutes: item.restMinutes ?? 0,\n\t\t\tmeat: item.meat ? (resolveMeatProfile(item.meat)?.name ?? item.meat) : null,\n\t\t};\n\t}\n\n\tif (!item.meat) {\n\t\tthrow new Error(\"Each item needs either a meat type or an explicit cook time in hours.\");\n\t}\n\n\tconst profile = resolveMeatProfile(item.meat);\n\tif (!profile) {\n\t\tthrow new Error(\n\t\t\t`Unknown meat: \"${item.meat}\". Use one of the built-in profiles or pass hours.`,\n\t\t);\n\t}\n\n\tlet hours: number;\n\tif (profile.hoursPerPound != null) {\n\t\tif (\n\t\t\ttypeof item.weightLb !== \"number\" ||\n\t\t\t!Number.isFinite(item.weightLb) ||\n\t\t\titem.weightLb <= 0\n\t\t) {\n\t\t\tif (profile.fixedHours != null) {\n\t\t\t\thours = profile.fixedHours;\n\t\t\t} else {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`${profile.name} is planned per pound. Pass a weight, e.g. \"${item.meat}=12\".`,\n\t\t\t\t);\n\t\t\t}\n\t\t} else {\n\t\t\thours = profile.hoursPerPound * item.weightLb;\n\t\t}\n\t} else if (profile.fixedHours != null) {\n\t\thours = profile.fixedHours;\n\t} else {\n\t\tthrow new Error(`${profile.name} has no cook-time estimate. Pass explicit hours.`);\n\t}\n\n\treturn { hours, restMinutes: item.restMinutes ?? profile.restMinutes, meat: profile.name };\n}\n\n/**\n * Build a cook plan. For each item, the start time is the shared ready time\n * minus rest minus cook time. Items are returned sorted by earliest start.\n *\n * @throws Error if readyAt is invalid or an item cannot be resolved.\n */\nexport function planCook(items: CookPlanItemInput[], options: PlanCookOptions): CookPlan {\n\tconst readyAt = options.readyAt;\n\tif (!(readyAt instanceof Date) || Number.isNaN(readyAt.getTime())) {\n\t\tthrow new Error(\"A valid ready time is required.\");\n\t}\n\tif (items.length === 0) {\n\t\tthrow new Error(\"Plan at least one item.\");\n\t}\n\n\tconst planned: CookPlanItem[] = items.map((item, index) => {\n\t\tconst { hours, restMinutes, meat } = cookHoursFor(item);\n\t\tconst cookMinutes = Math.round(hours * 60);\n\t\tconst removeAt = new Date(readyAt.getTime() - restMinutes * 60_000);\n\t\tconst startAt = new Date(removeAt.getTime() - cookMinutes * 60_000);\n\t\treturn {\n\t\t\tlabel: item.label ?? meat ?? `Item ${index + 1}`,\n\t\t\tmeat,\n\t\t\tcookMinutes,\n\t\t\trestMinutes,\n\t\t\tstartAt,\n\t\t\tremoveAt,\n\t\t\treadyAt,\n\t\t};\n\t});\n\n\tplanned.sort((a, b) => a.startAt.getTime() - b.startAt.getTime());\n\treturn { readyAt, items: planned };\n}\n","import { getMeatProfiles, resolveMeatProfile } from \"./plan.js\";\n\n/** Probe placement guidance for one built-in cut. */\nexport interface ProbePlacement {\n\t/** Canonical meat profile name. */\n\treadonly meat: string;\n\t/** Where to place the food probe. */\n\treadonly meatProbe: string;\n\t/** Where to place the pit or ambient probe. */\n\treadonly pitProbe: string;\n\t/** Common placement mistakes to avoid. */\n\treadonly avoid: readonly string[];\n\t/** Extra cook-specific placement notes. */\n\treadonly notes: readonly string[];\n}\n\nconst PLACEMENTS: Record<string, Omit<ProbePlacement, \"meat\">> = {\n\tBrisket: {\n\t\tmeatProbe: \"Center of the flat, from the side, with the tip in the thickest lean section.\",\n\t\tpitProbe: \"Clipped to the grate near the brisket, shielded from direct radiant heat.\",\n\t\tavoid: [\"Fat seams\", \"the point\", \"touching the grate\", \"the firebox side of an offset smoker\"],\n\t\tnotes: [\"Use a second probe in the point if you want to track both muscles.\"],\n\t},\n\t\"Pork Butt\": {\n\t\tmeatProbe: \"Deep in the money muscle or thickest center, angled away from the blade bone.\",\n\t\tpitProbe: \"At grate level beside the butt, not above the dome thermometer.\",\n\t\tavoid: [\"Blade bone\", \"large fat pockets\", \"the outer bark\"],\n\t\tnotes: [\"Probe tenderness in several spots near the end because shoulders finish unevenly.\"],\n\t},\n\t\"Pork Ribs\": {\n\t\tmeatProbe: \"Between bones in the thickest center rack section if using a probe.\",\n\t\tpitProbe: \"At grate level beside the rack, with the clip clear of sauce or foil.\",\n\t\tavoid: [\"Touching bone\", \"thin end bones\", \"pinching the cable under a lid\"],\n\t\tnotes: [\"Ribs are still best judged by bend, pullback, and probe tenderness.\"],\n\t},\n\t\"Baby Back Ribs\": {\n\t\tmeatProbe: \"Between bones in the thickest center of the rack, parallel to the bones.\",\n\t\tpitProbe: \"At grate level beside the rack and away from direct flame.\",\n\t\tavoid: [\"Bone contact\", \"the narrow tail end\", \"placing the pit probe above a water pan edge\"],\n\t\tnotes: [\"Use temperature as a trend. Tenderness matters more than a single number.\"],\n\t},\n\t\"Whole Chicken\": {\n\t\tmeatProbe: \"Deep in the breast from the side, tip centered and not touching the cavity.\",\n\t\tpitProbe: \"At grate level beside the bird, away from dripping flare-ups.\",\n\t\tavoid: [\"Bone\", \"the cavity\", \"skin-only shallow placement\"],\n\t\tnotes: [\"Spot-check the thigh separately. It should finish hotter than the breast.\"],\n\t},\n\t\"Chicken Wings\": {\n\t\tmeatProbe: \"Use the largest drumette or flat if monitoring, with the tip centered in meat.\",\n\t\tpitProbe: \"At grate level near the wing pile or basket.\",\n\t\tavoid: [\"Bone\", \"skin folds\", \"tiny pieces that over-read quickly\"],\n\t\tnotes: [\"Wings are small, so spot checks are often more reliable than a leave-in probe.\"],\n\t},\n\tTurkey: {\n\t\tmeatProbe: \"Deep in the breast from the side, tip centered and clear of the rib cage.\",\n\t\tpitProbe: \"At grate level beside the turkey, not high in the dome.\",\n\t\tavoid: [\"Rib bones\", \"the cavity\", \"stuffing\", \"touching a roasting rack\"],\n\t\tnotes: [\"Track the breast and spot-check the thigh so dark meat has time to finish.\"],\n\t},\n\t\"Tri-Tip\": {\n\t\tmeatProbe: \"Into the thickest center from the side, following the grain line.\",\n\t\tpitProbe: \"At grate level beside the roast, away from the sear zone.\",\n\t\tavoid: [\"The tapered tip\", \"surface fat\", \"direct radiant heat during a reverse sear\"],\n\t\tnotes: [\"Move or remove the probe before a hard sear if cable heat is a risk.\"],\n\t},\n\tSalmon: {\n\t\tmeatProbe: \"Into the thickest fillet section from the side, shallow enough to stay centered.\",\n\t\tpitProbe: \"At grate level beside the fillet or plank.\",\n\t\tavoid: [\"Thin tail sections\", \"touching the plank\", \"pushing through the fillet\"],\n\t\tnotes: [\"Salmon changes quickly near the end. Pair the probe with visual flake checks.\"],\n\t},\n\t\"Beef Short Ribs\": {\n\t\tmeatProbe: \"Centered in the thickest meat between bones, inserted from the side.\",\n\t\tpitProbe: \"At grate level beside the ribs, clear of the bone side and water pan edge.\",\n\t\tavoid: [\"Bone\", \"fat pockets\", \"the membrane side\"],\n\t\tnotes: [\"Probe each rib for tenderness because thickness varies across the plate.\"],\n\t},\n};\n\nfunction copyPlacement(meat: string, placement: Omit<ProbePlacement, \"meat\">): ProbePlacement {\n\treturn {\n\t\tmeat,\n\t\tmeatProbe: placement.meatProbe,\n\t\tpitProbe: placement.pitProbe,\n\t\tavoid: [...placement.avoid],\n\t\tnotes: [...placement.notes],\n\t};\n}\n\n/** Return probe placement guidance for every built-in meat profile. */\nexport function getProbePlacements(): ProbePlacement[] {\n\treturn getMeatProfiles().map((profile) => {\n\t\tconst placement = PLACEMENTS[profile.name];\n\t\tif (!placement) throw new Error(`Missing probe placement for ${profile.name}`);\n\t\treturn copyPlacement(profile.name, placement);\n\t});\n}\n\n/** Resolve a meat name or alias to probe placement guidance. */\nexport function resolveProbePlacement(meat: string): ProbePlacement | null {\n\tconst profile = resolveMeatProfile(meat);\n\tif (!profile) return null;\n\tconst placement = PLACEMENTS[profile.name];\n\treturn placement ? copyPlacement(profile.name, placement) : null;\n}\n","// ─── Done-Time Prediction ────────────────────────────────────────────────────\n\n/** Options for configuring prediction behavior. */\nexport interface PredictionOptions {\n\t/** Temperature units. Default: \"F\". */\n\tunits?: \"F\" | \"C\";\n\t/** Prediction method. Default: \"linear\". */\n\tmethod?: \"linear\" | \"weighted\";\n\t/**\n\t * Recent rate (degrees per minute) used for weighted prediction.\n\t * When method is \"weighted\" and this value is provided, the prediction\n\t * blends recentRate (70%) with the overall rate (30%).\n\t */\n\trecentRate?: number;\n}\n\n/** Result of a done-time prediction. */\nexport interface PredictionResult {\n\t/** Estimated minutes remaining, or null if rate is zero/negative with target above current. */\n\testimatedMinutes: number | null;\n\t/** ISO timestamp of predicted completion, or null if estimate is unavailable. */\n\testimatedTime: string | null;\n\t/** Confidence level based on rate stability. */\n\tconfidence: \"high\" | \"medium\" | \"low\";\n\t/** Prediction method used. */\n\tmethod: \"linear\" | \"weighted\";\n}\n\n/**\n * Predict when a probe will reach its target temperature based on the\n * current rate of temperature change.\n *\n * @param current - Current temperature reading.\n * @param target - Target temperature (e.g., high alarm value).\n * @param rateOfChange - Rate of change in degrees per minute.\n * @param options - Optional prediction configuration.\n * @returns A prediction result with estimated time and confidence.\n */\nexport function predictDoneTime(\n\tcurrent: number,\n\ttarget: number,\n\trateOfChange: number,\n\toptions?: PredictionOptions,\n): PredictionResult {\n\tconst method = options?.method ?? \"linear\";\n\n\t// Already at or past target: done now.\n\tif (current >= target) {\n\t\treturn {\n\t\t\testimatedMinutes: 0,\n\t\t\testimatedTime: new Date().toISOString(),\n\t\t\tconfidence: \"high\",\n\t\t\tmethod,\n\t\t};\n\t}\n\n\t// Determine the effective rate to use.\n\tlet effectiveRate: number;\n\tif (method === \"weighted\" && options?.recentRate != null) {\n\t\t// Blend recent rate (70%) with overall rate (30%).\n\t\teffectiveRate = options.recentRate * 0.7 + rateOfChange * 0.3;\n\t} else {\n\t\teffectiveRate = rateOfChange;\n\t}\n\n\t// If rate is zero or negative, we cannot predict done time.\n\tif (effectiveRate <= 0) {\n\t\treturn {\n\t\t\testimatedMinutes: null,\n\t\t\testimatedTime: null,\n\t\t\tconfidence: \"low\",\n\t\t\tmethod,\n\t\t};\n\t}\n\n\tconst remaining = target - current;\n\tconst minutes = remaining / effectiveRate;\n\tconst estimatedMinutes = Math.round(minutes);\n\n\tconst estimatedTime = new Date(Date.now() + minutes * 60 * 1000).toISOString();\n\n\t// Determine confidence based on rate magnitude and method.\n\tconst confidence = determineConfidence(effectiveRate, rateOfChange, method, options?.recentRate);\n\n\treturn {\n\t\testimatedMinutes,\n\t\testimatedTime,\n\t\tconfidence,\n\t\tmethod,\n\t};\n}\n\n/**\n * Determine confidence level for a prediction.\n *\n * - \"high\": rate is stable and using weighted method with consistent rates\n * - \"medium\": reasonable rate with linear method\n * - \"low\": rate is very slow or weighted rates diverge significantly\n */\nfunction determineConfidence(\n\teffectiveRate: number,\n\toverallRate: number,\n\tmethod: \"linear\" | \"weighted\",\n\trecentRate?: number,\n): \"high\" | \"medium\" | \"low\" {\n\t// Very slow rate: low confidence (more susceptible to stalls).\n\tif (effectiveRate < 0.1) {\n\t\treturn \"low\";\n\t}\n\n\tif (method === \"weighted\" && recentRate != null) {\n\t\t// If recent and overall rates diverge significantly, lower confidence.\n\t\tconst divergence = Math.abs(recentRate - overallRate);\n\t\tconst avgRate = (Math.abs(recentRate) + Math.abs(overallRate)) / 2;\n\t\tif (avgRate > 0 && divergence / avgRate > 0.5) {\n\t\t\treturn \"low\";\n\t\t}\n\t\treturn \"high\";\n\t}\n\n\t// Linear method: medium confidence (simple extrapolation).\n\treturn \"medium\";\n}\n","import type { HistoricalReading, TemperatureReading } from \"./types.js\";\n\n/** A reading that can be replayed. Accepts SDK archive or history readings. */\nexport interface ReplayReading {\n\tvalue: number;\n\ttimestamp: Date | string;\n\tunits?: string;\n}\n\n/** One frame in a replay sequence. */\nexport interface ReplayFrame {\n\t/** Zero-based position in the ordered sequence. */\n\tindex: number;\n\tvalue: number;\n\tunits: string;\n\t/** Original time the reading was recorded. */\n\ttimestamp: Date;\n\t/** Wall-clock delay before this frame relative to the previous one, in ms. */\n\tdelayMs: number;\n\t/** Wall-clock offset of this frame from the start of the replay, in ms. */\n\toffsetMs: number;\n}\n\n/** Options controlling how a replay sequence is built. */\nexport interface ReplayOptions {\n\t/**\n\t * How much faster than real time to replay. A speed of 60 plays a minute of\n\t * cook time per wall-clock second. Values <= 0 or non-finite fall back to 1.\n\t */\n\tspeed?: number;\n}\n\nfunction toDate(value: Date | string): Date {\n\treturn value instanceof Date ? value : new Date(value);\n}\n\nfunction normalizeSpeed(speed: number | undefined): number {\n\tif (speed == null || !Number.isFinite(speed) || speed <= 0) return 1;\n\treturn speed;\n}\n\n/**\n * Turn a set of readings into a time-ordered replay sequence.\n *\n * Readings with non-finite values or unparseable timestamps are dropped. The\n * remaining readings are sorted by timestamp. Each frame carries the wall-clock\n * delay from the previous frame (the real gap divided by `speed`) and the\n * cumulative offset from the start. The first frame always has a zero delay.\n *\n * Pure and side-effect free, so it can be unit tested without timers.\n */\nexport function buildReplaySequence(\n\treadings: ReplayReading[],\n\toptions: ReplayOptions = {},\n): ReplayFrame[] {\n\tconst speed = normalizeSpeed(options.speed);\n\n\tconst valid = readings\n\t\t.map((r) => ({ value: r.value, units: r.units ?? \"\", date: toDate(r.timestamp) }))\n\t\t.filter((r) => Number.isFinite(r.value) && !Number.isNaN(r.date.getTime()))\n\t\t.sort((a, b) => a.date.getTime() - b.date.getTime());\n\n\tconst frames: ReplayFrame[] = [];\n\tlet previousTime: number | null = null;\n\tlet offsetMs = 0;\n\n\tvalid.forEach((r, index) => {\n\t\tconst time = r.date.getTime();\n\t\tconst sourceDelta = previousTime == null ? 0 : Math.max(0, time - previousTime);\n\t\tconst delayMs = sourceDelta / speed;\n\t\toffsetMs += delayMs;\n\t\tframes.push({\n\t\t\tindex,\n\t\t\tvalue: r.value,\n\t\t\tunits: r.units,\n\t\t\ttimestamp: r.date,\n\t\t\tdelayMs,\n\t\t\toffsetMs,\n\t\t});\n\t\tpreviousTime = time;\n\t});\n\n\treturn frames;\n}\n\n/**\n * Compute the next index when streaming a replay. Returns the next position, or\n * 0 when looping past the end, or null when the sequence is finished (and not\n * looping) or empty.\n */\nexport function nextReplayIndex(current: number, length: number, loop: boolean): number | null {\n\tif (length <= 0) return null;\n\tconst next = current + 1;\n\tif (next < length) return next;\n\treturn loop ? 0 : null;\n}\n\n/** Narrow a history reading into a replay reading. */\nexport function historyReadingToReplay(reading: HistoricalReading): ReplayReading {\n\treturn { value: reading.value, timestamp: reading.timestamp, units: reading.units };\n}\n\n/** Narrow an archive reading into a replay reading. */\nexport function archiveReadingToReplay(reading: TemperatureReading): ReplayReading {\n\treturn { value: reading.value, timestamp: reading.timestamp, units: reading.units };\n}\n","// ─── Seasoning Calculator ────────────────────────────────────────────────────\n//\n// Offline helpers that scale dry rubs and brines to the weight of a cut. Pure\n// and side-effect free: no network, no clock, no files. All math is deterministic\n// so the CLI and any other caller get the same numbers for the same inputs.\n\nconst GRAMS_PER_POUND = 453.592;\nconst GRAMS_PER_QUART_WATER = 946.353;\nconst TSP_PER_TABLESPOON = 3;\n// Diamond Crystal kosher salt, a common baseline for seasoning by volume.\nconst GRAMS_PER_TSP_KOSHER = 2.84;\nconst GRAMS_PER_CUP_KOSHER = GRAMS_PER_TSP_KOSHER * TSP_PER_TABLESPOON * 16;\n\n/** A single ingredient in a dry-rub recipe, weighted by parts. */\nexport interface RubIngredient {\n\treadonly name: string;\n\treadonly parts: number;\n}\n\n/** A named dry-rub recipe and how heavily to apply it. */\nexport interface RubRecipe {\n\treadonly name: string;\n\treadonly label: string;\n\t/** Tablespoons of finished rub to apply per pound of meat. */\n\treadonly tablespoonsPerPound: number;\n\treadonly ingredients: readonly RubIngredient[];\n}\n\nconst RUB_RECIPES: Record<string, RubRecipe> = {\n\tclassic: {\n\t\tname: \"classic\",\n\t\tlabel: \"Classic BBQ\",\n\t\ttablespoonsPerPound: 1,\n\t\tingredients: [\n\t\t\t{ name: \"paprika\", parts: 8 },\n\t\t\t{ name: \"brown sugar\", parts: 8 },\n\t\t\t{ name: \"kosher salt\", parts: 4 },\n\t\t\t{ name: \"black pepper\", parts: 4 },\n\t\t\t{ name: \"garlic powder\", parts: 2 },\n\t\t\t{ name: \"onion powder\", parts: 2 },\n\t\t\t{ name: \"chili powder\", parts: 2 },\n\t\t\t{ name: \"cumin\", parts: 1 },\n\t\t],\n\t},\n\ttexas: {\n\t\tname: \"texas\",\n\t\tlabel: \"Texas salt and pepper\",\n\t\ttablespoonsPerPound: 1,\n\t\tingredients: [\n\t\t\t{ name: \"kosher salt\", parts: 2 },\n\t\t\t{ name: \"coarse black pepper\", parts: 2 },\n\t\t\t{ name: \"garlic powder\", parts: 1 },\n\t\t],\n\t},\n\tcoffee: {\n\t\tname: \"coffee\",\n\t\tlabel: \"Coffee chophouse\",\n\t\ttablespoonsPerPound: 1,\n\t\tingredients: [\n\t\t\t{ name: \"ground coffee\", parts: 4 },\n\t\t\t{ name: \"brown sugar\", parts: 4 },\n\t\t\t{ name: \"paprika\", parts: 2 },\n\t\t\t{ name: \"kosher salt\", parts: 2 },\n\t\t\t{ name: \"black pepper\", parts: 2 },\n\t\t\t{ name: \"cocoa\", parts: 1 },\n\t\t],\n\t},\n\tcajun: {\n\t\tname: \"cajun\",\n\t\tlabel: \"Cajun\",\n\t\ttablespoonsPerPound: 1,\n\t\tingredients: [\n\t\t\t{ name: \"paprika\", parts: 4 },\n\t\t\t{ name: \"kosher salt\", parts: 2 },\n\t\t\t{ name: \"garlic powder\", parts: 2 },\n\t\t\t{ name: \"onion powder\", parts: 2 },\n\t\t\t{ name: \"cayenne\", parts: 1 },\n\t\t\t{ name: \"oregano\", parts: 1 },\n\t\t\t{ name: \"thyme\", parts: 1 },\n\t\t\t{ name: \"black pepper\", parts: 1 },\n\t\t],\n\t},\n\tsweet: {\n\t\tname: \"sweet\",\n\t\tlabel: \"Sweet and smoky\",\n\t\ttablespoonsPerPound: 1,\n\t\tingredients: [\n\t\t\t{ name: \"brown sugar\", parts: 10 },\n\t\t\t{ name: \"paprika\", parts: 4 },\n\t\t\t{ name: \"kosher salt\", parts: 3 },\n\t\t\t{ name: \"smoked paprika\", parts: 2 },\n\t\t\t{ name: \"black pepper\", parts: 1 },\n\t\t\t{ name: \"garlic powder\", parts: 1 },\n\t\t],\n\t},\n};\n\nconst DEFAULT_RECIPE = \"classic\";\nconst DEFAULT_WET_SALINITY_PERCENT = 5;\nconst DEFAULT_DRY_SALT_PERCENT = 1;\n\n/** A scaled amount of one rub ingredient. */\nexport interface SeasoningIngredientAmount {\n\treadonly name: string;\n\t/** Tablespoons of this ingredient, rounded to the nearest quarter tablespoon. */\n\treadonly tablespoons: number;\n}\n\n/** A scaled dry-rub plan. */\nexport interface RubPlan {\n\treadonly mode: \"rub\";\n\treadonly recipe: string;\n\treadonly label: string;\n\treadonly weightLb: number;\n\treadonly totalTablespoons: number;\n\treadonly ingredients: readonly SeasoningIngredientAmount[];\n}\n\n/** A scaled wet-brine plan. */\nexport interface WetBrinePlan {\n\treadonly mode: \"wet-brine\";\n\treadonly weightLb: number;\n\treadonly waterQuarts: number;\n\treadonly salinityPercent: number;\n\treadonly saltGrams: number;\n\treadonly saltCups: number;\n\treadonly sugarGrams: number;\n\treadonly minHours: number;\n\treadonly maxHours: number;\n}\n\n/** A scaled dry-brine plan. */\nexport interface DryBrinePlan {\n\treadonly mode: \"dry-brine\";\n\treadonly weightLb: number;\n\treadonly saltPercent: number;\n\treadonly saltGrams: number;\n\treadonly saltTeaspoons: number;\n\treadonly minHours: number;\n\treadonly maxHours: number;\n}\n\n/** The result of {@link calculateSeasoning}. */\nexport type SeasoningPlan = RubPlan | WetBrinePlan | DryBrinePlan;\n\n/** Options for {@link calculateSeasoning}. */\nexport interface SeasoningOptions {\n\t/** Which seasoning to compute. Defaults to a dry rub. */\n\tmode?: \"rub\" | \"wet-brine\" | \"dry-brine\";\n\t/** Dry-rub recipe name (rub mode). Defaults to \"classic\". */\n\trecipe?: string;\n\t/** Wet-brine salinity as a percent of the water weight. Defaults to 5. */\n\tsalinityPercent?: number;\n\t/** Dry-brine salt as a percent of the meat weight. Defaults to 1. */\n\tsaltPercent?: number;\n}\n\nfunction roundTo(value: number, step: number): number {\n\treturn Math.round(value / step) * step;\n}\n\nfunction round1(value: number): number {\n\treturn Math.round(value * 10) / 10;\n}\n\n/** List the built-in dry-rub recipes. */\nexport function listRubRecipes(): RubRecipe[] {\n\treturn Object.values(RUB_RECIPES);\n}\n\n/** Look up a dry-rub recipe by name (case-insensitive). Returns null if unknown. */\nexport function resolveRubRecipe(name: string): RubRecipe | null {\n\treturn RUB_RECIPES[name.trim().toLowerCase()] ?? null;\n}\n\nfunction buildRubPlan(weightLb: number, recipe: RubRecipe): RubPlan {\n\tconst totalParts = recipe.ingredients.reduce((sum, i) => sum + i.parts, 0);\n\tconst totalTablespoons = weightLb * recipe.tablespoonsPerPound;\n\n\tconst ingredients = recipe.ingredients.map((ingredient) => ({\n\t\tname: ingredient.name,\n\t\ttablespoons: roundTo((totalTablespoons * ingredient.parts) / totalParts, 0.25),\n\t}));\n\n\treturn {\n\t\tmode: \"rub\",\n\t\trecipe: recipe.name,\n\t\tlabel: recipe.label,\n\t\tweightLb,\n\t\ttotalTablespoons: round1(totalTablespoons),\n\t\tingredients,\n\t};\n}\n\nfunction buildWetBrinePlan(weightLb: number, salinityPercent: number): WetBrinePlan {\n\tconst waterQuarts = Math.max(1, Math.round(weightLb));\n\tconst waterGrams = waterQuarts * GRAMS_PER_QUART_WATER;\n\tconst saltGrams = waterGrams * (salinityPercent / 100);\n\n\treturn {\n\t\tmode: \"wet-brine\",\n\t\tweightLb,\n\t\twaterQuarts,\n\t\tsalinityPercent,\n\t\tsaltGrams: Math.round(saltGrams),\n\t\tsaltCups: round1(saltGrams / GRAMS_PER_CUP_KOSHER),\n\t\tsugarGrams: Math.round(saltGrams * 0.5),\n\t\tminHours: Math.max(2, Math.round(weightLb)),\n\t\tmaxHours: Math.max(4, Math.round(weightLb * 1.5)),\n\t};\n}\n\nfunction buildDryBrinePlan(weightLb: number, saltPercent: number): DryBrinePlan {\n\tconst meatGrams = weightLb * GRAMS_PER_POUND;\n\tconst saltGrams = meatGrams * (saltPercent / 100);\n\n\treturn {\n\t\tmode: \"dry-brine\",\n\t\tweightLb,\n\t\tsaltPercent,\n\t\tsaltGrams: Math.round(saltGrams),\n\t\tsaltTeaspoons: round1(saltGrams / GRAMS_PER_TSP_KOSHER),\n\t\tminHours: 8,\n\t\tmaxHours: weightLb > 6 ? 48 : 24,\n\t};\n}\n\n/**\n * Scale a dry rub or brine to the weight of a cut.\n *\n * Dry rub (default): scales a named recipe's ingredients by weight and returns\n * a per-ingredient breakdown in tablespoons. Wet brine: returns the water\n * volume, salt, sugar, and a brine-time range. Dry brine: returns the salt\n * amount and a fridge-rest range.\n *\n * @throws RangeError if `weightLb` is not a positive, finite number, or if a\n *   named rub recipe cannot be found.\n */\nexport function calculateSeasoning(\n\tweightLb: number,\n\toptions: SeasoningOptions = {},\n): SeasoningPlan {\n\tif (!Number.isFinite(weightLb) || weightLb <= 0) {\n\t\tthrow new RangeError(`weightLb must be a positive number, got ${weightLb}`);\n\t}\n\n\tconst mode = options.mode ?? \"rub\";\n\n\tif (mode === \"wet-brine\") {\n\t\tconst salinity = options.salinityPercent ?? DEFAULT_WET_SALINITY_PERCENT;\n\t\tif (!Number.isFinite(salinity) || salinity <= 0) {\n\t\t\tthrow new RangeError(`salinityPercent must be a positive number, got ${salinity}`);\n\t\t}\n\t\treturn buildWetBrinePlan(weightLb, salinity);\n\t}\n\n\tif (mode === \"dry-brine\") {\n\t\tconst saltPercent = options.saltPercent ?? DEFAULT_DRY_SALT_PERCENT;\n\t\tif (!Number.isFinite(saltPercent) || saltPercent <= 0) {\n\t\t\tthrow new RangeError(`saltPercent must be a positive number, got ${saltPercent}`);\n\t\t}\n\t\treturn buildDryBrinePlan(weightLb, saltPercent);\n\t}\n\n\tconst recipeName = options.recipe ?? DEFAULT_RECIPE;\n\tconst recipe = resolveRubRecipe(recipeName);\n\tif (!recipe) {\n\t\tthrow new RangeError(`Unknown rub recipe: \"${recipeName}\"`);\n\t}\n\treturn buildRubPlan(weightLb, recipe);\n}\n","import type { TemperatureReading } from \"./types.js\";\n\n/** Options for {@link analyzePitStability}. */\nexport interface PitStabilityOptions {\n\t/** Desired pit temperature in Fahrenheit. */\n\treadonly targetF: number;\n\t/** Allowed degrees above or below target. */\n\treadonly bandF?: number;\n}\n\n/** A continuous out-of-band period. */\nexport interface PitStabilityExcursion {\n\treadonly direction: \"high\" | \"low\";\n\treadonly startedAt: Date;\n\treadonly endedAt: Date;\n\treadonly durationMinutes: number;\n\treadonly peakTempF: number;\n}\n\n/** Time-in-band statistics for a pit temperature series. */\nexport interface PitStabilityResult {\n\treadonly targetF: number;\n\treadonly bandF: number;\n\treadonly lowLimitF: number;\n\treadonly highLimitF: number;\n\treadonly startedAt: Date | null;\n\treadonly endedAt: Date | null;\n\treadonly durationMinutes: number;\n\treadonly sampleCount: number;\n\treadonly inBandMinutes: number;\n\treadonly highMinutes: number;\n\treadonly lowMinutes: number;\n\treadonly inBandPercent: number;\n\treadonly averageTempF: number | null;\n\treadonly minTempF: number | null;\n\treadonly maxTempF: number | null;\n\treadonly longestExcursion: PitStabilityExcursion | null;\n}\n\nconst DEFAULT_BAND_F = 15;\n\nfunction round(value: number): number {\n\treturn Math.round(value * 10) / 10;\n}\n\nfunction minutesBetween(a: Date, b: Date): number {\n\treturn Math.max(0, (b.getTime() - a.getTime()) / 60_000);\n}\n\nfunction classify(value: number, lowLimitF: number, highLimitF: number): \"in\" | \"high\" | \"low\" {\n\tif (value < lowLimitF) return \"low\";\n\tif (value > highLimitF) return \"high\";\n\treturn \"in\";\n}\n\n/**\n * Analyze how tightly pit readings held a target temperature.\n *\n * Readings are sorted chronologically before analysis. Each sample owns the time\n * until the next sample, which matches how archive charts are usually read.\n */\nexport function analyzePitStability(\n\treadings: TemperatureReading[],\n\toptions: PitStabilityOptions,\n): PitStabilityResult {\n\tconst targetF = options.targetF;\n\tconst bandF = options.bandF ?? DEFAULT_BAND_F;\n\tif (!Number.isFinite(targetF)) throw new Error(`Target must be a number, got: ${targetF}`);\n\tif (!Number.isFinite(bandF) || bandF <= 0) {\n\t\tthrow new Error(`Band must be a positive number of degrees, got: ${bandF}`);\n\t}\n\n\tconst lowLimitF = targetF - bandF;\n\tconst highLimitF = targetF + bandF;\n\tif (readings.length === 0) {\n\t\treturn {\n\t\t\ttargetF,\n\t\t\tbandF,\n\t\t\tlowLimitF,\n\t\t\thighLimitF,\n\t\t\tstartedAt: null,\n\t\t\tendedAt: null,\n\t\t\tdurationMinutes: 0,\n\t\t\tsampleCount: 0,\n\t\t\tinBandMinutes: 0,\n\t\t\thighMinutes: 0,\n\t\t\tlowMinutes: 0,\n\t\t\tinBandPercent: 0,\n\t\t\taverageTempF: null,\n\t\t\tminTempF: null,\n\t\t\tmaxTempF: null,\n\t\t\tlongestExcursion: null,\n\t\t};\n\t}\n\n\tconst sorted = [...readings].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());\n\tconst first = sorted[0];\n\tconst last = sorted[sorted.length - 1];\n\tif (!first || !last) throw new Error(\"No readings to analyze.\");\n\tconst values = sorted.map((reading) => reading.value);\n\tconst minTempF = Math.min(...values);\n\tconst maxTempF = Math.max(...values);\n\tconst averageTempF = values.reduce((sum, value) => sum + value, 0) / values.length;\n\n\tlet inBandMinutes = 0;\n\tlet highMinutes = 0;\n\tlet lowMinutes = 0;\n\tlet current: PitStabilityExcursion | null = null;\n\tconst excursions: PitStabilityExcursion[] = [];\n\n\tfunction closeCurrent(): void {\n\t\tif (current) {\n\t\t\texcursions.push({ ...current, durationMinutes: round(current.durationMinutes) });\n\t\t\tcurrent = null;\n\t\t}\n\t}\n\n\tfunction extendExcursion(\n\t\tdirection: \"high\" | \"low\",\n\t\tstartedAt: Date,\n\t\tendedAt: Date,\n\t\tdurationMinutes: number,\n\t\ttempF: number,\n\t): void {\n\t\tif (current?.direction !== direction) {\n\t\t\tcloseCurrent();\n\t\t\tcurrent = {\n\t\t\t\tdirection,\n\t\t\t\tstartedAt,\n\t\t\t\tendedAt,\n\t\t\t\tdurationMinutes: 0,\n\t\t\t\tpeakTempF: tempF,\n\t\t\t};\n\t\t}\n\t\tcurrent = {\n\t\t\t...current,\n\t\t\tendedAt,\n\t\t\tdurationMinutes: current.durationMinutes + durationMinutes,\n\t\t\tpeakTempF:\n\t\t\t\tdirection === \"high\"\n\t\t\t\t\t? Math.max(current.peakTempF, tempF)\n\t\t\t\t\t: Math.min(current.peakTempF, tempF),\n\t\t};\n\t}\n\n\tfor (let i = 0; i < sorted.length - 1; i++) {\n\t\tconst reading = sorted[i];\n\t\tconst next = sorted[i + 1];\n\t\tif (!reading || !next) continue;\n\t\tconst minutes = minutesBetween(reading.timestamp, next.timestamp);\n\t\tif (minutes === 0) continue;\n\n\t\tconst bucket = classify(reading.value, lowLimitF, highLimitF);\n\t\tif (bucket === \"in\") {\n\t\t\tinBandMinutes += minutes;\n\t\t\tcloseCurrent();\n\t\t} else if (bucket === \"high\") {\n\t\t\thighMinutes += minutes;\n\t\t\textendExcursion(\"high\", reading.timestamp, next.timestamp, minutes, reading.value);\n\t\t} else {\n\t\t\tlowMinutes += minutes;\n\t\t\textendExcursion(\"low\", reading.timestamp, next.timestamp, minutes, reading.value);\n\t\t}\n\t}\n\tcloseCurrent();\n\n\tconst durationMinutes = round(minutesBetween(first.timestamp, last.timestamp));\n\tconst longestExcursion =\n\t\texcursions.length === 0\n\t\t\t? null\n\t\t\t: excursions.reduce((best, item) =>\n\t\t\t\t\titem.durationMinutes > best.durationMinutes ? item : best,\n\t\t\t\t);\n\n\treturn {\n\t\ttargetF,\n\t\tbandF,\n\t\tlowLimitF,\n\t\thighLimitF,\n\t\tstartedAt: first.timestamp,\n\t\tendedAt: last.timestamp,\n\t\tdurationMinutes,\n\t\tsampleCount: sorted.length,\n\t\tinBandMinutes: round(inBandMinutes),\n\t\thighMinutes: round(highMinutes),\n\t\tlowMinutes: round(lowMinutes),\n\t\tinBandPercent: durationMinutes > 0 ? round((inBandMinutes / durationMinutes) * 100) : 0,\n\t\taverageTempF: round(averageTempF),\n\t\tminTempF: round(minTempF),\n\t\tmaxTempF: round(maxTempF),\n\t\tlongestExcursion,\n\t};\n}\n","import type { TemperatureReading } from \"./types.js\";\n\n// ─── Stall Detection ─────────────────────────────────────────────────────────\n\n/** Options for configuring stall detection sensitivity. */\nexport interface StallOptions {\n\t/** Maximum temperature variance (in degrees) to consider a stall. Default: 2. */\n\tthresholdDegrees?: number;\n\t/** Minimum duration (in minutes) of low variance to qualify as a stall. Default: 30. */\n\tdurationMinutes?: number;\n}\n\n/** Result of stall detection analysis. */\nexport interface StallResult {\n\t/** Whether the readings indicate an active stall. */\n\tisStalling: boolean;\n\t/** ISO timestamp of when the stall began, or null if not stalling. */\n\tstallStart: string | null;\n\t/** Duration of the stall in minutes (0 if not stalling). */\n\tstallDuration: number;\n\t/** Average temperature during the stall window, or null if not stalling. */\n\tavgTemp: number | null;\n}\n\n/**\n * Analyze a series of temperature readings to detect a stall (plateau).\n *\n * A stall is identified when the temperature varies by less than\n * `thresholdDegrees` over a trailing window of at least `durationMinutes`.\n * Readings must be sorted chronologically (oldest first).\n */\nexport function detectStall(readings: TemperatureReading[], options?: StallOptions): StallResult {\n\tconst threshold = options?.thresholdDegrees ?? 2;\n\tconst minDuration = options?.durationMinutes ?? 30;\n\n\tconst noStall: StallResult = {\n\t\tisStalling: false,\n\t\tstallStart: null,\n\t\tstallDuration: 0,\n\t\tavgTemp: null,\n\t};\n\n\tif (readings.length < 2) return noStall;\n\n\t// Walk backwards from the most recent reading to find the longest trailing\n\t// window where max - min temperature stays within threshold.\n\tconst last = readings.at(-1);\n\tif (!last) return noStall;\n\n\tlet windowMin = last.value;\n\tlet windowMax = last.value;\n\tlet stallStartIndex = readings.length - 1;\n\n\tfor (let i = readings.length - 2; i >= 0; i--) {\n\t\tconst reading = readings[i];\n\t\tif (!reading) break;\n\t\tconst candidateMin = Math.min(windowMin, reading.value);\n\t\tconst candidateMax = Math.max(windowMax, reading.value);\n\n\t\tif (candidateMax - candidateMin > threshold) break;\n\n\t\twindowMin = candidateMin;\n\t\twindowMax = candidateMax;\n\t\tstallStartIndex = i;\n\t}\n\n\tconst startReading = readings[stallStartIndex];\n\tconst endReading = readings.at(-1);\n\tif (!startReading || !endReading) return noStall;\n\n\tconst durationMs = endReading.timestamp.getTime() - startReading.timestamp.getTime();\n\tconst durationMinutes = durationMs / (1000 * 60);\n\n\tif (durationMinutes < minDuration) return noStall;\n\n\t// Compute average temperature across the stall window.\n\tlet sum = 0;\n\tconst count = readings.length - stallStartIndex;\n\tfor (let i = stallStartIndex; i < readings.length; i++) {\n\t\tconst r = readings[i];\n\t\tif (r) sum += r.value;\n\t}\n\tconst avgTemp = Math.round((sum / count) * 10) / 10;\n\n\treturn {\n\t\tisStalling: true,\n\t\tstallStart: startReading.timestamp.toISOString(),\n\t\tstallDuration: Math.round(durationMinutes),\n\t\tavgTemp,\n\t};\n}\n\n// ─── Rapid Change Detection ──────────────────────────────────────────────────\n\n/** Options for configuring rapid change detection sensitivity. */\nexport interface RapidChangeOptions {\n\t/** Rate threshold in degrees per 5 minutes. Default: 5. */\n\trateThreshold?: number;\n}\n\n/** Result of rapid change detection analysis. */\nexport interface RapidChangeResult {\n\t/** Whether the recent rate of change exceeds the threshold. */\n\tisRapid: boolean;\n\t/** Direction of the rapid change, or null if not rapid. */\n\tdirection: \"rising\" | \"falling\" | null;\n\t/** Current rate of change in degrees per 5 minutes. */\n\trate: number;\n}\n\n/**\n * Analyze recent temperature readings to detect rapid changes.\n *\n * Computes the rate of change between the two most recent readings,\n * normalized to degrees per 5 minutes. If only one or zero readings\n * are provided, returns a zero-rate result.\n */\nexport function detectRapidChange(\n\treadings: TemperatureReading[],\n\toptions?: RapidChangeOptions,\n): RapidChangeResult {\n\tconst threshold = options?.rateThreshold ?? 5;\n\n\tconst noChange: RapidChangeResult = { isRapid: false, direction: null, rate: 0 };\n\n\tif (readings.length < 2) return noChange;\n\n\tconst prev = readings.at(-2);\n\tconst curr = readings.at(-1);\n\tif (!prev || !curr) return noChange;\n\n\tconst deltaTemp = curr.value - prev.value;\n\tconst deltaMs = curr.timestamp.getTime() - prev.timestamp.getTime();\n\n\t// Guard against zero or negative time intervals.\n\tif (deltaMs <= 0) return noChange;\n\n\t// Normalize to degrees per 5 minutes.\n\tconst fiveMinMs = 5 * 60 * 1000;\n\tconst ratePer5Min = Math.round((deltaTemp / deltaMs) * fiveMinMs * 10) / 10;\n\n\tconst isRapid = Math.abs(ratePer5Min) >= threshold;\n\n\treturn {\n\t\tisRapid,\n\t\tdirection: isRapid ? (ratePer5Min > 0 ? \"rising\" : \"falling\") : null,\n\t\trate: ratePer5Min,\n\t};\n}\n","import type { TemperatureReading } from \"./types.js\";\n\n// ─── Cook Timeline ───────────────────────────────────────────────────────────\n\n/** A single milestone kind on a cook timeline. */\nexport type TimelineKind = \"start\" | \"low\" | \"stall\" | \"target\" | \"peak\" | \"end\";\n\n/** A single annotated milestone on a cook timeline. */\nexport interface TimelineEvent {\n\t/** What kind of milestone this is. */\n\tkind: TimelineKind;\n\t/** Timestamp of the reading that triggered the milestone. */\n\ttimestamp: Date;\n\t/** Whole minutes from the first reading. */\n\tminuteOffset: number;\n\t/** Temperature at the milestone, in Fahrenheit. */\n\ttempF: number;\n\t/** Short human-readable description. */\n\tdetail: string;\n}\n\n/** A full annotated cook timeline. */\nexport interface CookTimeline {\n\t/** Milestones in chronological order. */\n\tevents: TimelineEvent[];\n\t/** Timestamp of the first reading, or null when there are none. */\n\tstartedAt: Date | null;\n\t/** Timestamp of the last reading, or null when there are none. */\n\tendedAt: Date | null;\n\t/** Whole minutes from first to last reading. */\n\tdurationMinutes: number;\n\t/** Lowest temperature seen, in Fahrenheit, or null when there are no readings. */\n\tminTempF: number | null;\n\t/** Highest temperature seen, in Fahrenheit, or null when there are no readings. */\n\tmaxTempF: number | null;\n\t/** Whether the target was reached (only meaningful when a target was given). */\n\ttargetReached: boolean;\n}\n\n/** Options for {@link buildCookTimeline}. */\nexport interface TimelineOptions {\n\t/** Target internal temperature in Fahrenheit. When set, the first crossing is marked. */\n\ttargetF?: number;\n\t/** Maximum variance (degrees) for the stall window. Default 2. */\n\tstallThresholdDegrees?: number;\n\t/** Minimum duration (minutes) for a plateau to count as a stall. Default 30. */\n\tstallMinutes?: number;\n}\n\nconst DEFAULT_STALL_THRESHOLD = 2;\nconst DEFAULT_STALL_MINUTES = 30;\n\nfunction round(value: number): number {\n\treturn Math.round(value * 10) / 10;\n}\n\nfunction minutesBetween(a: Date, b: Date): number {\n\treturn Math.round((b.getTime() - a.getTime()) / 60_000);\n}\n\ninterface StallWindow {\n\tstartIndex: number;\n\tdurationMinutes: number;\n\tavgTempF: number;\n}\n\n/**\n * Find the longest contiguous plateau where the temperature stays within\n * `threshold` for at least `minDuration` minutes. Uses a sliding window with\n * monotonic deques so it runs in linear time over the series.\n */\nfunction findLongestStall(\n\treadings: TemperatureReading[],\n\tthreshold: number,\n\tminDuration: number,\n): StallWindow | null {\n\tconst minDeque: number[] = [];\n\tconst maxDeque: number[] = [];\n\tlet left = 0;\n\tlet best: StallWindow | null = null;\n\n\tfor (let right = 0; right < readings.length; right++) {\n\t\tconst value = readings[right]?.value ?? 0;\n\n\t\twhile (\n\t\t\tminDeque.length > 0 &&\n\t\t\t(readings[minDeque[minDeque.length - 1] ?? 0]?.value ?? 0) >= value\n\t\t) {\n\t\t\tminDeque.pop();\n\t\t}\n\t\tminDeque.push(right);\n\n\t\twhile (\n\t\t\tmaxDeque.length > 0 &&\n\t\t\t(readings[maxDeque[maxDeque.length - 1] ?? 0]?.value ?? 0) <= value\n\t\t) {\n\t\t\tmaxDeque.pop();\n\t\t}\n\t\tmaxDeque.push(right);\n\n\t\twhile (\n\t\t\t(readings[maxDeque[0] ?? 0]?.value ?? 0) - (readings[minDeque[0] ?? 0]?.value ?? 0) >\n\t\t\tthreshold\n\t\t) {\n\t\t\tleft++;\n\t\t\tif ((minDeque[0] ?? 0) < left) minDeque.shift();\n\t\t\tif ((maxDeque[0] ?? 0) < left) maxDeque.shift();\n\t\t}\n\n\t\tconst startReading = readings[left];\n\t\tconst endReading = readings[right];\n\t\tif (!startReading || !endReading) continue;\n\n\t\tconst durationMinutes = minutesBetween(startReading.timestamp, endReading.timestamp);\n\t\tif (\n\t\t\tdurationMinutes >= minDuration &&\n\t\t\t(best === null || durationMinutes > best.durationMinutes)\n\t\t) {\n\t\t\tlet sum = 0;\n\t\t\tfor (let i = left; i <= right; i++) sum += readings[i]?.value ?? 0;\n\t\t\tbest = {\n\t\t\t\tstartIndex: left,\n\t\t\t\tdurationMinutes,\n\t\t\t\tavgTempF: round(sum / (right - left + 1)),\n\t\t\t};\n\t\t}\n\t}\n\n\treturn best;\n}\n\nconst NON_ANCHOR_PRIORITY: Record<Exclude<TimelineKind, \"start\" | \"end\">, number> = {\n\ttarget: 4,\n\tstall: 3,\n\tpeak: 2,\n\tlow: 1,\n};\n\n/**\n * Turn a chronological series of probe readings into an annotated cook timeline.\n *\n * Milestones detected: the start, the low point, the longest stall, the first\n * target crossing (when a target is given), the peak, and the end. Non-anchor\n * milestones that land on the first or last reading are folded into the start\n * and end lines; when two milestones share a reading, the more specific one\n * wins (target over stall over peak over low).\n *\n * Pure and side-effect free. Readings must already be in Fahrenheit and sorted\n * chronologically (oldest first).\n */\nexport function buildCookTimeline(\n\treadings: TemperatureReading[],\n\toptions?: TimelineOptions,\n): CookTimeline {\n\tconst threshold = options?.stallThresholdDegrees ?? DEFAULT_STALL_THRESHOLD;\n\tconst minStall = options?.stallMinutes ?? DEFAULT_STALL_MINUTES;\n\tconst targetF = options?.targetF;\n\n\tif (readings.length === 0) {\n\t\treturn {\n\t\t\tevents: [],\n\t\t\tstartedAt: null,\n\t\t\tendedAt: null,\n\t\t\tdurationMinutes: 0,\n\t\t\tminTempF: null,\n\t\t\tmaxTempF: null,\n\t\t\ttargetReached: false,\n\t\t};\n\t}\n\n\tconst first = readings[0];\n\tconst lastIndex = readings.length - 1;\n\tconst last = readings[lastIndex];\n\tif (!first || !last) {\n\t\treturn {\n\t\t\tevents: [],\n\t\t\tstartedAt: null,\n\t\t\tendedAt: null,\n\t\t\tdurationMinutes: 0,\n\t\t\tminTempF: null,\n\t\t\tmaxTempF: null,\n\t\t\ttargetReached: false,\n\t\t};\n\t}\n\n\tlet minIndex = 0;\n\tlet maxIndex = 0;\n\tfor (let i = 1; i < readings.length; i++) {\n\t\tconst v = readings[i]?.value ?? 0;\n\t\tif (v < (readings[minIndex]?.value ?? 0)) minIndex = i;\n\t\tif (v > (readings[maxIndex]?.value ?? 0)) maxIndex = i;\n\t}\n\n\tlet targetIndex = -1;\n\tif (targetF !== undefined) {\n\t\tfor (let i = 0; i < readings.length; i++) {\n\t\t\tif ((readings[i]?.value ?? 0) >= targetF) {\n\t\t\t\ttargetIndex = i;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst stall = findLongestStall(readings, threshold, minStall);\n\n\t// Collect non-anchor candidates, then keep the highest-priority one per index.\n\tconst candidates: {\n\t\tindex: number;\n\t\tkind: Exclude<TimelineKind, \"start\" | \"end\">;\n\t\tdetail: string;\n\t}[] = [];\n\tconst inMiddle = (index: number): boolean => index > 0 && index < lastIndex;\n\n\tif (inMiddle(minIndex)) {\n\t\tcandidates.push({\n\t\t\tindex: minIndex,\n\t\t\tkind: \"low\",\n\t\t\tdetail: `Low point ${round(readings[minIndex]?.value ?? 0)}\\u00B0F`,\n\t\t});\n\t}\n\tif (stall && inMiddle(stall.startIndex)) {\n\t\tcandidates.push({\n\t\t\tindex: stall.startIndex,\n\t\t\tkind: \"stall\",\n\t\t\tdetail: `Stall began near ${stall.avgTempF}\\u00B0F, held ${stall.durationMinutes}m`,\n\t\t});\n\t}\n\tif (targetIndex >= 0 && inMiddle(targetIndex) && targetF !== undefined) {\n\t\tcandidates.push({\n\t\t\tindex: targetIndex,\n\t\t\tkind: \"target\",\n\t\t\tdetail: `Hit target ${round(targetF)}\\u00B0F`,\n\t\t});\n\t}\n\tif (inMiddle(maxIndex)) {\n\t\tcandidates.push({\n\t\t\tindex: maxIndex,\n\t\t\tkind: \"peak\",\n\t\t\tdetail: `Peaked at ${round(readings[maxIndex]?.value ?? 0)}\\u00B0F`,\n\t\t});\n\t}\n\n\tconst bestPerIndex = new Map<number, (typeof candidates)[number]>();\n\tfor (const candidate of candidates) {\n\t\tconst existing = bestPerIndex.get(candidate.index);\n\t\tif (!existing || NON_ANCHOR_PRIORITY[candidate.kind] > NON_ANCHOR_PRIORITY[existing.kind]) {\n\t\t\tbestPerIndex.set(candidate.index, candidate);\n\t\t}\n\t}\n\n\tconst toEvent = (index: number, kind: TimelineKind, detail: string): TimelineEvent => ({\n\t\tkind,\n\t\ttimestamp: readings[index]?.timestamp ?? first.timestamp,\n\t\tminuteOffset: minutesBetween(first.timestamp, readings[index]?.timestamp ?? first.timestamp),\n\t\ttempF: round(readings[index]?.value ?? 0),\n\t\tdetail,\n\t});\n\n\tconst events: TimelineEvent[] = [\n\t\ttoEvent(0, \"start\", `Cook started at ${round(first.value)}\\u00B0F`),\n\t];\n\n\tfor (const candidate of [...bestPerIndex.values()].sort((a, b) => a.index - b.index)) {\n\t\tevents.push(toEvent(candidate.index, candidate.kind, candidate.detail));\n\t}\n\n\tif (lastIndex > 0) {\n\t\tevents.push(toEvent(lastIndex, \"end\", `Cook ended at ${round(last.value)}\\u00B0F`));\n\t}\n\n\treturn {\n\t\tevents,\n\t\tstartedAt: first.timestamp,\n\t\tendedAt: last.timestamp,\n\t\tdurationMinutes: minutesBetween(first.timestamp, last.timestamp),\n\t\tminTempF: round(readings[minIndex]?.value ?? 0),\n\t\tmaxTempF: round(readings[maxIndex]?.value ?? 0),\n\t\ttargetReached: targetIndex >= 0,\n\t};\n}\n","import { detectRapidChange, detectStall, type StallOptions } from \"./stall-detection.js\";\nimport type { TemperatureReading } from \"./types.js\";\n\n// ─── Wrap Advisor ────────────────────────────────────────────────────────────\n\n/** The wrap call returned by the advisor. */\nexport type WrapRecommendation = \"wrap-now\" | \"hold\" | \"below-window\" | \"at-target\" | \"no-data\";\n\n/** Default temperature (in Fahrenheit) at which the wrap window opens. */\nexport const DEFAULT_WRAP_AT_F = 160;\n\n/** Default rate (degrees per 5 minutes) below which a climb counts as stalled out. */\nexport const DEFAULT_SLOW_RATE = 1;\n\n/** Input for {@link assessWrap}. Readings are assumed to be in Fahrenheit. */\nexport interface WrapInput {\n\t/** Chronological readings (oldest first), values in Fahrenheit. */\n\treadings: TemperatureReading[];\n\t/** Target internal temperature in Fahrenheit. */\n\ttargetF: number;\n\t/** Temperature at which the wrap window opens. Default 160. */\n\twrapAtF?: number;\n\t/** Sensitivity for the underlying stall detection. */\n\tstallOptions?: StallOptions;\n\t/** Rate (degrees per 5 minutes) at or below which a climb is treated as stalled. Default 1. */\n\tslowRateThreshold?: number;\n}\n\n/** Result of a wrap assessment. */\nexport interface WrapResult {\n\t/** The wrap call. */\n\trecommendation: WrapRecommendation;\n\t/** Human-readable reasoning behind the call. */\n\treason: string;\n\t/** Most recent temperature in Fahrenheit, or null when there are no readings. */\n\tcurrentTempF: number | null;\n\t/** Target internal temperature in Fahrenheit. */\n\ttargetTempF: number;\n\t/** Temperature at which the wrap window opens. */\n\twrapAtF: number;\n\t/** Whether the readings indicate an active stall. */\n\tisStalling: boolean;\n\t/** Duration of the detected stall in minutes (0 when not stalling). */\n\tstallDuration: number;\n\t/** Current rate of change in degrees per 5 minutes. */\n\tratePer5Min: number;\n}\n\nfunction round(value: number): number {\n\treturn Math.round(value * 10) / 10;\n}\n\n/**\n * Decide whether to wrap the cook now (the \"Texas crutch\").\n *\n * The advisor reads a trailing series of probe temperatures and combines the\n * current temperature, the wrap window, stall detection, and rate of change\n * into a single call:\n *\n * - `below-window`: under the wrap temperature, so let the bark set first.\n * - `wrap-now`: inside the wrap window and either stalled or barely climbing.\n * - `hold`: inside the wrap window but still climbing well, so wrapping is optional.\n * - `at-target`: at or above the target, so pull and rest instead.\n * - `no-data`: no readings available yet.\n *\n * Pure and side-effect free. Readings must already be in Fahrenheit and sorted\n * chronologically (oldest first).\n */\nexport function assessWrap(input: WrapInput): WrapResult {\n\tconst wrapAtF = input.wrapAtF ?? DEFAULT_WRAP_AT_F;\n\tconst slowRate = input.slowRateThreshold ?? DEFAULT_SLOW_RATE;\n\tconst { readings, targetF } = input;\n\n\tconst stall = detectStall(readings, input.stallOptions);\n\tconst rapid = detectRapidChange(readings);\n\tconst last = readings.at(-1);\n\tconst current = last ? last.value : null;\n\n\tconst base = {\n\t\ttargetTempF: targetF,\n\t\twrapAtF,\n\t\tisStalling: stall.isStalling,\n\t\tstallDuration: stall.stallDuration,\n\t\tratePer5Min: rapid.rate,\n\t};\n\n\tif (current == null) {\n\t\treturn {\n\t\t\trecommendation: \"no-data\",\n\t\t\treason: \"No readings yet. Start the cook and check back once temperatures are flowing.\",\n\t\t\tcurrentTempF: null,\n\t\t\t...base,\n\t\t};\n\t}\n\n\tif (current >= targetF) {\n\t\treturn {\n\t\t\trecommendation: \"at-target\",\n\t\t\treason: `At ${round(current)}\\u00B0F, already at or above the ${round(targetF)}\\u00B0F target. Pull and rest instead of wrapping.`,\n\t\t\tcurrentTempF: current,\n\t\t\t...base,\n\t\t};\n\t}\n\n\tif (current < wrapAtF) {\n\t\treturn {\n\t\t\trecommendation: \"below-window\",\n\t\t\treason: `At ${round(current)}\\u00B0F, below the ${wrapAtF}\\u00B0F wrap window. Let the bark set first.`,\n\t\t\tcurrentTempF: current,\n\t\t\t...base,\n\t\t};\n\t}\n\n\tif (stall.isStalling) {\n\t\tconst at = round(stall.avgTemp ?? current);\n\t\treturn {\n\t\t\trecommendation: \"wrap-now\",\n\t\t\treason: `Stalled ${stall.stallDuration}m near ${at}\\u00B0F. Wrapping now pushes through the stall.`,\n\t\t\tcurrentTempF: current,\n\t\t\t...base,\n\t\t};\n\t}\n\n\tif (rapid.rate <= slowRate) {\n\t\treturn {\n\t\t\trecommendation: \"wrap-now\",\n\t\t\treason: `Climb stalled to ${round(rapid.rate)}\\u00B0F/5min inside the wrap window. Wrapping speeds the finish.`,\n\t\t\tcurrentTempF: current,\n\t\t\t...base,\n\t\t};\n\t}\n\n\treturn {\n\t\trecommendation: \"hold\",\n\t\treason: `Still climbing ${round(rapid.rate)}\\u00B0F/5min at ${round(current)}\\u00B0F. Wrap is optional; hold for more bark.`,\n\t\tcurrentTempF: current,\n\t\t...base,\n\t};\n}\n"],"mappings":"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","names":["sanitizeLabel","import_promises","undiciRequest","delay","sanitizeLabel","durationMinutes","round","minutesBetween","round"]}