feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,38 @@
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Server Badge (US-026) */
|
||||
.server-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs, 4px);
|
||||
padding: var(--space-xs, 4px) var(--space-sm, 8px);
|
||||
background: var(--primary-100, #dbeafe);
|
||||
color: var(--primary-700, #1d4ed8);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
font-size: 0.75rem;
|
||||
font-weight: var(--font-semibold, 600);
|
||||
}
|
||||
|
||||
.server-badge i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Dark mode support for server badge */
|
||||
[data-theme="dark"] .server-badge {
|
||||
background: var(--primary-900, #1e3a8a);
|
||||
color: var(--primary-200, #bfdbfe);
|
||||
}
|
||||
|
||||
/* Gradient header server badge */
|
||||
.header-container--gradient .server-badge {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Server Switch - ServerSelector component has its own scoped styles */
|
||||
/* Only header-specific overrides here if needed */
|
||||
|
||||
/* Theme Toggle Button */
|
||||
.theme-toggle-btn {
|
||||
display: flex;
|
||||
@@ -202,3 +234,129 @@
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Server Switch Password Modal (US-009) */
|
||||
.server-switch-modal .server-switch-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md, 16px);
|
||||
}
|
||||
|
||||
.server-switch-modal .form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs, 4px);
|
||||
}
|
||||
|
||||
.server-switch-modal .switch-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm, 8px);
|
||||
padding: var(--space-sm, 8px) var(--space-md, 12px);
|
||||
background: var(--red-50, #fef2f2);
|
||||
border: 1px solid var(--red-200, #fecaca);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
color: var(--red-700, #b91c1c);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.server-switch-modal .switch-error i {
|
||||
color: var(--red-500, #ef4444);
|
||||
}
|
||||
|
||||
/* Dark mode support for modal */
|
||||
[data-theme="dark"] .server-switch-modal .switch-error {
|
||||
background: var(--red-900, #7f1d1d);
|
||||
border-color: var(--red-700, #b91c1c);
|
||||
color: var(--red-200, #fecaca);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .server-switch-modal .switch-error i {
|
||||
color: var(--red-400, #f87171);
|
||||
}
|
||||
|
||||
/* ===== Server Switch Password Modal Dialog ===== */
|
||||
.server-switch-modal .p-dialog {
|
||||
border-radius: var(--radius-lg) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
background: var(--surface-card) !important;
|
||||
border: 1px solid var(--surface-border) !important;
|
||||
min-width: 320px !important;
|
||||
}
|
||||
|
||||
.server-switch-modal .p-dialog-header {
|
||||
background: var(--surface-card) !important;
|
||||
border-bottom: 1px solid var(--surface-border) !important;
|
||||
padding: var(--space-md) var(--space-lg) !important;
|
||||
}
|
||||
|
||||
.server-switch-modal .p-dialog-content {
|
||||
background: var(--surface-card) !important;
|
||||
padding: var(--space-lg) !important;
|
||||
}
|
||||
|
||||
.server-switch-modal .p-dialog-footer {
|
||||
background: var(--surface-card) !important;
|
||||
border-top: 1px solid var(--surface-border) !important;
|
||||
padding: var(--space-md) var(--space-lg) !important;
|
||||
}
|
||||
|
||||
/* Dark mode support for modal dialog */
|
||||
[data-theme="dark"] .server-switch-modal .p-dialog {
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4) !important;
|
||||
background: var(--surface-card) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .server-switch-modal .p-dialog-header {
|
||||
background: var(--surface-card) !important;
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .server-switch-modal .p-dialog-header .p-dialog-title {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .server-switch-modal .p-dialog-content {
|
||||
background: var(--surface-card) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .server-switch-modal .p-dialog-footer {
|
||||
background: var(--surface-card) !important;
|
||||
}
|
||||
|
||||
/* Dark mode support for Password input inside modal */
|
||||
[data-theme="dark"] .server-switch-modal .p-password .p-inputtext {
|
||||
background: var(--surface-overlay, #374151) !important;
|
||||
color: var(--text-color, #f9fafb) !important;
|
||||
border-color: var(--surface-border, #4b5563) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .server-switch-modal .p-password .p-inputtext::placeholder {
|
||||
color: var(--text-color-secondary, #9ca3af) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .server-switch-modal .p-password .p-inputtext:focus {
|
||||
border-color: var(--primary-400, #60a5fa) !important;
|
||||
}
|
||||
|
||||
/* Password toggle icon dark mode */
|
||||
[data-theme="dark"] .server-switch-modal .p-password-toggle-icon {
|
||||
color: var(--text-color-secondary, #9ca3af) !important;
|
||||
}
|
||||
|
||||
/* System preference dark mode support for Password input */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .server-switch-modal .p-password .p-inputtext {
|
||||
background: var(--surface-overlay, #374151) !important;
|
||||
color: var(--text-color, #f9fafb) !important;
|
||||
border-color: var(--surface-border, #4b5563) !important;
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .server-switch-modal .p-password .p-inputtext::placeholder {
|
||||
color: var(--text-color-secondary, #9ca3af) !important;
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .server-switch-modal .p-password-toggle-icon {
|
||||
color: var(--text-color-secondary, #9ca3af) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +89,23 @@
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
padding: 1rem 2rem;
|
||||
background-color: var(--surface-50);
|
||||
border-top: 1px solid var(--surface-200);
|
||||
background-color: var(--surface-ground);
|
||||
border-top: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.login-footer small {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
/* Dark mode support */
|
||||
[data-theme="dark"] .login-footer {
|
||||
background-color: var(--surface-ground);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .login-error-message {
|
||||
background-color: rgba(239, 68, 68, 0.15);
|
||||
color: var(--red-300);
|
||||
border-color: var(--red-800);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
|
||||
Reference in New Issue
Block a user