Files
Skald-Circle/web/components/login-page.js
T
dguiducci 126886e309 Major rebranding, i18n, dashboard, shared folders, role capabilities
- Rebrand: new app/agent icons, SKALD.md, warm "paper" CSS palette
  (terracotta accent, --radius tokens, WCAG contrast, reduced-motion),
  updated favicon, tray icon, skaldkonur asset
- i18n: backend crate (i18n.rs, locale column, ui_locale config),
  frontend library (web/lib/i18n.js, I18nMixin, t(key)),
  translation files (web/i18n/), every component wired
- Dashboard: <dashboard-page> replaces old home-page content;
  <app-copilot> becomes the landing page (full/dock layout modes)
- Shared folders: API endpoints (shared_folders.rs), frontend page,
  can_write membership, container mount topology, user_fs routing
- Role capabilities: new db table & authorization seam (data not enums),
  roles.attrs JSON for ui_mode / interface select
- Setup: skald-setup prompts for language + password, sets ui_locale
- General: components migrated to CSS variables, Lit conventions cleanup,
  connectors/catalog/marketplace/approval refactoring
2026-07-18 21:38:42 +01:00

108 lines
2.8 KiB
JavaScript

import { html } from 'lit';
import { LightElement } from '../lib/base.js';
import { t, I18nMixin } from '../lib/i18n.js';
export class LoginPage extends I18nMixin(LightElement) {
static get properties() {
return {
_username: { state: true },
_password: { state: true },
_error: { state: true },
_busy: { state: true },
};
}
constructor() {
super();
this._username = '';
this._password = '';
this._error = null;
this._busy = false;
}
_submit(e) {
e.preventDefault();
if (this._busy) return;
this._error = null;
if (!this._username.trim() || !this._password) {
this._error = t('login.missing');
return;
}
this._doLogin();
}
async _doLogin() {
this._busy = true;
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: this._username.trim(),
password: this._password,
}),
});
if (!res.ok) {
this._error = t('login.error');
return;
}
// Logged in — reload into the app.
window.location.reload();
} catch {
this._error = t('login.network');
} finally {
this._busy = false;
}
}
render() {
const btnLabel = this._busy
? html`<span class="login-spinner"></span>${t('login.signing')}`
: t('login.submit');
return html`
<div class="login-page">
<form class="login-card" @submit=${this._submit} autocomplete="on">
<div class="login-logo">
<img src="/assets/icons/icon-192.png" alt="Skald" />
</div>
<h1 class="login-title">${t('login.title')}</h1>
<p class="login-subtitle">${t('login.subtitle')}</p>
${this._error ? html`<div class="login-error">${this._error}</div>` : null}
<div class="mb-3">
<label class="form-label">${t('login.username')}</label>
<input
type="text"
class="form-control"
autocomplete="username"
.value=${this._username}
@input=${e => this._username = e.target.value}
?disabled=${this._busy} />
</div>
<div class="mb-3">
<label class="form-label">${t('login.password')}</label>
<input
type="password"
class="form-control"
autocomplete="current-password"
.value=${this._password}
@input=${e => this._password = e.target.value}
?disabled=${this._busy} />
</div>
<button type="submit" class="btn btn-primary login-submit" ?disabled=${this._busy}>
${btnLabel}
</button>
</form>
</div>
`;
}
}