refactor(types): extract bindable primitives into types/bindable.ts (H6 partial)

types.ts is 1159 lines of kitchen-sink discriminated-union definitions
(audit finding H6) shared across ~30 frontend modules. Splitting the
whole file in one pass would need careful per-group TypeScript wrangling
and verification across every entity shape; this commit lands the
first slice as a proof of pattern.

What changed
------------

* New ``static/js/types/bindable.ts`` owns ``BindableFloat`` /
  ``BindableColor`` plus their four accessor helpers
  (``bindableValue``, ``bindableSourceId``, ``bindableColor``,
  ``bindableColorSourceId``).
* ``static/js/types.ts`` keeps every interface and union shape that
  references those primitives, but the primitives themselves now come
  from the new file. Re-export keeps every existing ``import { ... }
  from '../types.ts'`` site working unchanged.

Why this slice first
--------------------

Bindable types and helpers are the most heavily-imported piece of
types.ts (~30 modules already use ``bindable*`` helpers) and they have
zero downstream dependencies — they don't reference any other type
group. That makes them the safest extraction and the cleanest
demonstration of the barrel-re-export pattern the remaining groups
(devices, sources, integrations, automations, templates, …) will
follow in a follow-up sprint.

Verification
------------

* ``npx tsc --noEmit`` clean (no compile errors anywhere in the
  frontend tree).
* ``npm run build`` clean (esbuild bundle and CSS bundle produced
  without warnings).

