Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "samsung-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev:samsung"],
"port": 4301
}
]
}
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ A build is one **cell**: `nx build samsung --customer=ccl --profile=tizen6`.
| `platforms/<platform>/` | Packaging inputs: `profiles/*.json` (capabilities) + `templates/` (container manifests). **Not app code.** |
| `customers/<slug>/` | Per-cruiseline content: `config.json` (sectioned), `layouts/`, `i18n/`, `assets/`. |
| `tools/` | Build tooling: `executors/build-tv.mjs`, `packaging/` (package-tv, signing, customer-slug), `vite/xtv-aliases.ts`. |
| `docs/` | `signing.md` (manual signing), `config-hot-reload.md` (live config, no reboot), `state-and-storage.md` (state layers + persistence), `tv-platform-reference.md` (keycodes + device APIs), `DEV-PLAYBOOK.md` (skills/workflow). |
| `docs/` | `signing.md` (manual signing), `config-hot-reload.md` (live config, no reboot), `theming.md` (config-driven themes), `state-and-storage.md` (state layers + persistence), `tv-platform-reference.md` (keycodes + device APIs), `DEV-PLAYBOOK.md` (skills/workflow). |
| `signing/` | `signing.example.json` (template). Real creds in gitignored `.signing.local.json`. |

### Key libs
Expand Down Expand Up @@ -117,6 +117,13 @@ Sign a build by exporting `XTV_CCL_*` env (see `docs/signing.md`) before `build`
service-gateway — localStorage is best-effort only (TV quota; wiped on
update/uninstall). zustand was removed (unused; Blits covers reactive state).
Full model: `docs/state-and-storage.md`.
10. **Release version is build-time, not runtime.** Each cruiseline's version lives
in `customers/<line>/release.json` (engineering-owned; gradle-style versionName,
versionCode derived) — baked into the artifact (config.xml/appinfo version, `.wgt`
name, Android versionCode). **NOT** in `config.json` (that's deployment/head-end-
owned + runtime-overridable). Packager precedence: `--version` > `XTV_APP_VERSION`
(CI) > `release.json` > root `package.json`. Don't pass `--version` through
`nx build` — nx swallows it; use `XTV_APP_VERSION` for CI.

## How to…

