mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-01 18:52:21 +01:00
fix(editor): stabilize image generation, layer editing and controls (#6992)
* fix(editor): stabilize image workflows and editing controls
* docs(editor): add desktop and mobile visual validation evidence
* fix(editor): synchronize quick edit model events and register imagePreferences in test coordinator
* fix(settings): enforce authoritative global image-gen policy and non-admin preference persistence
- Enforce authoritative administrator global image_gen_enabled disable in get_user_setting, do_generate_image, and image_editing_provider_available, preventing non-admin user preference overrides while preserving user-disabled opt-outs and can_generate_images privilege checks across direct, gallery, and editor paths.
- Filter non-admin preference persistence in saveImagePreferences to user preference keys only (image_model, image_edit_model, image_quality), preventing global policy toggles from leaking into user preferences.
- Return accurate 200 OK Response from saveImagePreferences after fallback persistence succeeds so settings UI displays 'Saved' instead of false 403 error, while retaining visibility of real errors.
- Load per-user preferences via readImagePreferences in initImageSettings so non-admin selections persist and display correctly after page reload.
- Clear per-user model preferences on successful admin save (POST /api/auth/settings) via PUT /api/prefs/{key} with value: null to prevent stale per-user preferences from shadowing updated global settings.
- Support clearing keys in PUT /api/prefs/{key} when value is null.
- Add comprehensive Python regression tests for global-disabled/user-enabled, global-enabled/user-disabled, and authorized generation, plus Node and browser regression tests for non-admin persistence, toggle isolation, reload, and admin clearing.
* fix(settings): invalidate shared appConfig cache near auth settings write
* fix(test): isolate test_tool_policy unit suite from external model endpoint state
* fix(test): isolate web_fetch failure regression from contextual follow-up heuristics
* fix(test): isolate test_tool_policy from cross-test approval gate state
Ensure _patch_loop_basics disables TOOL_APPROVAL_GATE_ENABLED so routing tests in test_tool_policy run in the intended opt-out posture even when prior tests populate DATA_DIR with skills or untrusted context. Remove unnecessary forced_tools and contextual web mock workarounds.
* fix(settings): isolate non-admin image toggle and surface preference cleanup errors
- Restrict image generation availability toggle to admin users in imageModels.js, disabling the switch for non-admins and reverting unauthorized changes with an explicit message.
- Separate non-admin image model preference saves from global toggle updates.
- Track preference cleanup results on admin save in imagePreferences.js, propagating cleanup failures instead of ignoring them.
- Require valid preference keys on 403 fallback so toggle-only attempts reject cleanly.
- Add /api/prefs route mock to test_image_model_settings_sync_both_directions_and_survive_reopening.
- Add regression coverage for admin cleanup failures and non-admin toggle rejection.
* fix(settings): reconcile admin toggle on partial cleanup failure and report errors
- In imagePreferences.js, distinguish pre-persistence failures from post-save cleanup failures using err.globalSaved and err.phase.
- In imagePreferences.js, notify listeners of global policy updates on partial success before propagating cleanup failure.
- In imageModels.js, retain the persisted target state on admin toggle when global persistence succeeds but cleanup fails.
- In imageModels.js, reconcile admin toggle with authoritative server state via readImagePreferences() after ambiguous or partial failures.
- In imageModels.js, report partial success accurately without suppressing preference cleanup error messages.
- Add regression coverage in test_settings_save_response_ok_js.py for cleanup HTTP 500, cleanup network rejection, successful cleanup, and failed global POST.
---------
Co-authored-by: pewdiepie-archdaemon <pewdiepie-archdaemon@users.noreply.github.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
This commit is contained in:
co-authored by
pewdiepie-archdaemon
Alexandre Teixeira
parent
5353241743
commit
39303f19cd
@@ -54,16 +54,16 @@ export function resolveQuickEditModel() {
|
||||
label: ((opt && opt.textContent) || '').trim(),
|
||||
};
|
||||
}
|
||||
const inp = document.getElementById('ge-ai-inpaint');
|
||||
const opt = resolveInpaintModel(inp);
|
||||
if (!opt) return { endpoint: '', model: '', label: '' };
|
||||
const raw = opt.value || '';
|
||||
const idx = raw.indexOf('::');
|
||||
return {
|
||||
endpoint: idx < 0 ? raw : raw.slice(0, idx),
|
||||
model: idx < 0 ? '' : raw.slice(idx + 2),
|
||||
label: ((opt.textContent || '').trim().replace(/^Auto\s*\((.*)\)$/, '$1')),
|
||||
};
|
||||
const generation = document.getElementById('ge-ai-model');
|
||||
if (generation?.value && generation.value !== '__serve_cookbook__') {
|
||||
const index = generation.value.indexOf('::');
|
||||
const model = index < 0 ? '' : generation.value.slice(index + 2);
|
||||
if (model && modelCaps(model, '', '').inpaint) {
|
||||
return { endpoint: generation.value.slice(0, index), model,
|
||||
label: generation.options[generation.selectedIndex]?.textContent?.trim() || model };
|
||||
}
|
||||
}
|
||||
return { endpoint: '', model: '', label: 'Auto (saved settings)' };
|
||||
}
|
||||
|
||||
// Resolve the Quick Edit AGENT (planner) model — the vision-language model
|
||||
@@ -144,21 +144,19 @@ export function modelCaps(modelId, endpointName, endpointType) {
|
||||
function syncCommandModel(value) {
|
||||
const select = document.getElementById('ge-ai-command-model');
|
||||
if (!select || !select._modelsReady) return;
|
||||
if (value && ![...select.options].some(o => !o.disabled && o.value === value && value !== '__serve_cookbook__')) {
|
||||
if (localStorage.getItem('ge-ai-command-model') === value) localStorage.removeItem('ge-ai-command-model');
|
||||
value = '';
|
||||
}
|
||||
if (select.value !== value) {
|
||||
select.value = value;
|
||||
if (select.value !== value) select.value = '';
|
||||
updateFooterEditModel();
|
||||
}
|
||||
}
|
||||
window.addEventListener('ge-ai-command-model-change', e => syncCommandModel(e.detail?.value || ''));
|
||||
window.addEventListener('storage', e => {
|
||||
if (e.key === 'ge-ai-command-model') syncCommandModel(e.newValue || '');
|
||||
});
|
||||
window.addEventListener('ge:model-endpoints-updated', e => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('ge-ai-command-model-change', e => syncCommandModel(e.detail?.value || ''));
|
||||
window.addEventListener('storage', e => {
|
||||
if (e.key === 'ge-ai-command-model') syncCommandModel(e.newValue || '');
|
||||
});
|
||||
}
|
||||
globalThis.window?.addEventListener('ge:model-endpoints-updated', e => {
|
||||
document.getElementById('ge-ai-command-model')?._refreshAIModels?.(e.detail?.baseUrl || '');
|
||||
});
|
||||
|
||||
@@ -256,9 +254,9 @@ export function wireAIModelSelectors({ container, apiBase = '', openCookbookForI
|
||||
const data = await res.json();
|
||||
if (listeners.signal.aborted || !root.isConnected) return;
|
||||
const modelsList = data.models || [];
|
||||
if (aiGenSelect) aiGenSelect.innerHTML = '<option value="">None</option>';
|
||||
if (aiGenSelect) aiGenSelect.innerHTML = '<option value="">Auto-detect</option>';
|
||||
if (aiInpaintSelect) aiInpaintSelect.innerHTML = '<option value="">Auto</option>';
|
||||
if (aiCommandSelect) aiCommandSelect.innerHTML = '<option value="">Auto (default)</option>';
|
||||
if (aiCommandSelect) aiCommandSelect.innerHTML = '<option value="">Same as generation</option>';
|
||||
const perToolSelects = Array.from(document.querySelectorAll('select.ge-tool-model'));
|
||||
for (const ts of perToolSelects) ts.innerHTML = '<option value="">Auto</option>';
|
||||
let firstGen = null;
|
||||
@@ -346,45 +344,15 @@ export function wireAIModelSelectors({ container, apiBase = '', openCookbookForI
|
||||
// Sentinel changes are handled by the component's delegated listener.
|
||||
if (aiCommandSelect) {
|
||||
aiCommandSelect._modelsReady = true;
|
||||
try {
|
||||
const rawSaved = localStorage.getItem('ge-ai-command-model') || '';
|
||||
let saved = rawSaved;
|
||||
if (saved.startsWith('http://') || saved.startsWith('https://') || saved.includes('://') || saved.startsWith('::')) {
|
||||
const modelPart = saved.includes('::') ? saved.split('::').pop() : '';
|
||||
const matching = loaded.filter(l => l.model === modelPart);
|
||||
if (matching.length === 1) {
|
||||
saved = matching[0].value;
|
||||
} else {
|
||||
saved = '';
|
||||
}
|
||||
try {
|
||||
if (saved) localStorage.setItem('ge-ai-command-model', saved);
|
||||
else localStorage.removeItem('ge-ai-command-model');
|
||||
} catch {}
|
||||
}
|
||||
if (saved && [...aiCommandSelect.options].some(o => o.value === saved)) {
|
||||
aiCommandSelect.value = saved;
|
||||
} else {
|
||||
aiCommandSelect.value = '';
|
||||
}
|
||||
if (aiCommandSelect.value !== rawSaved) {
|
||||
if (aiCommandSelect.value) localStorage.setItem('ge-ai-command-model', aiCommandSelect.value);
|
||||
else localStorage.removeItem('ge-ai-command-model');
|
||||
window.dispatchEvent(new CustomEvent('ge-ai-command-model-change', { detail: { value: aiCommandSelect.value } }));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Legacy URL migration check: legacy endpoints with raw URLs are scrubbed.
|
||||
const scrubModelUrl = (saved) => (saved && saved.startsWith('http://') ? '' : saved);
|
||||
if (aiCommandSelect._commandWired !== listeners) {
|
||||
aiCommandSelect._commandWired = listeners;
|
||||
listen(aiCommandSelect, 'change', () => {
|
||||
if (aiCommandSelect.value === '__serve_cookbook__') return;
|
||||
const val = aiCommandSelect.value || '';
|
||||
try {
|
||||
if (val) localStorage.setItem('ge-ai-command-model', aiCommandSelect.value);
|
||||
else localStorage.removeItem('ge-ai-command-model');
|
||||
} catch {}
|
||||
const val = scrubModelUrl(aiCommandSelect.value) || '';
|
||||
updateFooterEditModel();
|
||||
window.dispatchEvent(new CustomEvent('ge-ai-command-model-change', { detail: { value: val } }));
|
||||
globalThis.window?.dispatchEvent?.(new CustomEvent('ge-ai-command-model-change', { detail: { value: val } }));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -409,6 +377,7 @@ export function wireAIModelSelectors({ container, apiBase = '', openCookbookForI
|
||||
});
|
||||
}
|
||||
_aiModelsStatus = { ready: true, models: loaded, error: null, at: Date.now() };
|
||||
container.dispatchEvent(new Event('ge-image-models-ready'));
|
||||
try { window.dispatchEvent(new CustomEvent('ge:ai-models-status')); } catch {}
|
||||
} catch (e) {
|
||||
if (listeners.signal.aborted || !root.isConnected) return;
|
||||
|
||||
Reference in New Issue
Block a user