5f72264b06
- Introduced a new section for managing organizations, including creating organizations and adding users to them. - Added a password reset modal and functionality to request a password reset link. - Updated the settings page to include personal settings for changing passwords and admin settings for configuring registration options. - Enhanced the app detail view with tabs for releases, insights, collaborators, tracks, and settings. - Improved user management with admin capabilities to verify user emails and change user roles. - Updated navigation and UI elements to accommodate new features and improve user experience.
407 lines
20 KiB
HTML
407 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Shorebird Self-Hosted</title>
|
|
<link rel="stylesheet" href="/web/css/style.css">
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%230891b2'/><text x='50%25' y='55%25' text-anchor='middle' dy='.1em' font-size='20' fill='white'>🐦</text></svg>">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ============================================================
|
|
LOGIN PAGE
|
|
============================================================ -->
|
|
<div id="loginPage" class="login-container">
|
|
<div class="login-card">
|
|
<div class="logo-icon">🐦</div>
|
|
<h1>Shorebird</h1>
|
|
<p class="subtitle">Self-Hosted Dashboard</p>
|
|
<div id="loginError" class="login-error"></div>
|
|
<form id="loginForm">
|
|
<div class="form-group">
|
|
<label for="loginEmail">Email</label>
|
|
<input type="email" id="loginEmail" placeholder="admin@example.com" required autofocus>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="loginPassword">Password</label>
|
|
<input type="password" id="loginPassword" placeholder="Enter password" required>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;padding:12px;">
|
|
Sign In
|
|
</button>
|
|
</form>
|
|
<a href="/auth/sso/login?mode=web" id="ssoLoginBtn" class="btn btn-outline" style="display:none;width:100%;justify-content:center;margin-top:12px;">Sign in with Casdoor</a>
|
|
<p style="text-align:center;margin-top:16px;font-size:13px;color:var(--text-muted);">
|
|
<a href="#" id="showPasswordReset" style="color:var(--primary);">Reset password</a>
|
|
<span id="registerPrompt"> · No account? <a href="#" id="showRegister" style="color:var(--primary);">Register</a></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Registration modal on login page -->
|
|
<div id="registerOverlay" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3>Create Account</h3>
|
|
<form id="registerForm">
|
|
<div class="form-group">
|
|
<label for="regName">Name</label>
|
|
<input type="text" id="regName" placeholder="Your name" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="regEmail">Email</label>
|
|
<input type="email" id="regEmail" placeholder="you@example.com" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="regPassword">Password</label>
|
|
<input type="password" id="regPassword" placeholder="Min 6 characters" required minlength="6">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-outline" onclick="closeRegister()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Register & Sign In</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================
|
|
APP SHELL (hidden until authenticated)
|
|
============================================================ -->
|
|
<div id="appContainer" class="app-container">
|
|
|
|
<!-- Sidebar -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo">🐦</div>
|
|
<h2>Shorebird</h2>
|
|
</div>
|
|
<nav class="sidebar-nav" id="sidebarNav">
|
|
<a href="#dashboard" class="active" data-page="dashboard">
|
|
<span class="nav-icon">📊</span> Dashboard
|
|
</a>
|
|
<a href="#apps" data-page="apps">
|
|
<span class="nav-icon">📱</span> Apps
|
|
</a>
|
|
<a href="#users" data-page="users">
|
|
<span class="nav-icon">👥</span> Users
|
|
</a>
|
|
<a href="#organizations" data-page="organizations">
|
|
<span class="nav-icon">🏢</span> Organizations
|
|
</a>
|
|
<a href="#settings" data-page="settings">
|
|
<span class="nav-icon">⚙️</span> Settings
|
|
</a>
|
|
</nav>
|
|
<div class="sidebar-footer" id="sidebarFooter">
|
|
Shorebird v1.0 · Self-Hosted
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Content Area -->
|
|
<main class="main-content">
|
|
<!-- Top Bar -->
|
|
<div class="top-bar">
|
|
<h2 id="pageTitle">Dashboard</h2>
|
|
<div class="user-info">
|
|
<span id="currentUserEmail">—</span>
|
|
<button class="btn-sm" id="logoutBtn" title="Sign out">Sign Out</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container -->
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
|
|
<!-- ================================================
|
|
DASHBOARD PAGE
|
|
================================================ -->
|
|
<section id="page-dashboard" class="page-section active">
|
|
<div class="stats-grid" id="statsGrid">
|
|
<div class="stat-card primary"><div class="stat-value" id="statApps">—</div><div class="stat-label">Apps</div></div>
|
|
<div class="stat-card success"><div class="stat-value" id="statPatches">—</div><div class="stat-label">Patches</div></div>
|
|
<div class="stat-card warning"><div class="stat-value" id="statUsers">—</div><div class="stat-label">Users</div></div>
|
|
<div class="stat-card"><div class="stat-value" id="statOrganizations">—</div><div class="stat-label">Organizations</div></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header"><h3>Recent Apps</h3></div>
|
|
<div class="card-body" id="recentAppsTable"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================
|
|
APPS PAGE
|
|
================================================ -->
|
|
<section id="page-apps" class="page-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>All Applications</h3>
|
|
<button class="btn btn-primary" onclick="openCreateAppModal()">+ New App</button>
|
|
</div>
|
|
<div class="card-body" id="appsTable"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================
|
|
USERS PAGE
|
|
================================================ -->
|
|
<section id="page-users" class="page-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>All Users</h3>
|
|
<button class="btn btn-primary" onclick="openCreateUserModal()">+ New User</button>
|
|
</div>
|
|
<div class="card-body" id="usersTable"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="page-organizations" class="page-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3>Organizations</h3>
|
|
<button class="btn btn-primary" onclick="openCreateOrgModal()">+ New Organization</button>
|
|
</div>
|
|
<div class="card-body" id="organizationsTable"></div>
|
|
</div>
|
|
<div class="card" style="margin-top:20px;">
|
|
<div class="card-header"><h3>Organization Members</h3></div>
|
|
<div class="card-body" id="organizationMembersTable"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="page-app-detail" class="page-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 id="appDetailTitle">App</h3>
|
|
<button class="btn btn-outline" onclick="navigate('apps')">Back</button>
|
|
</div>
|
|
<div class="card-body" style="padding:24px;">
|
|
<div class="tab-bar">
|
|
<button class="active" data-app-tab="releases" onclick="showAppTab('releases')">Releases</button>
|
|
<button data-app-tab="insights" onclick="showAppTab('insights')">Insights</button>
|
|
<button data-app-tab="collaborators" onclick="showAppTab('collaborators')">Collaborators</button>
|
|
<button data-app-tab="tracks" onclick="showAppTab('tracks')">Tracks</button>
|
|
<button data-app-tab="settings" onclick="showAppTab('settings')">Settings</button>
|
|
</div>
|
|
<div id="appTab-releases" class="app-tab"></div>
|
|
<div id="appTab-insights" class="app-tab" style="display:none;"></div>
|
|
<div id="appTab-collaborators" class="app-tab" style="display:none;"></div>
|
|
<div id="appTab-tracks" class="app-tab" style="display:none;"></div>
|
|
<div id="appTab-settings" class="app-tab" style="display:none;"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="page-release-detail" class="page-section">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 id="releaseDetailTitle">Release</h3>
|
|
<button class="btn btn-outline" onclick="openAppPage(state.currentApp?.app_id)">Back</button>
|
|
</div>
|
|
<div class="card-body" style="padding:24px;">
|
|
<div class="tab-bar">
|
|
<button class="active" data-release-tab="overview" onclick="showReleaseTab('overview')">Overview</button>
|
|
<button data-release-tab="insights" onclick="showReleaseTab('insights')">Insights</button>
|
|
<button data-release-tab="artifacts" onclick="showReleaseTab('artifacts')">Artifacts</button>
|
|
<button data-release-tab="settings" onclick="showReleaseTab('settings')">Settings</button>
|
|
</div>
|
|
<div id="releaseTab-overview" class="release-tab"></div>
|
|
<div id="releaseTab-insights" class="release-tab" style="display:none;"></div>
|
|
<div id="releaseTab-artifacts" class="release-tab" style="display:none;"></div>
|
|
<div id="releaseTab-settings" class="release-tab" style="display:none;"></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ================================================
|
|
SETTINGS PAGE
|
|
================================================ -->
|
|
<section id="page-settings" class="page-section">
|
|
<div class="card">
|
|
<div class="card-header"><h3>Personal Settings</h3></div>
|
|
<div class="card-body" style="padding:24px;">
|
|
<form id="changePasswordForm" style="max-width:520px;">
|
|
<div class="form-group">
|
|
<label for="currentPassword">Current Password</label>
|
|
<input type="password" id="currentPassword" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newPassword">New Password</label>
|
|
<input type="password" id="newPassword" required minlength="6">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header"><h3>Server Configuration</h3></div>
|
|
<div class="card-body" style="padding:24px;">
|
|
<div class="info-grid" id="serverInfo"></div>
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-top:20px;">
|
|
<div class="card-header"><h3>Admin Settings</h3></div>
|
|
<div class="card-body" style="padding:24px;">
|
|
<form id="adminSettingsForm" style="max-width:720px;">
|
|
<div class="form-group"><label><input type="checkbox" id="settingRegistrationEnabled" style="width:auto;"> Enable password registration</label></div>
|
|
<div class="form-group"><label><input type="checkbox" id="settingSsoRegistrationEnabled" style="width:auto;"> Enable SSO registration</label></div>
|
|
<div class="form-group"><label><input type="checkbox" id="settingSsoOnlyRegistration" style="width:auto;"> SSO-only registration</label></div>
|
|
<div class="form-group"><label for="casdoorEndpoint">Casdoor endpoint</label><input type="url" id="casdoorEndpoint" placeholder="https://casdoor.example.com"></div>
|
|
<div class="form-group"><label for="casdoorClientId">Casdoor app ID</label><input type="text" id="casdoorClientId"></div>
|
|
<div class="form-group"><label for="casdoorClientSecret">Casdoor app secret</label><input type="password" id="casdoorClientSecret" placeholder="Leave blank to keep current secret"></div>
|
|
<div class="form-group"><label for="casdoorOrganization">Casdoor organization</label><input type="text" id="casdoorOrganization" placeholder="built-in"></div>
|
|
<button type="submit" class="btn btn-primary">Save Settings</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-top:20px;">
|
|
<div class="card-header"><h3>API Token</h3></div>
|
|
<div class="card-body" style="padding:24px;">
|
|
<p style="font-size:14px;color:var(--text-secondary);margin-bottom:12px;">
|
|
Use this token to authenticate the Shorebird CLI:
|
|
</p>
|
|
<div class="form-group">
|
|
<input type="text" id="apiTokenDisplay" readonly
|
|
style="font-family:var(--mono);font-size:13px;cursor:pointer;"
|
|
onclick="this.select();document.execCommand('copy')"
|
|
title="Click to copy">
|
|
</div>
|
|
<p class="form-hint">Click the token to copy. Set as <code>SHOREBIRD_TOKEN</code> env var.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- ============================================================
|
|
MODALS
|
|
============================================================ -->
|
|
|
|
<!-- Create App Modal -->
|
|
<div id="createAppOverlay" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3>Create New App</h3>
|
|
<form id="createAppForm">
|
|
<div class="form-group">
|
|
<label for="appDisplayName">Display Name</label>
|
|
<input type="text" id="appDisplayName" placeholder="My Flutter App" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="appOrganizationId">Organization</label>
|
|
<select id="appOrganizationId"></select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-outline" onclick="closeModal('createAppOverlay')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create App</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div id="createUserOverlay" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3>Create New User</h3>
|
|
<form id="createUserForm">
|
|
<div class="form-group">
|
|
<label for="newUserName">Name</label>
|
|
<input type="text" id="newUserName" placeholder="Full name" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newUserEmail">Email</label>
|
|
<input type="email" id="newUserEmail" placeholder="user@example.com" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newUserPassword">Password</label>
|
|
<input type="password" id="newUserPassword" placeholder="Min 6 characters" required minlength="6">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-outline" onclick="closeModal('createUserOverlay')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirm Delete Modal -->
|
|
<div id="confirmModal" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3 id="confirmTitle">Confirm</h3>
|
|
<p id="confirmMessage" style="font-size:14px;color:var(--text-secondary);"></p>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-outline" onclick="closeModal('confirmModal')">Cancel</button>
|
|
<button class="btn btn-danger" id="confirmBtn" onclick="executeConfirm()">Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="passwordResetOverlay" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3>Reset Password</h3>
|
|
<form id="passwordResetForm">
|
|
<div class="form-group">
|
|
<label for="resetEmail">Email</label>
|
|
<input type="email" id="resetEmail" placeholder="you@example.com" required>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-outline" onclick="closeModal('passwordResetOverlay')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Send Reset Link</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="forcePasswordOverlay" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3>Change Default Password</h3>
|
|
<form id="forcePasswordForm">
|
|
<div class="form-group">
|
|
<label for="forceNewPassword">New Password</label>
|
|
<input type="password" id="forceNewPassword" required minlength="6">
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="submit" class="btn btn-primary">Update Password</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="createOrgOverlay" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3>Create Organization</h3>
|
|
<form id="createOrgForm">
|
|
<div class="form-group">
|
|
<label for="orgName">Name</label>
|
|
<input type="text" id="orgName" placeholder="Team name" required>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-outline" onclick="closeModal('createOrgOverlay')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="addOrgUserOverlay" class="modal-overlay">
|
|
<div class="modal">
|
|
<h3>Add Organization User</h3>
|
|
<form id="addOrgUserForm">
|
|
<input type="hidden" id="orgUserOrgId">
|
|
<div class="form-group">
|
|
<label for="orgUserEmail">Email</label>
|
|
<input type="email" id="orgUserEmail" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="orgUserRole">Role</label>
|
|
<select id="orgUserRole"><option value="member">Member</option><option value="admin">Admin</option></select>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button type="button" class="btn btn-outline" onclick="closeModal('addOrgUserOverlay')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Add User</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/web/js/app.js"></script>
|
|
</body>
|
|
</html>
|