Expand Down
6 changes: 3 additions & 3 deletions customers/ccl/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"runtime": {
"customer": "ccl",
"locale": "en",
"theme": "ccl-red",
"theme": "carnival-day",
"features": {
"remoteLayouts": true,
"websocketEvents": true,
Expand All @@ -20,8 +20,8 @@
"integrations": {
"runtime": {
"customerId": "ccl",
"layoutSource": "liferay",
"contentSource": "liferay"
"layoutSource": "local",
"contentSource": "local"
},
"liferayApi": {
"baseUrl": "https://portal.ccl.example.invalid/",
Expand Down
4 changes: 4 additions & 0 deletions customers/ccl/release.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"_comment": "Engineering-owned release version for this cruiseline (gradle-style versionName). Baked into the artifact at build time — NOT runtime config, NOT head-end-overridable. CI may override via XTV_APP_VERSION. versionCode is derived from this.",
"version": "1.0.0"
}
97 changes: 75 additions & 22 deletions docs/signing.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Signing xTV Platform Artifacts

Every production TV artifact must be **signed** with the cruiseline's own
certificate. Signing is **cert-pluggable**: the build injects credentials when
they are available and otherwise emits an **unsigned** artifact plus a loud
warning (the build stays green so procurement is never a blocker). Secrets are
**Samsung** and **Android** production artifacts must be **signed** with the
cruiseline's own certificate; **LG/webOS does not** (an unsigned `.ipk` is
deployable — see §4). Signing is **cert-pluggable**: the build injects credentials
when available, otherwise emits an unsigned artifact + a warning (Samsung/Android
only) so the build stays green and procurement is never a blocker. Secrets are
**never** committed.

This guide covers both paths:
Expand All @@ -25,7 +26,7 @@ The resolver `tools/packaging/signing.mjs` reads, per `(cruiseline, platform)`:
| Platform | Variables |
|---|---|
| Samsung | `XTV_<LINE>_TIZEN_PROFILE` (Certificate Manager profile name), `XTV_TIZEN_CLI` (path to the `tizen` bin dir) |
| LG | `XTV_<LINE>_WEBOS_SIGN_KEY` (Pro:Centric signing cert), `XTV_WEBOS_CLI` (path to the ares bin dir) |
| LG | `XTV_WEBOS_CLI` (path to the ares bin dir). `XTV_<LINE>_WEBOS_SIGN_KEY` is **optional** — Pro:Centric SI signing only; not needed for dev/store. |
| Android | `XTV_<LINE>_ANDROID_KEYSTORE`, `XTV_<LINE>_ANDROID_KS_PASS`, `XTV_<LINE>_ANDROID_KEY_ALIAS`, `XTV_<LINE>_ANDROID_KEY_PASS` |

Example (CCL):
Expand Down Expand Up @@ -76,43 +77,95 @@ tizen security-profiles add -n ccl-dev-2 \
-d /path/to/distributor.p12 -dp <distributorPass>
```

**Sign + package a stage:**
**Sign via the build (recommended)** — set the profile and the build signs the
`.wgt` with `tizen package -s`:
```bash
cd ~/.tizen-extension-platform/server/sdktools/data/tools/ide/bin
./tizen package -t wgt -s ccl-dev-2 -- \
export XTV_CCL_TIZEN_PROFILE=ccl-dev-2
npm run build:samsung -- --customer=ccl # signed .wgt + sssp_config.xml
npm run build:samsung -- --customer=ccl --sssp=1.01 # bump SSSP <ver> for a redeploy
```

> **App version** comes from `customers/<line>/release.json` (engineering-owned),
> or `XTV_APP_VERSION` for CI — it sets the `.wgt` name (`CCL_T65_<ver>`) and
> Android versionCode. Do **not** pass `--version` through `nx build` (nx swallows
> it). This is separate from the SSSP `<ver>` (`--sssp`), which only triggers the
> TV re-download.

> ⚠️ **A real Samsung TV rejects an UNSIGNED `.wgt`** — the packager's zip
> fallback (no profile set) installs nowhere and shows **"unable to install custom
> app."** If install fails, this is almost always the cause. Verify:
> `unzip -l <wgt> | grep -i signature` → must list `author-signature.xml` +
> `signature1.xml`. If empty, `XTV_CCL_TIZEN_PROFILE` wasn't set.

**Or sign a stage manually:**
```bash
tizen package -t wgt -s ccl-dev-2 -- \
/abs/path/dist/platforms/samsung/ccl/tizen6/stage
# produces stage/<name>.wgt — move it to artifacts/ and name it CCL_T6_<ver>.wgt
# move stage/<name>.wgt artifacts/CCL_T65_<ver>.wgt
```

**Deploy (SSSP / URL-Launcher):** copy **both** the `.wgt` and `sssp_config.xml`
to the launcher folder the TV points at. `sssp_config.xml` `<size>` must equal the
`.wgt` byte size (the build generates it correctly); bump `<ver>` to force a
re-download.
`.wgt` byte size (the build sets it); **bump `<ver>` (via `--sssp`) on every
redeploy** or the TV won't re-download a version it already tried.

> **DUID:** the distributor cert must include the target TV's DUID. A cert issued
> for one TV won't install on a different TV — re-issue the distributor cert with
> the new DUID.
>
> Known limitation: even with a partner cert, `$B2BAPIS`/avplay may require the
> MDC/B2B install channel rather than URL-Launcher download. Signing alone does
> not grant it.

---

## 4. LG / webOS (`.ipk`)
## 4. LG / webOS (`.ipk`) — packaging & install

**webOS does NOT need a Samsung-style distributor cert.** An unsigned `.ipk` is a
valid, deployable artifact: it installs on a Dev Mode TV as-is, and the Content
Store signs on submission. Pro:Centric (SI) signing is the only exception and is
optional — set `XTV_CCL_WEBOS_SIGN_KEY` to have the build apply it.

**Prereqs:** webOS TV SDK (`ares-*` CLI), e.g. `/opt/webOS_TV_SDK/CLI/bin`.

**Package a stage:**
**Package a stage** (the build does this; manual equivalent):
```bash
ares-package dist/platforms/lg/ccl/webos6/stage -o dist/platforms/lg/ccl/webos6/artifacts
```

**Install to a dev TV / emulator:**
```bash
ares-setup-device # register the TV once
ares-install --device <name> dist/platforms/lg/ccl/webos6/artifacts/*.ipk
```

**Pro:Centric (hospitality) signing:** the `.ipk` is signed with the LG SI /
Pro:Centric certificate using LG's tooling once the cert is provisioned. Set
`XTV_CCL_WEBOS_SIGN_KEY` so the automated build applies it.
### Dev install (Developer Mode TV)

1. **Enable Developer Mode** (one-time): Content Store → install the **Developer
Mode** app → log in with your LG developer account → toggle **Dev Mode ON**
(TV reboots). The app shows the TV **IP**, port **9922**, and a **passphrase**.
Keep the TV on the same network.
2. **Register the TV** (one-time):
```bash
ares-setup-device # add device: name, IP, port 9922
ares-novacom --device <name> --getkey # enter the Dev Mode passphrase
```
3. **Install the `.ipk`:**
```bash
ares-install --device <name> \
dist/platforms/lg/ccl/webos6/artifacts/com.xcontrol.ccl.webos_0.1.0_all.ipk
```
4. **Launch** (app id = LG `app-identity`):
```bash
ares-launch --device <name> com.xcontrol.ccl.webos
```
5. **Debug (web inspector):**
```bash
ares-inspect --device <name> --app com.xcontrol.ccl.webos --open
```

Housekeeping: `ares-install --device <name> --list` / `--remove com.xcontrol.ccl.webos`.
Dev Mode session expires ~**50 hours** — reopen the Developer Mode app and extend.

### Commercial / hospitality (fleet)

Not `ares`. Pro:Centric TVs are provisioned centrally via the **Pro:Centric Smart /
SI server** (or USB commercial config), which pushes the app to cabins. This is the
XMM / head-end integration path — separate from dev sideload.

---

Expand Down
44 changes: 44 additions & 0 deletions docs/theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Theming

Themes are **config-driven**: the tenant config picks one theme by id. Multiple
themes ship in the app; the config selects which one is active. (Device/group-
driven theming — different theme per balcony/lobby/cabin — comes later via XMM;
it's not wired in the client.)

## Concept — "moments aboard"

Carnival's funnel (red / white / blue) is the constant brand identity; each theme
shifts the *environment* (light + place). Brand anchors: Carnival Navy `#003C71`,
Carnival Red `#E4002B`, Sun Gold `#FFB81C`.

| Theme id | Mood |
|---|---|
| `carnival-day` | bright atrium — sky-white bg, navy text, red accent |
| `carnival-sea` | ocean immersive — deep navy bg, white text, gold accent |
| `carnival-sunset` | golden hour — plum bg, cream text, coral accent |
| `carnival-night` | dark, low-glare — near-black navy, red glow accent |

## Tokens

Each theme is a `ThemeTokens` (`libs/themes`): `background`, `surface`, `text`,
`textMuted`, `accent`, `accentText`, `focus`. `focus` is the D-pad focus color —
always high-contrast on its background (TVs are navigated from a couch). Resolve
one with `getTheme(id)`.

## Config

In `customers/<line>/config.json` → `runtime`:

```jsonc
"theme": "carnival-day"
```

`runtime-config` sets `runtimeConfig.theme` from this; the root app resolves it via
`getTheme(id)`, passes the tokens into components, and seeds it into
`this.$appState.theme`. Changing the theme is a config change (and takes effect on
a head-end config push, no rebuild).

## Adding a theme

Add a `ThemeTokens` in `libs/themes`, register it, set its id as a tenant's
`theme`. New brand = its own theme set; nothing else changes.
22 changes: 16 additions & 6 deletions libs/core/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import Blits from "@lightningjs/blits";
import { cclTheme } from "@x-tv/themes";
import { getTheme } from "@x-tv/themes";
import { HelloWorld } from "@x-tv/widgets";
import { getBootConfig } from "./boot-config";

// Root Blits Application. Renders the Hello World screen; its caption reads
// global reactive state (this.$appState, seeded in core). Next: a config-driven,
// feature-gated multi-widget layout engine.
// Root Blits Application. Resolves the active theme (config-driven, per location)
// and passes its tokens down. Caption reads global reactive state (this.$appState).
// Next: a config-driven, feature-gated multi-widget layout engine.
export default Blits.Application({
components: { HelloWorld },
template: `
<Element w="1920" h="1080" color="$background">
<HelloWorld background="$background" />
<HelloWorld
background="$background"
text="$text"
textMuted="$textMuted"
accent="$accent"
/>
</Element>
`,
state() {
const theme = getTheme(getBootConfig().theme);
return {
background: cclTheme.colors.background,
background: theme.colors.background,
text: theme.colors.text,
textMuted: theme.colors.textMuted,
accent: theme.colors.accent,
};
},
});
29 changes: 20 additions & 9 deletions libs/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,31 @@ export async function bootstrapTvPlatform(
customer: runtimeConfig.customer,
platform: runtimeConfig.platform.platform,
locale: runtimeConfig.locale,
theme: runtimeConfig.theme,
});

// Mount diagnostics FIRST so its on-screen console is available even if the
// Blits launch fails on-device (unlock with the remote PIN). Errors then
// surface on the TV instead of a blank screen.
if (runtimeConfig.diagnostics.enabled) {
diagnostics.mount();
}

// Launch the Blits (LightningJS canvas) app into #app. The font set is
// tenant-driven (customers/<line>/config.json `fonts`), served from the
// tenant public dir with relative paths so they resolve under file://.
Blits.Launch(App, "app", {
w: 1920,
h: 1080,
debugLevel: 1,
defaultFont: runtimeConfig.fonts.default,
fonts: runtimeConfig.fonts.families,
} as Parameters<typeof Blits.Launch>[2]);
if (runtimeConfig.diagnostics.enabled) {
diagnostics.mount();
// multithreaded:false — the renderer worker fails under file:// on some TVs.
try {
Blits.Launch(App, "app", {
w: 1920,
h: 1080,
debugLevel: 1,
multithreaded: false,
defaultFont: runtimeConfig.fonts.default,
fonts: runtimeConfig.fonts.families,
} as Parameters<typeof Blits.Launch>[2]);
} catch (error) {
console.error("Blits.Launch failed", error);
}
connectLiveConfig();
connectMuting();
Expand Down
19 changes: 14 additions & 5 deletions libs/runtime-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,16 @@ interface TenantIntegrations extends ServiceGatewayConfig {
configUrl?: string | null;
}

interface TenantRuntimeSection {
customer: string;
locale: string;
theme: string; // theme id (device/group-driven theming comes later via XMM)
features: Record<string, boolean>;
diagnostics: RuntimeConfig["diagnostics"];
}

interface TenantConfigFile {
runtime: Omit<
RuntimeConfig,
"layout" | "platform" | "services" | "keymapOverride" | "realtime" | "fonts"
>;
runtime: TenantRuntimeSection;
integrations: TenantIntegrations;
identity?: unknown;
keymap?: KeymapConfig;
Expand Down Expand Up @@ -115,10 +120,14 @@ export function createRuntimeConfigLoader(options: {
// is authoritative and merged on top — so config changes without a rebuild.
const merged = await applyRemoteOverride(bundledConfig);
const integrations = merged.integrations;
const runtime = merged.runtime;

return {
...merged.runtime,
customer,
locale: runtime.locale,
theme: runtime.theme,
features: runtime.features,
diagnostics: runtime.diagnostics,
layout: bundledLayout,
platform: {
...profile,
Expand Down
Loading
Loading