ui: add firefly card and onboarding guides
This commit is contained in:
parent
341e10db3d
commit
f08ec8310e
@ -21,315 +21,321 @@
|
||||
Change password
|
||||
</a>
|
||||
<button v-else class="pill mono" type="button" @click="doLogin">Login</button>
|
||||
<a class="pill mono" href="/onboarding">Onboarding</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="auth.ready && auth.authenticated">
|
||||
<div class="account-grid">
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Mail</h2>
|
||||
<span class="pill mono">{{ mailu.status }}</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Use a dedicated app password for IMAP/SMTP clients (mobile mail, Thunderbird, Outlook). Rotate it any time.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">IMAP</span>
|
||||
<span class="v mono">{{ mailu.imap }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">SMTP</span>
|
||||
<span class="v mono">{{ mailu.smtp }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Email</span>
|
||||
<span class="v mono">{{ mailu.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="mailu.rotating" @click="rotateMailu">
|
||||
{{ mailu.rotating ? "Rotating..." : "Rotate mail app password" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mailu.currentPassword" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Current password</div>
|
||||
<div class="secret-actions">
|
||||
<button class="copy mono" type="button" @click="copy('mailu-current', mailu.currentPassword)">
|
||||
copy
|
||||
<span v-if="copied['mailu-current']" class="copied">copied</span>
|
||||
</button>
|
||||
<button class="copy mono" type="button" @click="mailu.revealPassword = !mailu.revealPassword">
|
||||
{{ mailu.revealPassword ? "hide" : "show" }}
|
||||
<div class="account-column">
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Firefly III</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
firefly.status === 'ready'
|
||||
? 'pill-ok'
|
||||
: firefly.status === 'needs provisioning' || firefly.status === 'login required'
|
||||
? 'pill-warn'
|
||||
: firefly.status === 'unavailable' || firefly.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ firefly.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Personal finance manager for budgets and spending. Sign in with the email below and set the server URL to
|
||||
money.bstein.dev in the Abacus mobile app.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://money.bstein.dev" target="_blank" rel="noreferrer">
|
||||
money.bstein.dev
|
||||
</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Email</span>
|
||||
<span class="v mono">{{ firefly.username || auth.email || auth.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Password updated</span>
|
||||
<span class="v mono">{{ firefly.passwordUpdatedAt || "unknown" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="firefly.resetting" @click="resetFirefly">
|
||||
{{ firefly.resetting ? "Resetting..." : "Reset Firefly password" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mono secret">{{ mailu.revealPassword ? mailu.currentPassword : "••••••••••••••••" }}</div>
|
||||
<div class="hint mono">Use this in your mail client (IMAP/SMTP).</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="hint mono">No app password set yet. Rotate to generate one.</div>
|
||||
|
||||
<div v-if="mailu.newPassword" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono pill-warn">Show once</div>
|
||||
<button class="copy mono" type="button" @click="copy('mailu-new', mailu.newPassword)">
|
||||
copy
|
||||
<span v-if="copied['mailu-new']" class="copied">copied</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mono secret">{{ mailu.newPassword }}</div>
|
||||
<div class="hint mono">Update your mail client password to match.</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mailu.error" class="error-box">
|
||||
<div class="mono">{{ mailu.error }}</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="module-head subhead">
|
||||
<h3>Nextcloud Mail</h3>
|
||||
<span class="pill mono">{{ nextcloudMail.status }}</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Syncs your Nextcloud Mail app with your Mailu mailbox (dedupes accounts and keeps the app password updated).
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">Primary</span>
|
||||
<span class="v mono">{{ nextcloudMail.primaryEmail || mailu.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Accounts</span>
|
||||
<span class="v mono">{{ nextcloudMail.accountCount || "0" }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Synced</span>
|
||||
<span class="v mono">{{ nextcloudMail.syncedAt || "never" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="nextcloudMail.syncing" @click="syncNextcloudMail">
|
||||
{{ nextcloudMail.syncing ? "Syncing..." : "Sync Nextcloud Mail now" }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="nextcloudMail.error" class="error-box">
|
||||
<div class="mono">{{ nextcloudMail.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="account-stack">
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Vaultwarden</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
vaultwarden.status === 'ready' || vaultwarden.status === 'already_present'
|
||||
? 'pill-ok'
|
||||
: vaultwarden.status === 'unavailable' || vaultwarden.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ vaultwarden.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
v-if="vaultwarden.status !== 'ready' && vaultwarden.status !== 'already_present'"
|
||||
class="muted"
|
||||
>
|
||||
Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://vault.bstein.dev" target="_blank" rel="noreferrer">vault.bstein.dev</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Username</span>
|
||||
<span class="v mono">{{ vaultwarden.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Synced</span>
|
||||
<span class="v mono">{{ vaultwarden.syncedAt || "never" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="vaultwarden.status === 'invited'" class="hint mono">
|
||||
Invitation created. Open Vaultwarden and complete setup by choosing a master password.
|
||||
</div>
|
||||
<div v-if="vaultwarden.error" class="error-box">
|
||||
<div class="mono">{{ vaultwarden.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Wger</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
wger.status === 'ready'
|
||||
? 'pill-ok'
|
||||
: wger.status === 'needs provisioning' || wger.status === 'login required'
|
||||
? 'pill-warn'
|
||||
: wger.status === 'unavailable' || wger.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ wger.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Workout + nutrition tracking with the wger mobile app. Sign in with the credentials below and set the server
|
||||
URL to health.bstein.dev in the app.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://health.bstein.dev" target="_blank" rel="noreferrer">health.bstein.dev</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Username</span>
|
||||
<span class="v mono">{{ wger.username || auth.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Password updated</span>
|
||||
<span class="v mono">{{ wger.passwordUpdatedAt || "unknown" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="wger.resetting" @click="resetWger">
|
||||
{{ wger.resetting ? "Resetting..." : "Reset wger password" }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="wger.password" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Password</div>
|
||||
<div class="secret-actions">
|
||||
<button class="copy mono" type="button" @click="copy('wger-password', wger.password)">
|
||||
copy
|
||||
<span v-if="copied['wger-password']" class="copied">copied</span>
|
||||
</button>
|
||||
<button class="copy mono" type="button" @click="wger.revealPassword = !wger.revealPassword">
|
||||
{{ wger.revealPassword ? "hide" : "show" }}
|
||||
</button>
|
||||
<div v-if="firefly.password" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Password</div>
|
||||
<div class="secret-actions">
|
||||
<button class="copy mono" type="button" @click="copy('firefly-password', firefly.password)">
|
||||
copy
|
||||
<span v-if="copied['firefly-password']" class="copied">copied</span>
|
||||
</button>
|
||||
<button class="copy mono" type="button" @click="firefly.revealPassword = !firefly.revealPassword">
|
||||
{{ firefly.revealPassword ? "hide" : "show" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mono secret">{{ firefly.revealPassword ? firefly.password : "••••••••••••••••" }}</div>
|
||||
<div class="hint mono">Use this in Firefly III and the Abacus app.</div>
|
||||
</div>
|
||||
<div v-else class="hint mono">No password available yet. Try resetting or check back later.</div>
|
||||
<div v-if="firefly.error" class="error-box">
|
||||
<div class="mono">{{ firefly.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mono secret">{{ wger.revealPassword ? wger.password : "••••••••••••••••" }}</div>
|
||||
<div class="hint mono">Use this in the wger mobile app and web UI.</div>
|
||||
</div>
|
||||
<div v-else class="hint mono">No password available yet. Try resetting or check back later.</div>
|
||||
<div v-if="wger.error" class="error-box">
|
||||
<div class="mono">{{ wger.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Firefly III</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
firefly.status === 'ready'
|
||||
? 'pill-ok'
|
||||
: firefly.status === 'needs provisioning' || firefly.status === 'login required'
|
||||
? 'pill-warn'
|
||||
: firefly.status === 'unavailable' || firefly.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ firefly.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Personal finance manager for budgets and spending. Sign in with the email below and set the server URL to
|
||||
money.bstein.dev in the Abacus mobile app.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://money.bstein.dev" target="_blank" rel="noreferrer">money.bstein.dev</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Email</span>
|
||||
<span class="v mono">{{ firefly.username || auth.email || auth.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Password updated</span>
|
||||
<span class="v mono">{{ firefly.passwordUpdatedAt || "unknown" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="firefly.resetting" @click="resetFirefly">
|
||||
{{ firefly.resetting ? "Resetting..." : "Reset Firefly password" }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="firefly.password" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Password</div>
|
||||
<div class="secret-actions">
|
||||
<button class="copy mono" type="button" @click="copy('firefly-password', firefly.password)">
|
||||
copy
|
||||
<span v-if="copied['firefly-password']" class="copied">copied</span>
|
||||
</button>
|
||||
<button class="copy mono" type="button" @click="firefly.revealPassword = !firefly.revealPassword">
|
||||
{{ firefly.revealPassword ? "hide" : "show" }}
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Mail</h2>
|
||||
<span class="pill mono">{{ mailu.status }}</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Use a dedicated app password for IMAP/SMTP clients (mobile mail, Thunderbird, Outlook). Rotate it any time.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">IMAP</span>
|
||||
<span class="v mono">{{ mailu.imap }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">SMTP</span>
|
||||
<span class="v mono">{{ mailu.smtp }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Email</span>
|
||||
<span class="v mono">{{ mailu.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="mailu.rotating" @click="rotateMailu">
|
||||
{{ mailu.rotating ? "Rotating..." : "Rotate mail app password" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mono secret">{{ firefly.revealPassword ? firefly.password : "••••••••••••••••" }}</div>
|
||||
<div class="hint mono">Use this in Firefly III and the Abacus app.</div>
|
||||
</div>
|
||||
<div v-else class="hint mono">No password available yet. Try resetting or check back later.</div>
|
||||
<div v-if="firefly.error" class="error-box">
|
||||
<div class="mono">{{ firefly.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Jellyfin</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="jellyfin.syncStatus === 'ok' ? 'pill-ok' : jellyfin.syncStatus === 'degraded' ? 'pill-bad' : ''"
|
||||
>
|
||||
{{
|
||||
jellyfin.syncStatus === "ok"
|
||||
? "in sync"
|
||||
: jellyfin.syncStatus === "degraded"
|
||||
? "out of sync"
|
||||
: jellyfin.status
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Jellyfin authentication is backed by LDAP (Keycloak is the source of truth). Use your Keycloak username and
|
||||
password.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://stream.bstein.dev" target="_blank" rel="noreferrer">stream.bstein.dev</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Username</span>
|
||||
<span class="v mono">{{ jellyfin.username }}</span>
|
||||
<div v-if="mailu.currentPassword" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Current password</div>
|
||||
<div class="secret-actions">
|
||||
<button class="copy mono" type="button" @click="copy('mailu-current', mailu.currentPassword)">
|
||||
copy
|
||||
<span v-if="copied['mailu-current']" class="copied">copied</span>
|
||||
</button>
|
||||
<button class="copy mono" type="button" @click="mailu.revealPassword = !mailu.revealPassword">
|
||||
{{ mailu.revealPassword ? "hide" : "show" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mono secret">{{ mailu.revealPassword ? mailu.currentPassword : "••••••••••••••••" }}</div>
|
||||
<div class="hint mono">Use this in your mail client (IMAP/SMTP).</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="hint mono">No app password set yet. Rotate to generate one.</div>
|
||||
|
||||
<div v-if="mailu.newPassword" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono pill-warn">Show once</div>
|
||||
<button class="copy mono" type="button" @click="copy('mailu-new', mailu.newPassword)">
|
||||
copy
|
||||
<span v-if="copied['mailu-new']" class="copied">copied</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mono secret">{{ mailu.newPassword }}</div>
|
||||
<div class="hint mono">Update your mail client password to match.</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mailu.error" class="error-box">
|
||||
<div class="mono">{{ mailu.error }}</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="module-head subhead">
|
||||
<h3>Nextcloud Mail</h3>
|
||||
<span class="pill mono">{{ nextcloudMail.status }}</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Syncs your Nextcloud Mail app with your Mailu mailbox (dedupes accounts and keeps the app password updated).
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">Primary</span>
|
||||
<span class="v mono">{{ nextcloudMail.primaryEmail || mailu.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Accounts</span>
|
||||
<span class="v mono">{{ nextcloudMail.accountCount || "0" }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Synced</span>
|
||||
<span class="v mono">{{ nextcloudMail.syncedAt || "never" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="nextcloudMail.syncing" @click="syncNextcloudMail">
|
||||
{{ nextcloudMail.syncing ? "Syncing..." : "Sync Nextcloud Mail now" }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="nextcloudMail.error" class="error-box">
|
||||
<div class="mono">{{ nextcloudMail.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="jellyfin.error" class="error-box">
|
||||
<div class="mono">{{ jellyfin.error }}</div>
|
||||
|
||||
<div class="account-stack">
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Vaultwarden</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
vaultwarden.status === 'ready' || vaultwarden.status === 'already_present'
|
||||
? 'pill-ok'
|
||||
: vaultwarden.status === 'unavailable' || vaultwarden.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ vaultwarden.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="vaultwarden.status !== 'ready' && vaultwarden.status !== 'already_present'" class="muted">
|
||||
Password manager for Atlas accounts. Store your Element recovery key here. Signups are admin-provisioned.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://vault.bstein.dev" target="_blank" rel="noreferrer">vault.bstein.dev</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Username</span>
|
||||
<span class="v mono">{{ vaultwarden.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Synced</span>
|
||||
<span class="v mono">{{ vaultwarden.syncedAt || "never" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="vaultwarden.status === 'invited'" class="hint mono">
|
||||
Invitation created. Open Vaultwarden and complete setup by choosing a master password.
|
||||
</div>
|
||||
<div v-if="vaultwarden.error" class="error-box">
|
||||
<div class="mono">{{ vaultwarden.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Wger</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="
|
||||
wger.status === 'ready'
|
||||
? 'pill-ok'
|
||||
: wger.status === 'needs provisioning' || wger.status === 'login required'
|
||||
? 'pill-warn'
|
||||
: wger.status === 'unavailable' || wger.status === 'error'
|
||||
? 'pill-bad'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
{{ wger.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Workout + nutrition tracking with the wger mobile app. Sign in with the credentials below and set the server
|
||||
URL to health.bstein.dev in the app.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://health.bstein.dev" target="_blank" rel="noreferrer">
|
||||
health.bstein.dev
|
||||
</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Username</span>
|
||||
<span class="v mono">{{ wger.username || auth.username }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Password updated</span>
|
||||
<span class="v mono">{{ wger.passwordUpdatedAt || "unknown" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="primary" type="button" :disabled="wger.resetting" @click="resetWger">
|
||||
{{ wger.resetting ? "Resetting..." : "Reset wger password" }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="wger.password" class="secret-box">
|
||||
<div class="secret-head">
|
||||
<div class="pill mono">Password</div>
|
||||
<div class="secret-actions">
|
||||
<button class="copy mono" type="button" @click="copy('wger-password', wger.password)">
|
||||
copy
|
||||
<span v-if="copied['wger-password']" class="copied">copied</span>
|
||||
</button>
|
||||
<button class="copy mono" type="button" @click="wger.revealPassword = !wger.revealPassword">
|
||||
{{ wger.revealPassword ? "hide" : "show" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mono secret">{{ wger.revealPassword ? wger.password : "••••••••••••••••" }}</div>
|
||||
<div class="hint mono">Use this in the wger mobile app and web UI.</div>
|
||||
</div>
|
||||
<div v-else class="hint mono">No password available yet. Try resetting or check back later.</div>
|
||||
<div v-if="wger.error" class="error-box">
|
||||
<div class="mono">{{ wger.error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card module">
|
||||
<div class="module-head">
|
||||
<h2>Jellyfin</h2>
|
||||
<span
|
||||
class="pill mono"
|
||||
:class="jellyfin.syncStatus === 'ok' ? 'pill-ok' : jellyfin.syncStatus === 'degraded' ? 'pill-bad' : ''"
|
||||
>
|
||||
{{
|
||||
jellyfin.syncStatus === "ok"
|
||||
? "in sync"
|
||||
: jellyfin.syncStatus === "degraded"
|
||||
? "out of sync"
|
||||
: jellyfin.status
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Jellyfin authentication is backed by LDAP (Keycloak is the source of truth). Use your Keycloak username and
|
||||
password.
|
||||
</p>
|
||||
<div class="kv">
|
||||
<div class="row">
|
||||
<span class="k mono">URL</span>
|
||||
<a class="v mono link" href="https://stream.bstein.dev" target="_blank" rel="noreferrer">
|
||||
stream.bstein.dev
|
||||
</a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="k mono">Username</span>
|
||||
<span class="v mono">{{ jellyfin.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="jellyfin.error" class="error-box">
|
||||
<div class="mono">{{ jellyfin.error }}</div>
|
||||
</div>
|
||||
<div v-if="jellyfin.syncDetail" class="hint mono jellyfin-detail">{{ jellyfin.syncDetail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="jellyfin.syncDetail" class="hint mono jellyfin-detail">{{ jellyfin.syncDetail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="admin.enabled" class="card module admin">
|
||||
@ -789,19 +795,20 @@ h1 {
|
||||
|
||||
.account-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.account-column,
|
||||
.account-stack {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.account-column .module,
|
||||
.account-stack .module {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
@ -944,6 +951,10 @@ button.primary {
|
||||
.account-stack .module {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.account-column .module {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.admin {
|
||||
|
||||
@ -451,6 +451,49 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mobile-guides">
|
||||
<div class="module-head">
|
||||
<h3>Mobile app guides</h3>
|
||||
<span class="pill mono">step-by-step</span>
|
||||
</div>
|
||||
<p class="muted">
|
||||
Each guide expands with screenshots. Set the server URL before logging in so everything points at Atlas.
|
||||
</p>
|
||||
<div class="guide-grid">
|
||||
<div v-for="guide in mobileGuides" :key="guide.id" class="card module guide-card">
|
||||
<div class="module-head">
|
||||
<h3>{{ guide.title }}</h3>
|
||||
<span class="pill mono">{{ guide.app }}</span>
|
||||
</div>
|
||||
<p class="muted">{{ guide.description }}</p>
|
||||
<div class="kv">
|
||||
<div v-for="field in guide.fields" :key="`${guide.id}-${field.label}`" class="row">
|
||||
<span class="k mono">{{ field.label }}</span>
|
||||
<span class="v mono">
|
||||
<a v-if="field.href" class="guide-link" :href="field.href" target="_blank" rel="noreferrer">
|
||||
{{ field.value }}
|
||||
</a>
|
||||
<span v-else>{{ field.value }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guide-actions">
|
||||
<a class="pill mono guide-link" :href="guide.url" target="_blank" rel="noreferrer">Open</a>
|
||||
</div>
|
||||
<details class="guide-details">
|
||||
<summary class="mono">Photo guide</summary>
|
||||
<div v-if="guideShots[guide.id] && guideShots[guide.id].length" class="guide-images">
|
||||
<figure v-for="(shot, index) in guideShots[guide.id]" :key="shot.url" class="guide-shot">
|
||||
<img :src="shot.url" :alt="`${guide.title} step ${index + 1}`" loading="lazy" />
|
||||
<figcaption v-if="shot.label" class="mono">{{ shot.label }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
<p v-else class="muted">Guide coming soon.</p>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="status === 'ready'" class="ready-box">
|
||||
<h3>You're ready</h3>
|
||||
<p class="muted">
|
||||
@ -537,6 +580,112 @@ const extraSteps = [
|
||||
},
|
||||
];
|
||||
|
||||
// Guide images: drop files into src/assets/onboarding/<guideId>/01-step.png to auto-load and order.
|
||||
const guideShots = buildGuideShots(
|
||||
import.meta.glob("../assets/onboarding/**/*.{png,jpg,jpeg,webp}", { eager: true, as: "url" }),
|
||||
);
|
||||
|
||||
const mobileGuides = [
|
||||
{
|
||||
id: "vaultwarden",
|
||||
title: "Vaultwarden",
|
||||
app: "Bitwarden",
|
||||
description: "Password manager with autofill across devices.",
|
||||
url: "https://vault.bstein.dev",
|
||||
fields: [
|
||||
{ label: "Server", value: "vault.bstein.dev" },
|
||||
{ label: "Login", value: "Atlas email + password" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "elementx",
|
||||
title: "Element X",
|
||||
app: "Matrix chat",
|
||||
description: "Secure chat for Othrys rooms and direct messages.",
|
||||
url: "https://live.bstein.dev",
|
||||
fields: [
|
||||
{ label: "Homeserver", value: "live.bstein.dev" },
|
||||
{ label: "Login", value: "Atlas username + password" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "wger",
|
||||
title: "Wger",
|
||||
app: "Health tracking",
|
||||
description: "Workout + nutrition tracking for Atlas.",
|
||||
url: "https://health.bstein.dev",
|
||||
fields: [
|
||||
{ label: "Server", value: "health.bstein.dev" },
|
||||
{ label: "Login", value: "Use Account page credentials" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "firefly",
|
||||
title: "Firefly III",
|
||||
app: "Abacus",
|
||||
description: "Personal finance manager for budgets and spending.",
|
||||
url: "https://money.bstein.dev",
|
||||
fields: [
|
||||
{ label: "Server", value: "money.bstein.dev" },
|
||||
{ label: "Login", value: "Use Account page credentials" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "nextcloud",
|
||||
title: "Nextcloud",
|
||||
app: "Files + mail",
|
||||
description: "Files, calendar, and mail on the go.",
|
||||
url: "https://cloud.bstein.dev",
|
||||
fields: [
|
||||
{ label: "Server", value: "cloud.bstein.dev" },
|
||||
{ label: "Login", value: "Atlas username + password" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "jellyfin",
|
||||
title: "Jellyfin",
|
||||
app: "Media streaming",
|
||||
description: "Personal media library for Atlas.",
|
||||
url: "https://stream.bstein.dev",
|
||||
fields: [
|
||||
{ label: "Server", value: "stream.bstein.dev" },
|
||||
{ label: "Login", value: "Atlas username + password" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function buildGuideShots(rawShots) {
|
||||
const grouped = {};
|
||||
for (const [path, url] of Object.entries(rawShots)) {
|
||||
const normalized = path.replace(/\\/g, "/");
|
||||
const parts = normalized.split("/assets/onboarding/");
|
||||
if (parts.length < 2) continue;
|
||||
const [guideId, file] = parts[1].split("/");
|
||||
if (!guideId || !file) continue;
|
||||
const order = guideOrder(file);
|
||||
const label = guideLabel(file);
|
||||
if (!grouped[guideId]) grouped[guideId] = [];
|
||||
grouped[guideId].push({ url, order, label, file });
|
||||
}
|
||||
Object.values(grouped).forEach((shots) => {
|
||||
shots.sort((a, b) => (a.order - b.order) || a.file.localeCompare(b.file));
|
||||
});
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function guideOrder(filename) {
|
||||
const prefix = filename.match(/^(\d{1,3})/);
|
||||
if (prefix) return Number(prefix[1]);
|
||||
const step = filename.match(/step[-_]?(\d{1,3})/i);
|
||||
if (step) return Number(step[1]);
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
|
||||
function guideLabel(filename) {
|
||||
const base = filename.replace(/\.(png|jpe?g|webp)$/i, "");
|
||||
return base.replace(/^\d+[-_]?/, "").replace(/[-_]/g, " ").trim();
|
||||
}
|
||||
|
||||
function statusLabel(value) {
|
||||
const key = (value || "").trim();
|
||||
if (key === "pending_email_verification") return "confirm email";
|
||||
@ -1228,6 +1377,73 @@ button.secondary {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mobile-guides {
|
||||
margin-top: 18px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.guide-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.guide-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guide-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.guide-link {
|
||||
color: rgba(125, 208, 255, 0.9);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.guide-details {
|
||||
margin-top: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.guide-details summary {
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.guide-images {
|
||||
margin-top: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.guide-shot {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.guide-shot img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.guide-shot figcaption {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.recovery-verify {
|
||||
flex-direction: column;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user