diff --git a/AGENTS.md b/AGENTS.md index 7f5f0d7..d410344 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,6 +92,23 @@ as project instructions, not as optional documentation. proof. - Do not hand-edit generated frontend output. +## Phone Configurator parity (mandatory) + +- Every new, changed, renamed, moved, or removed configurable value in + `sky_phone/config/config.lua` or `sky_phone/config/media.lua` **must** be + reflected in the in-game Phone Configurator in the same change. Config-only + changes without matching Configurator support are incomplete and must not be + committed or merged. +- Keep the Configurator's runtime schema, defaults, value types, fixed versus + extensible collection rules, labels, descriptions, English and German + locales, SQL persistence, validation, and save/load roundtrip aligned with + the Lua configuration. +- The Configurator must remain complete when file-based configuration is + disabled. Do not introduce a setting that can only be managed by editing Lua + after `Config.PhoneConfigurator.Enabled` is enabled. +- Add or update contract/fixture coverage so configuration parity regressions + fail automated checks. + ## Working method and verification 1. Trace the relevant client, server, NUI, configuration, persistence, and event diff --git a/README.md b/README.md index 11fc060..7597172 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,34 @@ The files contain clearly separated sections for: Restart `sky_phone` after changing Lua configuration. +### In-game phone configurator + +Set the switch at the beginning of `config/config.lua` to use SQL-backed configuration: + +```lua +Config.PhoneConfigurator = { + Enabled = true, +} +``` + +When enabled, `config.lua` and the server-only `media.lua` are first-run defaults. Sky Phone creates +the `sky_phone_configurator` table automatically, loads its saved values before framework and phone +modules initialize, and exposes the editor through `/phonepanel`. Nothing autosaves: stage changes +in the Phone Configurator tool and press the green check. Saving verifies both SQL payloads and then +applies the new server, client, media, app, item, command, provider, animation, and UI values through +Sky Phone's internal runtime refresh. It does not execute a resource restart command. + +Every `Config.*` value from `config.lua`, including server-only sections, and every value from +`Config.Media` is discovered automatically. The bootstrap switch above intentionally remains +file-owned because it decides whether SQL configuration is loaded. Lists, nested objects, vectors, +and numeric-keyed Lua tables use structured editors instead of raw JSON. Shipped schema rows stay +editable but cannot be renamed, converted, or removed. Every list and table still accepts any number +of additional rows; administrator-added rows remain removable. Company job keys are intentionally +fully removable because `Config.Companies.Definitions` is a freely managed job collection. + +Media API keys and server peppers are never returned in plaintext to the NUI. Existing secrets are +shown only as configured and are replaced only when an administrator enters a new value. + ### Language Available locales: @@ -541,7 +569,11 @@ Select `rtx`, `quasar`, `vms`, `rx`, `nolag`, `sn`, `esx_property`, or `qbx_prop ### Companies -Company jobs, public profiles, service numbers, services, permissions, locations, and default availability are configured under `Config.Companies.Definitions`. +Company jobs, public profiles, service numbers, services, permissions, locations, and default +availability are configured under `Config.Companies.Definitions`. Definitions are not limited to the +shipped jobs: add any number of company IDs in the in-game configurator and fill the freely +configurable `Job` value in the automatically generated full company template. Existing job keys can +also be removed; the remaining Companies settings stay available as normal individual fields. ### Weazel News diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 21f575a..095f0cf 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -11,6 +11,7 @@ import { import { useRoute, useRouter } from 'vue-router' import { SkyProvider } from '@/ui' +import AdminPanel from '@/components/AdminPanel.vue' import PhoneHomeIndicator from '@/components/PhoneHomeIndicator.vue' import PhoneControlCenter from '@/components/PhoneControlCenter.vue' import PhoneDynamicIsland from '@/components/PhoneDynamicIsland.vue' @@ -115,8 +116,13 @@ type AppMessage = { | CustomAppCatalogEventData | CustomAppEventData | NavigationEventData + | AdminPanelOpenPayload } +type AdminPanelOpenPayload = Required< + Pick +> + type CustomAppCatalogEventData = { apps?: unknown } @@ -357,6 +363,9 @@ const appTransitionName = computed(() => route.query.transition === 'app-switch' ? 'app-switch' : 'app-window', ) const isLocked = ref(false) +const adminPanelOpen = ref( + isDevelopment && developmentParameters.has('adminPanel'), +) const springboardEditing = ref(false) const isUnlocking = ref(false) const passcodeBusy = ref(false) @@ -630,6 +639,8 @@ function loadUnlockedPhoneData(): void { } function completePhoneSetup(): void { + const requestedRoute = pendingUnlockRoute.value + pendingUnlockRoute.value = null setupPreviewDismissed.value = true setupAppearanceSelected.value = false isLocked.value = false @@ -637,7 +648,7 @@ function completePhoneSetup(): void { passcodeVisible.value = false passcodeRequired.value = false controlCenterOpened.value = false - void router.replace('/') + void router.replace(requestedRoute ?? '/') loadUnlockedPhoneData() } @@ -721,7 +732,15 @@ function openDevelopmentPayphonePreview(): void { function onMessage(event: MessageEvent): void { if (!isTrustedRootMessageSource(event.source, window)) return - if (event.data?.type === 'custom-apps:catalog') { + if (event.data?.type === 'admin:open') { + const data = event.data.data as AdminPanelOpenPayload | undefined + if (data?.lang && data.locales && data.fallbackLocales) { + phone.setLocale(data.lang, data.locales, data.fallbackLocales) + } + adminPanelOpen.value = true + } else if (event.data?.type === 'admin:close') { + adminPanelOpen.value = false + } else if (event.data?.type === 'custom-apps:catalog') { appCatalog.replaceCatalog(event.data.data) const catalogPayload = event.data.data as | { apps?: unknown; debug?: unknown } @@ -766,7 +785,12 @@ function onMessage(event: MessageEvent): void { isPhoneAppId(data.appId) && appStore.isInstalled(data.appId) ) { - void router.push(`/apps/${data.appId}`) + const requestedRoute = `/apps/${data.appId}` + if (setupRequired.value || isLocked.value) { + pendingUnlockRoute.value = requestedRoute + } else { + void router.push(requestedRoute) + } } else { console.error('[Navigation] Ignored an unavailable app target.') } @@ -1731,6 +1755,15 @@ onBeforeUnmount(() => {