Build API v2

This commit is contained in:
NeilClack
2026-08-07 14:06:20 -05:00
parent b2091deae0
commit 64f7070ad7
50 changed files with 25534 additions and 26502 deletions
+161
View File
@@ -0,0 +1,161 @@
// Canonical solfeggio id <- Hz, mirroring the kiosk's `frequencyIdForHz()`
// map (radiantwave-rebuild src/audio/audio_downloader.cpp). v2 must emit these
// canonical ids (1-9), never the portal DB autoincrement ids, because
// proclamations and color palettes on the kiosk are keyed by them.
export const CANONICAL_ID_FOR_HZ: Record<number, number> = {
174: 1,
285: 1,
396: 2,
417: 3,
432: 4,
528: 5,
639: 6,
741: 7,
852: 8,
963: 9
};
// The 9 canonical frequencies exactly as the kiosk expects from
// GET /frequencies (id 1-9, non-zero, `value` recognized by frequencyIdForHz).
export const CANONICAL_FREQUENCIES: { id: number; name: string; value: number }[] = [
{ id: 1, name: 'Grounding / Relief', value: 174 },
{ id: 2, name: 'Root', value: 396 },
{ id: 3, name: 'Sacral', value: 417 },
{ id: 4, name: 'Clarity / Wellbeing', value: 432 },
{ id: 5, name: 'Solar Plexus', value: 528 },
{ id: 6, name: 'Heart', value: 639 },
{ id: 7, name: 'Throat', value: 741 },
{ id: 8, name: '3rd Eye', value: 852 },
{ id: 9, name: 'Crown', value: 963 }
];
// Seed palettes per canonical id, matching the kiosk's
// DatabaseManager::seedColorPalettes(). Used to backfill /color-palettes
// when the DB has not been seeded for an id yet.
export const CANONICAL_PALETTES: Record<number, string[]> = {
1: ['#8B6914', '#B8860B', '#DAA520', '#E8B64C', '#FFD700', '#FFE9A8', '#FFF4CC'],
2: ['#8B0000', '#B22222', '#DC143C', '#FF6347', '#FF8C69', '#FFA500', '#FFDAB9'],
3: ['#FF4500', '#FF7F50', '#FF8C00', '#FFA500', '#FFC87F', '#FFDFBA', '#FFEBCD'],
4: ['#8B2252', '#CD3278', '#DB7093', '#FF69B4', '#FFB6C1', '#FFC0CB', '#FFE4E1'],
5: ['#32CD32', '#3CB371', '#90EE90', '#98FB98', '#ADFFA9', '#C8FFD2', '#F0FFF0'],
6: ['#00FF7F', '#00CED1', '#40E0D0', '#5F9EA0', '#7FFFD4', '#AFEEEE', '#E0FFFF'],
7: ['#00BFFF', '#1E90FF', '#4169E1', '#6495ED', '#87CEEB', '#ADD8E6', '#F0F8FF'],
8: ['#4B0082', '#9370DB', '#8A2BE2', '#7B68EE', '#DA70D6', '#D8BFD8', '#E6E6FA'],
9: ['#9400D3', '#BA55D3', '#C71585', '#DDA0DD', '#EE82EE', '#FFF0F5', '#FAF0FA']
};
/** Map a SolfeggioFrequency row to its canonical id, or null if it has no
* canonical id (e.g. 936 Hz). */
export function canonicalIdForFrequency(freq: { value: number }): number | null {
return CANONICAL_ID_FOR_HZ[freq.value] ?? null;
}
export type V2Proclamation = {
id: string;
name: string;
content: string;
categoryId: number;
solfeggioFrequencyId: number;
subscriptionLevelId: number | null;
colorPalette?: string[];
};
export type ProclamationWithFrequency = {
id: string;
name: string;
content: string;
categoryId: number;
subscriptionLevelId: number | null;
solfeggioFrequency: { value: number };
};
/** Serialize a DB proclamation row to the v2 wire format. Returns null when
* the proclamation's frequency has no canonical id (dropped from v2). */
export function serializeProclamation(
p: ProclamationWithFrequency,
palette?: string[]
): V2Proclamation | null {
const solfeggioFrequencyId = CANONICAL_ID_FOR_HZ[p.solfeggioFrequency.value];
if (!solfeggioFrequencyId) return null;
const record: V2Proclamation = {
id: p.id,
name: p.name,
content: p.content,
categoryId: p.categoryId,
solfeggioFrequencyId,
subscriptionLevelId: p.subscriptionLevelId
};
if (palette && palette.length > 0) {
record.colorPalette = palette;
}
return record;
}
export type V2Category = {
id: number;
name: string;
sortOrder: number;
parentId: number | null;
};
export function serializeCategory(c: {
id: number;
name: string;
sortOrder: number;
parentId: number | null;
}): V2Category {
return { id: c.id, name: c.name, sortOrder: c.sortOrder, parentId: c.parentId };
}
export type V2SubscriptionLevel = {
id: number;
name: string;
description: string | null;
};
export function serializeSubscriptionLevel(level: {
id: number;
name: string;
description: string | null;
}): V2SubscriptionLevel {
return { id: level.id, name: level.name, description: level.description };
}
export type V2License = {
key: string;
email_address: string;
subscription_level: { id: number | null; name: string; description: string | null };
};
/** Build the POST /license response. `subscription_level` is always an object;
* when no SubscriptionLevel row matches the license's level name, fall back to
* a structurally consistent object with a null id/description. */
export function serializeLicense(opts: {
key: string;
emailAddress: string;
levelName: string;
level: { id: number; name: string; description: string | null } | null;
}): V2License {
return {
key: opts.key,
email_address: opts.emailAddress,
subscription_level: opts.level
? { id: opts.level.id, name: opts.level.name, description: opts.level.description }
: { id: null, name: opts.levelName, description: null }
};
}
export type V2ColorPalette = {
solfeggioFrequencyId: number;
colors: string[];
};
export function serializeColorPalette(palette: {
solfeggioFrequencyId: number;
colors: string[];
}): V2ColorPalette {
return { solfeggioFrequencyId: palette.solfeggioFrequencyId, colors: palette.colors };
}