Seed the AppModel settings row and expose language/workspace options

Upstream's own web installer inserts the single-row AppModel settings
table that the admin Environment tab (Language/Timezone/SMTP) reads
and writes via app()/AppModel::updateSet(). hortusfox-setup bypasses
that installer and never created this row, so AppModel::updateSet()'s
WHERE-less UPDATE matched zero rows: every admin "Save" reported
success but changed nothing, and app_mail_config() was nulling out
$_ENV['SMTP_*'] on every request from the same missing row, silently
breaking outbound mail. Seed the row on first boot, and add
language/workspace options (alongside the existing timezone) so
APP_LANG/APP_WORKSPACE and the seeded row both follow them.
This commit is contained in:
2026-10-04 11:02:32 -04:00
parent 0c49938796
commit 88ed96550a
+51 -3
View File
@@ -91,8 +91,8 @@ let
APP_CONTACT = "dbrendel1988@gmail.com";
APP_DEBUG = false;
APP_BASEDIR = "";
APP_LANG = "en";
APP_WORKSPACE = "My workspace";
APP_LANG = cfg.language;
APP_WORKSPACE = cfg.workspace;
APP_OVERDUETASK_HOURS = 10;
APP_CRONJOB_MAILLIMIT = 5;
APP_GITHUB_URL = "https://github.com/danielbrendel/hortusfox-web";
@@ -256,6 +256,23 @@ in {
timezone = lib.mkOption { type = lib.types.str; default = "UTC"; };
language = lib.mkOption {
type = lib.types.str;
default = "en";
example = "de";
description = ''
UI language (APP_LANG). Must match one of the language codes
upstream ships translation files for under lang/.
'';
};
workspace = lib.mkOption {
type = lib.types.str;
default = "My workspace";
example = "Greenhouse";
description = "Workspace name shown in the UI (APP_WORKSPACE).";
};
database.name = lib.mkOption { type = lib.types.str; default = "hortusfox"; };
secretFile = lib.mkOption {
@@ -492,7 +509,38 @@ in {
if [ ! -e "${stateDir}/.installed" ]; then
php asatru migrate:fresh
# 5. Seed the initial admin account. migrate:fresh creates no
# 5. Seed the AppModel settings row. migrate:fresh only creates
# the (empty) table -- upstream's own web installer is what
# inserts the single settings row that app()/AppModel's own
# admin-panel features (the Environment tab's Language/
# Timezone fields, SMTP toggle, etc.) read and write, and we
# bypass that installer entirely. Without this row,
# AppModel::updateSet()'s `UPDATE AppModel SET ...` -- no
# WHERE clause, since it's a de-facto single-row table --
# matches zero rows, so every "Save" in the admin Environment
# tab reports success but silently changes nothing. This
# includes Language/Timezone: those live here, not in .env
# (.env's APP_LANG is only a fallback used when this row is
# missing, and APP_TIMEZONE only sets PHP's global default
# timezone -- see AppHelper.php/bootstrap.php upstream,
# neither is what the admin UI's fields actually edit). It
# also means app_mail_config() -- called at the top of every
# single request/cronjob -- was overwriting $_ENV['SMTP_*']
# with NULLs read from this nonexistent row on every request,
# silently breaking outbound mail regardless of smtp.*/
# secretFile; seeding smtp_* here fixes that too. smtp_password
# is deliberately left NULL: it's secret-only (see
# secretFile), not a plain Nix option, and leaving it NULL is
# no regression versus the previous (rowless) behavior either
# way. cronjob_pw is generated the same way AppModel's own
# generateCronjobToken() does. Every other column either has
# no corresponding Nix option yet or just mirrors the
# migration's own column default, same as upstream's
# installer does.
cronjob_pw="$(php -r 'echo md5(random_bytes(55) . date("Y-m-d H:i:s"));')"
mariadb "${cfg.database.name}" -e "INSERT INTO AppModel (id, workspace, language, timezone, scroller, chat_enable, chat_timelimit, chat_showusers, chat_indicator, chat_system, history_enable, history_name, enable_media_share, cronjob_pw, overlay_alpha, smtp_enable_auth, smtp_fromname, smtp_fromaddress, smtp_host, smtp_port, smtp_username, smtp_password, smtp_encryption, pwa_enable, owm_enable, owm_api_key, owm_latitude, owm_longitude, owm_unittype, owm_cache) VALUES (NULL, '${sqlEscape envVars.APP_WORKSPACE}', '${sqlEscape cfg.language}', '${sqlEscape cfg.timezone}', 1, 1, 5, 1, 1, 1, 1, 'History', 0, '$cronjob_pw', NULL, 1, '${sqlEscape cfg.smtp.fromName}', '${sqlEscape cfg.smtp.fromAddress}', '${sqlEscape cfg.smtp.host}', ${toString cfg.smtp.port}, '${sqlEscape cfg.smtp.username}', NULL, '${sqlEscape cfg.smtp.encryption}', 0, 0, NULL, NULL, NULL, 'default', 300);"
# 6. Seed the initial admin account. migrate:fresh creates no
# user rows at all, and the app's only "create user" flow
# lives behind the admin panel -- without this, a fresh
# deployment has no way to log in, ever. Insert directly