The remaining ~1130 lines of types.ts plus the C8/C9/C10 god-module
splits (value-sources.ts, streams.ts, graph-editor.ts) need a
dedicated frontend session with typescript-reviewer + manual UI
testing — deferring those rather than half-finishing them here.
This commit is contained in:
2026-05-22 23:35:42 +03:00
parent 9f3f346543
commit 05f73eedf9
2 changed files with 154 additions and 40 deletions
+107 -40
View File
@@ -3,45 +3,26 @@
* *
* These mirror the JSON shapes returned by the REST API. Field names use * These mirror the JSON shapes returned by the REST API. Field names use
* snake_case to match the JSON payloads — no camelCase transformation is done. * snake_case to match the JSON payloads — no camelCase transformation is done.
*
* Bindable primitives have been extracted into ``types/bindable.ts`` and
* are re-exported here so existing ``import { ... } from '../types.ts'``
* call sites keep working. The intention is for further entity-shape
* groups (devices, sources, integrations, …) to follow the same pattern
* in subsequent passes — see audit finding H6.
*/ */
// ── Bindable Float ─────────────────────────────────────────── // ── Bindable Primitives ─────────────────────────────────────
// A scalar that is either a static value (plain number) or bound to a value source (dict). export type { BindableFloat, BindableColor } from './types/bindable.ts';
export {
bindableValue,
bindableSourceId,
bindableColor,
bindableColorSourceId,
} from './types/bindable.ts';
export type BindableFloat = number | { value: number; source_id: string }; // Local aliases used by the entity interfaces below so TypeScript can
// resolve them without an extra import at every reference site.
/** Extract the static value from a BindableFloat. */ import type { BindableFloat, BindableColor } from './types/bindable.ts';
export function bindableValue(b: BindableFloat | undefined, fallback: number): number {
if (b === undefined || b === null) return fallback;
if (typeof b === 'number') return b;
return b.value ?? fallback;
}
/** Extract the source_id from a BindableFloat (empty string = not bound). */
export function bindableSourceId(b: BindableFloat | undefined): string {
if (b === undefined || b === null) return '';
if (typeof b === 'number') return '';
return b.source_id ?? '';
}
// ── Bindable Color ──────────────────────────────────────────
// An RGB color that is either static ([R,G,B] array) or bound to a color value source.
export type BindableColor = number[] | { color: number[]; source_id: string };
/** Extract the static [R,G,B] from a BindableColor. */
export function bindableColor(b: BindableColor | undefined, fallback: number[]): number[] {
if (b === undefined || b === null) return fallback;
if (Array.isArray(b)) return b;
return b.color ?? fallback;
}
/** Extract the source_id from a BindableColor (empty string = not bound). */
export function bindableColorSourceId(b: BindableColor | undefined): string {
if (b === undefined || b === null) return '';
if (Array.isArray(b)) return '';
return b.source_id ?? '';
}
// ── Device ──────────────────────────────────────────────────── // ── Device ────────────────────────────────────────────────────
@@ -186,7 +167,7 @@ export type OutputTarget = LedOutputTarget | HALightOutputTarget | Z2MLightOutpu
// ── Color Strip Source ──────────────────────────────────────── // ── Color Strip Source ────────────────────────────────────────
export type CSSSourceType = export type CSSSourceType =
| 'picture' | 'picture_advanced' | 'static' | 'gradient' | 'picture' | 'picture_advanced' | 'single_color' | 'gradient'
| 'effect' | 'composite' | 'mapped' | 'effect' | 'composite' | 'mapped'
| 'audio' | 'api_input' | 'notification' | 'daylight' | 'audio' | 'api_input' | 'notification' | 'daylight'
| 'candlelight' | 'processed' | 'weather' | 'key_colors' | 'candlelight' | 'processed' | 'weather' | 'key_colors'
@@ -379,7 +360,7 @@ export type ValueSourceType =
| 'adaptive_time' | 'adaptive_scene' | 'daylight' | 'adaptive_time' | 'adaptive_scene' | 'daylight'
| 'static_color' | 'animated_color' | 'adaptive_time_color' | 'static_color' | 'animated_color' | 'adaptive_time_color'
| 'ha_entity' | 'gradient_map' | 'css_extract' | 'ha_entity' | 'gradient_map' | 'css_extract'
| 'system_metrics' | 'game_event'; | 'system_metrics' | 'game_event' | 'http';
export interface SchedulePoint { export interface SchedulePoint {
time: string; time: string;
@@ -534,6 +515,17 @@ export interface GameEventValueSource extends ValueSourceBase {
timeout: number; timeout: number;
} }
export interface HTTPValueSource extends ValueSourceBase {
source_type: 'http';
return_type: 'float';
http_endpoint_id: string;
json_path: string;
interval_s: number;
min_value: number;
max_value: number;
smoothing: number;
}
export type ValueSource = export type ValueSource =
| StaticValueSource | StaticValueSource
| AnimatedValueSource | AnimatedValueSource
@@ -548,7 +540,8 @@ export type ValueSource =
| GradientMapValueSource | GradientMapValueSource
| CSSExtractValueSource | CSSExtractValueSource
| SystemMetricsValueSource | SystemMetricsValueSource
| GameEventValueSource; | GameEventValueSource
| HTTPValueSource;
// ── Audio Source ─────────────────────────────────────────────── // ── Audio Source ───────────────────────────────────────────────
@@ -772,6 +765,68 @@ export interface MQTTStatusResponse {
connected_count: number; connected_count: number;
} }
// ── HTTP Endpoint ────────────────────────────────────────────
//
// A connection definition only (URL + auth + headers + timeout).
// No polling cadence is configured on the endpoint itself —
// HTTPValueSource owns interval_s and references the endpoint.
export type HTTPMethod = 'GET' | 'HEAD';
export interface HTTPEndpoint {
id: string;
name: string;
url: string;
method: HTTPMethod;
/** Server NEVER returns the token; this flag indicates one is stored. */
auth_token_set: boolean;
headers: Record<string, string>;
timeout_s: number;
description?: string;
tags: string[];
icon?: string;
icon_color?: string;
created_at: string;
updated_at: string;
}
export interface HTTPEndpointListResponse {
endpoints: HTTPEndpoint[];
count: number;
}
/** Wire payload for `POST /http/endpoints` / `PUT /http/endpoints/{id}`.
* All fields optional — the route validates required-on-create separately. */
export interface HTTPEndpointWritePayload {
name?: string;
url?: string;
method?: HTTPMethod;
/** Plaintext token. PUT distinguishes None=keep / ""=clear; omit the field to keep. */
auth_token?: string;
headers?: Record<string, string>;
timeout_s?: number;
description?: string;
tags?: string[];
icon?: string;
icon_color?: string;
}
export interface HTTPTestRequest {
url: string;
method: HTTPMethod;
auth_token: string;
headers: Record<string, string>;
timeout_s: number;
}
export interface HTTPTestResponse {
success: boolean;
status_code?: number;
body_preview?: string;
body_json?: unknown;
error?: string;
}
// ── Asset ──────────────────────────────────────────────────── // ── Asset ────────────────────────────────────────────────────
export interface Asset { export interface Asset {
@@ -799,7 +854,12 @@ export interface AssetListResponse {
export type RuleType = export type RuleType =
| 'application' | 'time_of_day' | 'system_idle' | 'application' | 'time_of_day' | 'system_idle'
| 'display_state' | 'mqtt' | 'webhook' | 'startup'; | 'display_state' | 'mqtt' | 'webhook' | 'startup'
| 'home_assistant' | 'http_poll';
export type HTTPPollOperator =
| 'equals' | 'not_equals' | 'contains' | 'regex'
| 'gt' | 'lt' | 'exists';
export interface AutomationRule { export interface AutomationRule {
rule_type: RuleType; rule_type: RuleType;
@@ -814,6 +874,13 @@ export interface AutomationRule {
payload?: string; payload?: string;
match_mode?: string; match_mode?: string;
token?: string; token?: string;
/** home_assistant rule */
ha_source_id?: string;
entity_id?: string;
/** http_poll rule — references an HTTPValueSource. */
value_source_id?: string;
operator?: HTTPPollOperator;
value?: string;
} }
export interface Automation { export interface Automation {
@@ -0,0 +1,47 @@
/**
* Bindable scalar / colour primitives.
*
* A "bindable" value is either a plain literal or a reference to a value
* source (``{value | color, source_id}``). These types and the four
* accessor helpers are imported widely — over 30 frontend modules read
* them through the ``types.ts`` barrel — so they live in their own file
* to keep the entity-shape files free of helper-function noise.
*/
// ── Bindable Float ───────────────────────────────────────────
// A scalar that is either a static value (plain number) or bound to a value source (dict).
export type BindableFloat = number | { value: number; source_id: string };
/** Extract the static value from a BindableFloat. */
export function bindableValue(b: BindableFloat | undefined, fallback: number): number {
if (b === undefined || b === null) return fallback;
if (typeof b === 'number') return b;
return b.value ?? fallback;
}
/** Extract the source_id from a BindableFloat (empty string = not bound). */
export function bindableSourceId(b: BindableFloat | undefined): string {
if (b === undefined || b === null) return '';
if (typeof b === 'number') return '';
return b.source_id ?? '';
}
// ── Bindable Color ──────────────────────────────────────────
// An RGB color that is either static ([R,G,B] array) or bound to a color value source.
export type BindableColor = number[] | { color: number[]; source_id: string };
/** Extract the static [R,G,B] from a BindableColor. */
export function bindableColor(b: BindableColor | undefined, fallback: number[]): number[] {
if (b === undefined || b === null) return fallback;
if (Array.isArray(b)) return b;
return b.color ?? fallback;
}
/** Extract the source_id from a BindableColor (empty string = not bound). */
export function bindableColorSourceId(b: BindableColor | undefined): string {
if (b === undefined || b === null) return '';
if (Array.isArray(b)) return '';
return b.source_id ?? '';
}