Setup: utente admin via web, ruoli e run-context con security-group, onboarding install/uninstall script
Nightly Build / build (push) Failing after 6m12s
Nightly Build / build (push) Failing after 6m12s
This commit is contained in:
@@ -25,6 +25,7 @@ export class AppCopilot extends I18nMixin(ChatSession) {
|
||||
_mode: { state: true },
|
||||
_me: { state: true },
|
||||
_modelOpen: { state: true },
|
||||
_groupOpen: { state: true },
|
||||
_tabs: { state: true },
|
||||
_activeSource: { state: true },
|
||||
_cmdMenu: { state: true },
|
||||
@@ -38,6 +39,7 @@ export class AppCopilot extends I18nMixin(ChatSession) {
|
||||
this._mode = 'dock';
|
||||
this._me = null;
|
||||
this._modelOpen = false;
|
||||
this._groupOpen = false;
|
||||
this._resizing = false;
|
||||
// Slash-command autocomplete: `_cmdMenu` is the filtered list currently shown
|
||||
// (null = hidden), `_cmdSel` the highlighted index, `_allCommands` the merged
|
||||
@@ -62,6 +64,7 @@ export class AppCopilot extends I18nMixin(ChatSession) {
|
||||
this._restoreState();
|
||||
this._loadCommands();
|
||||
this._loadMe();
|
||||
this._loadSecurityGroups();
|
||||
// Same element, two layouts: the chat is the home page ('full') and docks
|
||||
// to the side on every other route — state is never lost, it only resizes.
|
||||
this._applyMode(this._pageFromHash() === 'home' ? 'full' : 'dock');
|
||||
@@ -450,6 +453,29 @@ export class AppCopilot extends I18nMixin(ChatSession) {
|
||||
</button>
|
||||
</div>
|
||||
` : nothing}
|
||||
${this._securityGroups.length > 1 ? html`
|
||||
<div class="copilot-model-wrap">
|
||||
${this._groupOpen ? html`
|
||||
<div class="copilot-model-overlay" @click=${() => { this._groupOpen = false; }}></div>
|
||||
<div class="copilot-model-dropdown">
|
||||
${this._securityGroups.map(g => html`
|
||||
<button
|
||||
class="copilot-model-item ${g.id === this._selectedGroup ? 'active' : ''}"
|
||||
@click=${() => { this._selectGroup(g.id); this._groupOpen = false; }}
|
||||
>${g.name}</button>
|
||||
`)}
|
||||
</div>
|
||||
` : nothing}
|
||||
<button
|
||||
class="copilot-model-pill"
|
||||
title=${t('chat.security_group')}
|
||||
@click=${() => { this._groupOpen = !this._groupOpen; }}>
|
||||
<i class="bi bi-shield-lock"></i>
|
||||
<span>${this._securityGroups.find(g => g.id === this._selectedGroup)?.name ?? this._selectedGroup}</span>
|
||||
<i class="bi bi-chevron-${this._groupOpen ? 'down' : 'up'}"></i>
|
||||
</button>
|
||||
</div>
|
||||
` : nothing}
|
||||
<button
|
||||
class="copilot-toolbar-btn"
|
||||
title=${t('chat.new_session')}
|
||||
|
||||
@@ -67,10 +67,21 @@ export class RolesPage extends LightElement {
|
||||
catch { return 'full'; }
|
||||
}
|
||||
|
||||
_mergeAttrs(attrs, uiMode) {
|
||||
// Extra security-groups the role may pick beyond its default `permission_group`
|
||||
// (the effective set is default ∪ these). Lives in attrs JSON (§0.1).
|
||||
_attrsAllowedGroups(attrs) {
|
||||
try {
|
||||
const a = JSON.parse(attrs || '{}').permission_groups;
|
||||
return Array.isArray(a) ? a : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
_mergeAttrs(attrs, uiMode, allowedGroups) {
|
||||
let o = {};
|
||||
try { o = JSON.parse(attrs || '{}') ?? {}; } catch { o = {}; }
|
||||
if (uiMode === 'simple') o.ui_mode = 'simple'; else delete o.ui_mode;
|
||||
const extras = Array.isArray(allowedGroups) ? allowedGroups.filter(Boolean) : [];
|
||||
if (extras.length) o.permission_groups = extras; else delete o.permission_groups;
|
||||
const keys = Object.keys(o);
|
||||
return keys.length ? JSON.stringify(o) : null;
|
||||
}
|
||||
@@ -78,7 +89,7 @@ export class RolesPage extends LightElement {
|
||||
_openCreate() {
|
||||
this._modal = {
|
||||
mode: 'create',
|
||||
form: { id: '', label: '', permission_group: this._groups?.[0]?.id ?? 'default', attrs: '', ui_mode: 'full' },
|
||||
form: { id: '', label: '', permission_group: this._groups?.[0]?.id ?? 'default', attrs: '', ui_mode: 'full', allowed_groups: [] },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,7 +97,7 @@ export class RolesPage extends LightElement {
|
||||
this._modal = {
|
||||
mode: 'edit',
|
||||
role,
|
||||
form: { label: role.label, permission_group: role.permission_group, attrs: role.attrs ?? '', ui_mode: this._attrsUiMode(role.attrs) },
|
||||
form: { label: role.label, permission_group: role.permission_group, attrs: role.attrs ?? '', ui_mode: this._attrsUiMode(role.attrs), allowed_groups: this._attrsAllowedGroups(role.attrs) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,6 +107,12 @@ export class RolesPage extends LightElement {
|
||||
this._modal = { ...this._modal, form: { ...this._modal.form, [field]: value } };
|
||||
}
|
||||
|
||||
_toggleAllowedGroup(id, checked) {
|
||||
const cur = new Set(this._modal.form.allowed_groups || []);
|
||||
if (checked) cur.add(id); else cur.delete(id);
|
||||
this._patch('allowed_groups', [...cur]);
|
||||
}
|
||||
|
||||
// ── API actions ──────────────────────────────────────────────────────────────
|
||||
|
||||
async _save() {
|
||||
@@ -112,7 +129,7 @@ export class RolesPage extends LightElement {
|
||||
id: form.id.trim(),
|
||||
label: form.label.trim(),
|
||||
permission_group: form.permission_group,
|
||||
attrs: this._mergeAttrs(form.attrs, form.ui_mode),
|
||||
attrs: this._mergeAttrs(form.attrs, form.ui_mode, form.allowed_groups),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
@@ -129,7 +146,7 @@ export class RolesPage extends LightElement {
|
||||
body: JSON.stringify({
|
||||
label: form.label.trim(),
|
||||
permission_group: form.permission_group,
|
||||
attrs: this._mergeAttrs(form.attrs, form.ui_mode),
|
||||
attrs: this._mergeAttrs(form.attrs, form.ui_mode, form.allowed_groups),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
@@ -189,6 +206,18 @@ export class RolesPage extends LightElement {
|
||||
${(this._groups ?? []).map(g => html`<option value=${g.id} ?selected=${form.permission_group === g.id}>${g.name}</option>`)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">${t('roles.form.allowed')}</label>
|
||||
<div class="form-text mb-2" style="font-size:.75rem">${t('roles.form.allowed_hint')}</div>
|
||||
${(this._groups ?? []).filter(g => g.id !== form.permission_group).map(g => html`
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="allow-${g.id}"
|
||||
.checked=${(form.allowed_groups || []).includes(g.id)}
|
||||
@change=${e => this._toggleAllowedGroup(g.id, e.target.checked)} />
|
||||
<label class="form-check-label" for="allow-${g.id}">${g.name}</label>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">${t('roles.form.interface')}</label>
|
||||
<select class="form-select" @change=${e => this._patch('ui_mode', e.target.value)}>
|
||||
|
||||
@@ -11,6 +11,8 @@ export class SetupPage extends I18nMixin(LightElement) {
|
||||
_confirm: { state: true },
|
||||
_encrypted: { state: true },
|
||||
_locale: { state: true },
|
||||
_profiles: { state: true },
|
||||
_profile: { state: true },
|
||||
_error: { state: true },
|
||||
_busy: { state: true },
|
||||
};
|
||||
@@ -23,10 +25,29 @@ export class SetupPage extends I18nMixin(LightElement) {
|
||||
this._confirm = '';
|
||||
this._encrypted = true;
|
||||
this._locale = getLocale();
|
||||
this._profiles = [];
|
||||
this._profile = 'family';
|
||||
this._error = null;
|
||||
this._busy = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadProfiles();
|
||||
}
|
||||
|
||||
async _loadProfiles() {
|
||||
try {
|
||||
const res = await fetch('/api/setup/profiles');
|
||||
if (!res.ok) return;
|
||||
const list = await res.json();
|
||||
if (Array.isArray(list) && list.length) {
|
||||
this._profiles = list;
|
||||
this._profile = list[0].id;
|
||||
}
|
||||
} catch { /* one preset ships; a failed fetch just keeps the default */ }
|
||||
}
|
||||
|
||||
_submit(e) {
|
||||
e.preventDefault();
|
||||
if (this._busy) return;
|
||||
@@ -60,6 +81,7 @@ export class SetupPage extends I18nMixin(LightElement) {
|
||||
password: this._password,
|
||||
encrypted: this._encrypted,
|
||||
locale: this._locale,
|
||||
profile: this._profile,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -92,6 +114,21 @@ export class SetupPage extends I18nMixin(LightElement) {
|
||||
|
||||
${this._error ? html`<div class="setup-error">${this._error}</div>` : null}
|
||||
|
||||
${this._profiles.length > 1 ? html`
|
||||
<div class="mb-3">
|
||||
<label class="form-label">${t('setup.profile')}</label>
|
||||
<select
|
||||
class="form-select"
|
||||
.value=${this._profile}
|
||||
@change=${e => this._profile = e.target.value}
|
||||
?disabled=${this._busy}>
|
||||
${this._profiles.map(p => html`
|
||||
<option value=${p.id} ?selected=${this._profile === p.id}>${p.label}</option>
|
||||
`)}
|
||||
</select>
|
||||
</div>
|
||||
` : null}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">${t('login.username')}</label>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user