Remove the one-time env-to-config migration from the settings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-09 10:38:06 +01:00
1 parent 151a92959d
commit c9f553dc60
8 files changed
+9 -157

No files matched your search

+7
View File
@@ -3,6 +3,13 @@
All notable changes to CodeCraft. Versions follow [Semantic Versioning](https://semver.org/);
until 1.0, minor versions may contain breaking changes.
## [Unreleased]
### Removed
- *Settings → Config file* no longer offers to move `${NAME}` values into the configuration (a
one-time migration); `${NAME}` references keep working.
## [v202611-00002] – 2026-11-09
### Added
+1 -2
View File
@@ -82,8 +82,7 @@ docker compose -f docker-compose.release.yml logs codecraft # one-time setup c
Open `http://localhost:8787`, enter the setup code and create the admin account. Then add a
provider under *Settings → Providers & models*. `.env` only holds deployment settings; keys,
tokens and everything else are set in the UI and stored in the configuration file
(`${NAME}` references to environment variables still work, and *Settings → Config file* moves
their values into the file).
(`${NAME}` references to environment variables still work).
**Update:** `docker compose -f docker-compose.release.yml pull && docker compose -f docker-compose.release.yml up -d`.
Releases are named `vYYYYMM-NNNNN` (a counter per month). Pin one with
+1 -2
View File
@@ -10,8 +10,7 @@
#
# Keys and tokens are entered in the web UI (Settings) and stored here, readable
# for admins and included in backups. Any string value may also reference an
# environment variable as ${NAME}; Settings → Config file moves such values
# into this file.
# environment variable as ${NAME}.
# =============================================================================
[server]
-19
View File
@@ -66,7 +66,6 @@ pub fn router(app: AppState) -> Router {
.route("/folders/{id}/access", put(set_folder_access))
.route("/config", get(get_config).patch(patch_config))
.route("/config/raw", get(get_config_raw).put(put_config_raw))
.route("/config/inline-env", get(inline_env_preview).post(inline_env_apply))
.route("/providers/reload", post(reload_providers))
.route("/models", get(models))
.route("/agents", get(agents))
@@ -290,24 +289,6 @@ struct RawBody {
text: String,
}
/// Which ${VAR} references the config still has (and whether they are set).
async fn inline_env_preview(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
require_admin(&user)?;
let (_, set, unset) = config::inline_env(&app.config_text()?)?;
Ok(Json(json!({"set": set, "unset": unset})))
}
/// Move the values of set environment variables into the config file.
async fn inline_env_apply(Extension(user): Extension<User>, State(app): State<AppState>) -> ApiResult {
require_admin(&user)?;
let (text, set, unset) = config::inline_env(&app.config_text()?)?;
if !set.is_empty() {
app.save_config(&text).await?;
app.audit(Some(&user), "config.inline_env", set.join(", "), "");
}
Ok(Json(json!({"set": set, "unset": unset})))
}
async fn put_config_raw(Extension(user): Extension<User>, State(app): State<AppState>, Json(body): Json<RawBody>) -> ApiResult {
require_admin(&user)?;
app.save_config(&body.text).await?;
-71
View File
@@ -470,57 +470,6 @@ pub fn load_or_init(path: &Path) -> Result<(Config, String)> {
Ok((cfg, text))
}
/// Replace `${NAME}` references in all string values with the environment's values,
/// so the settings live in the config only. Comments and formatting stay; references
/// to unset variables are kept. Returns (new text, replaced names, unset names).
pub fn inline_env(text: &str) -> Result<(String, Vec<String>, Vec<String>)> {
use std::collections::BTreeSet;
let mut doc: toml_edit::DocumentMut = text.parse().context("config is not valid TOML")?;
let re = regex::Regex::new(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}").unwrap();
let (mut done, mut missing) = (BTreeSet::new(), BTreeSet::new());
let mut fix = |s: &str| -> Option<String> {
if !re.is_match(s) {
return None;
}
let out = re.replace_all(s, |c: &regex::Captures| match std::env::var(&c[1]).ok().filter(|v| !v.is_empty()) {
Some(v) => {
done.insert(c[1].to_string());
v
}
None => {
missing.insert(c[1].to_string());
c[0].to_string()
}
});
(out != s).then(|| out.into_owned())
};
fn walk_value(v: &mut toml_edit::Value, fix: &mut dyn FnMut(&str) -> Option<String>) {
match v {
toml_edit::Value::String(f) => {
if let Some(new) = fix(f.value()) {
let decor = f.decor().clone();
let mut nv = toml_edit::Formatted::new(new);
*nv.decor_mut() = decor;
*f = nv;
}
}
toml_edit::Value::Array(a) => a.iter_mut().for_each(|x| walk_value(x, fix)),
toml_edit::Value::InlineTable(t) => t.iter_mut().for_each(|(_, x)| walk_value(x, fix)),
_ => {}
}
}
fn walk_item(i: &mut toml_edit::Item, fix: &mut dyn FnMut(&str) -> Option<String>) {
match i {
toml_edit::Item::Value(v) => walk_value(v, fix),
toml_edit::Item::Table(t) => t.iter_mut().for_each(|(_, x)| walk_item(x, fix)),
toml_edit::Item::ArrayOfTables(a) => a.iter_mut().for_each(|t| t.iter_mut().for_each(|(_, x)| walk_item(x, fix))),
toml_edit::Item::None => {}
}
}
walk_item(doc.as_item_mut(), &mut fix);
Ok((doc.to_string(), done.into_iter().collect(), missing.into_iter().collect()))
}
/// A structured edit coming from the UI. Applied with toml_edit so comments survive.
#[derive(Debug, Deserialize)]
pub struct ConfigPatch {
@@ -667,26 +616,6 @@ pub fn redacted_json(cfg: &Config) -> serde_json::Value {
v
}
#[cfg(test)]
mod inline_env_tests {
#[test]
fn replaces_set_variables_and_keeps_comments() {
// SAFETY: test-only, the variable names are unique to this test.
unsafe {
std::env::set_var("CC_TEST_INLINE_KEY", "sk-123");
std::env::remove_var("CC_TEST_INLINE_UNSET");
}
let text = "# keys: use ${NAME}\n[providers.a]\napi_key = \"${CC_TEST_INLINE_KEY}\" # stored\nheaders = { Authorization = \"Bearer ${CC_TEST_INLINE_KEY}\" }\nother = \"${CC_TEST_INLINE_UNSET}\"\n";
let (out, done, missing) = super::inline_env(text).unwrap();
assert!(out.contains("api_key = \"sk-123\" # stored"), "{out}");
assert!(out.contains("Bearer sk-123"));
assert!(out.contains("# keys: use ${NAME}"), "comments untouched");
assert!(out.contains("${CC_TEST_INLINE_UNSET}"));
assert_eq!(done, vec!["CC_TEST_INLINE_KEY"]);
assert_eq!(missing, vec!["CC_TEST_INLINE_UNSET"]);
}
}
#[cfg(test)]
mod tests {
use super::*;
-51
View File
@@ -1014,56 +1014,6 @@ function Permissions({ cfg, reload }: { cfg: ConfigResponse; reload: () => void
// --- raw ----------------------------------------------------------------------
/** ${VAR} references left in the config: move their values from the environment into it. */
function EnvRefs({ onDone }: { onDone: () => void }) {
const t = useT();
const { data, reload } = useFetch<{ set: string[]; unset: string[] }>("/config/inline-env");
const [busy, setBusy] = useState(false);
if (!data || (data.set.length === 0 && data.unset.length === 0)) return null;
const apply = async () => {
if (!(await confirmDialog({ title: t("settings.envTitle"), message: t("settings.envConfirm", { names: data.set.join(", ") }), confirmLabel: t("settings.envApply") }))) return;
setBusy(true);
try {
const r = await post<{ set: string[] }>("/config/inline-env");
toast(t("settings.envDone", { n: r.set.length }), "success");
reload();
onDone();
} catch (e: any) {
toast(e.message);
} finally {
setBusy(false);
}
};
return (
<div className="panel" style={{ marginBottom: 12 }}>
<h3>
<Icon name="file-text" /> {t("settings.envTitle")}
</h3>
<div className="hint" style={{ marginBottom: 8 }}>
{t("settings.envHint")}
</div>
{data.set.length > 0 && (
<div className="row" style={{ gap: 8, flexWrap: "wrap", alignItems: "center" }}>
{data.set.map((n) => (
<span key={n} className="chip mono">
{n}
</span>
))}
<span className="spacer" />
<button className="primary small" onClick={apply} disabled={busy}>
{t("settings.envApply")}
</button>
</div>
)}
{data.unset.length > 0 && (
<div className="hint" style={{ marginTop: 8 }}>
{t("settings.envUnset")} <span className="mono">{data.unset.join(", ")}</span>
</div>
)}
</div>
);
}
function Raw({ reload }: { reload: () => void }) {
const t = useT();
const [text, setText] = useState<string>();
@@ -1074,7 +1024,6 @@ function Raw({ reload }: { reload: () => void }) {
if (text === undefined) return <div className="empty">{t("common.loading")}</div>;
return (
<>
<EnvRefs onDone={() => get<{ text: string }>("/config/raw").then((r) => setText(r.text))} />
<div className="notice">{t("settings.rawHint")}</div>
<textarea className="raw-editor" spellCheck={false} value={text} onChange={(e) => setText(e.target.value)} />
{view}
-6
View File
@@ -65,12 +65,6 @@ export const de: Messages = {
"settings.budgetWarning": "Warnen ab",
"settings.adminKey": "Admin-Key (optional)",
"settings.adminKeyHint": "Anthropic Admin-API-Key (sk-ant-admin01-…), um die echten, organisationsweiten Monatskosten anzuzeigen.",
"settings.envTitle": "Einstellungen aus Umgebungsvariablen",
"settings.envHint": "Diese ${NAME}-Verweise holen ihre Werte aus der Server-Umgebung (.env). Übernimm die Werte in die Konfiguration, damit alles hier eingestellt ist – danach können die Variablen aus der .env raus.",
"settings.envApply": "In die Konfiguration übernehmen",
"settings.envConfirm": "Die aktuellen Werte von {names} in die Konfigurationsdatei schreiben? Die Keys liegen dann in der Config (und in ihren Backups) statt in der Umgebung.",
"settings.envDone": "{n} Werte in die Konfiguration übernommen. Du kannst sie jetzt aus der .env entfernen.",
"settings.envUnset": "Verwendet, aber nicht gesetzt (bleiben unverändert):",
"settings.fetchPrices": "Preise abrufen",
"settings.pricesFetched": "Preise für {n} Modelle aktualisiert – zum Übernehmen speichern.",
"stats.perMillion": "pro 1 Mio. Tok.",
-6
View File
@@ -64,12 +64,6 @@ export const en = {
"settings.budgetWarning": "Warn at",
"settings.adminKey": "Admin key (optional)",
"settings.adminKeyHint": "Anthropic Admin API key (sk-ant-admin01-…) to show the real, organisation-wide monthly cost.",
"settings.envTitle": "Settings from environment variables",
"settings.envHint": "These ${NAME} references take their values from the server's environment (.env). Move the values into the configuration so everything is set here – then remove the variables from .env.",
"settings.envApply": "Move into the configuration",
"settings.envConfirm": "Write the current values of {names} into the configuration file? Keys are then stored in the config (and in its backups) instead of the environment.",
"settings.envDone": "{n} values moved into the configuration. You can now remove them from .env.",
"settings.envUnset": "Referenced but not set (left as they are):",
"settings.fetchPrices": "Fetch prices",
"settings.pricesFetched": "Prices updated for {n} models – save to keep them.",
"stats.perMillion": "per 1M tok",