Compare commits

...
4 Commits
4 changed files with 27 additions and 14 deletions
+19 -7
View File
@@ -36,14 +36,26 @@ const orgPermissions = computed(() => {
}) })
// Get users for a role as sorted array of { uuid, ...user } // Get users for a role as sorted array of { uuid, ...user }
function getNormalizedName(name) {
let cleaned = name.replace(/\([^)]*\)/g, '').trim();
if (cleaned.includes(',')) {
return cleaned.toLowerCase();
} else {
const parts = cleaned.split(/\s+/);
const last = parts.pop();
const first = parts.join(' ');
return `${last}, ${first}`.toLowerCase();
}
}
function roleUsers(roleUuid) { function roleUsers(roleUuid) {
return Object.entries(props.selectedOrg.users) return Object.entries(props.selectedOrg.users)
.filter(([_, u]) => u.role === roleUuid) .filter(([_, u]) => u.role === roleUuid)
.map(([uuid, u]) => ({ uuid, ...u })) .map(([uuid, u]) => ({ uuid, ...u }))
.sort((a, b) => { .sort((a, b) => {
const nameA = a.display_name.toLowerCase() const normA = getNormalizedName(a.display_name);
const nameB = b.display_name.toLowerCase() const normB = getNormalizedName(b.display_name);
return nameA.localeCompare(nameB) return normA.localeCompare(normB);
}) })
} }
@@ -389,7 +401,7 @@ defineExpose({ focusFirstElement })
:title="u.uuid" :title="u.uuid"
> >
<span class="name">{{ u.display_name }}</span> <span class="name">{{ u.display_name }}</span>
<span class="meta">{{ u.last_seen ? new Date(u.last_seen).toLocaleDateString() : '—' }}</span> <span class="meta">{{ u.last_seen ? new Date(u.last_seen).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : '—' }}</span>
</li> </li>
</template> </template>
</draggable> </draggable>
@@ -409,7 +421,7 @@ defineExpose({ focusFirstElement })
.perm-matrix-grid .role-head { display: flex; align-items: flex-end; justify-content: center; } .perm-matrix-grid .role-head { display: flex; align-items: flex-end; justify-content: center; }
.perm-matrix-grid .role-head span { writing-mode: vertical-rl; transform: rotate(180deg); font-size: 0.65rem; } .perm-matrix-grid .role-head span { writing-mode: vertical-rl; transform: rotate(180deg); font-size: 0.65rem; }
.perm-matrix-grid .add-role-head { cursor: pointer; } .perm-matrix-grid .add-role-head { cursor: pointer; }
.roles-grid { display: flex; flex-wrap: wrap; gap: var(--space-lg); margin-top: var(--space-lg); justify-content: flex-start; align-items: stretch; } .roles-grid { display: flex; flex-wrap: wrap; gap: 0; margin-top: var(--space-lg); justify-content: flex-start; align-items: stretch; }
.role-column { flex: 0 0 240px; border-radius: var(--radius-md); padding: var(--space-md); display: flex; flex-direction: column; } .role-column { flex: 0 0 240px; border-radius: var(--radius-md); padding: var(--space-md); display: flex; flex-direction: column; }
.role-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-md); } .role-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-md); }
.role-name { display: flex; align-items: center; gap: var(--space-xs); font-size: 1.1rem; color: var(--color-heading); } .role-name { display: flex; align-items: center; gap: var(--space-xs); font-size: 1.1rem; color: var(--color-heading); }
@@ -418,9 +430,9 @@ defineExpose({ focusFirstElement })
.plus-btn:hover { background: rgba(37, 99, 235, 0.18); } .plus-btn:hover { background: rgba(37, 99, 235, 0.18); }
.user-list-wrapper { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 5.5rem; } .user-list-wrapper { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 5.5rem; }
.user-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--space-xs); flex: 1; } .user-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--space-xs); flex: 1; }
.user-chip { background: var(--color-accent-strong); color: white; border: none; border-radius: var(--radius-md); padding: 0.45rem 0.6rem; display: flex; justify-content: space-between; gap: var(--space-sm); cursor: grab; } .user-chip { background: var(--color-accent-strong); color: var(--color-accent-contrast); border: none; border-radius: var(--radius-md); padding: 0.45rem 0.6rem; display: flex; justify-content: space-between; gap: var(--space-sm); cursor: grab; }
.user-chip:focus { outline: 2px solid var(--color-accent); outline-offset: 1px; } .user-chip:focus { outline: 2px solid var(--color-accent); outline-offset: 1px; }
.user-chip .meta { font-size: 0.7rem; color: rgba(255, 255, 255, 0.8); } .user-chip .meta { font-size: 0.7rem; }
.user-chip.sortable-ghost { opacity: 0.5; } .user-chip.sortable-ghost { opacity: 0.5; }
.user-chip.sortable-chosen { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } .user-chip.sortable-chosen { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); }
.empty-role { position: absolute; inset: 0; border: 1px dashed var(--color-border-strong); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; pointer-events: none; } .empty-role { position: absolute; inset: 0; border: 1px dashed var(--color-border-strong); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; pointer-events: none; }
+3 -3
View File
@@ -9,8 +9,8 @@
--font-sans: "Inter", "Inter var", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif; --font-sans: "Inter", "Inter var", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", sans-serif;
--font-mono: "DM Mono", "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace; --font-mono: "DM Mono", "JetBrains Mono", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--color-canvas: white; --color-canvas: white;
--color-surface: white; --color-surface: #def;
--color-surface-subtle: white; --color-surface-subtle: #bcf;
--color-surface-hover: oklab(0.97 -0.01 -0.02); --color-surface-hover: oklab(0.97 -0.01 -0.02);
--color-dialog: oklab(0.96 -0.01 -0.03); --color-dialog: oklab(0.96 -0.01 -0.03);
--color-border: oklab(0.82 -0.02 -0.06); --color-border: oklab(0.82 -0.02 -0.06);
@@ -21,7 +21,7 @@
--color-link: oklab(0.5 -0.06 -0.17); --color-link: oklab(0.5 -0.06 -0.17);
--color-link-hover: oklab(0.45 -0.06 -0.19); --color-link-hover: oklab(0.45 -0.06 -0.19);
--color-accent: oklab(0.55 -0.06 -0.19); --color-accent: oklab(0.55 -0.06 -0.19);
--color-accent-strong: oklab(0.45 -0.06 -0.19); --color-accent-strong: #46f;
--color-accent-contrast: white; --color-accent-contrast: white;
--color-secondary: oklab(0.55 -0.02 -0.05); --color-secondary: oklab(0.55 -0.02 -0.05);
--color-secondary-strong: oklab(0.45 -0.02 -0.05); --color-secondary-strong: oklab(0.45 -0.02 -0.05);
+3 -3
View File
@@ -34,7 +34,7 @@ _logger = logging.getLogger(__name__)
class ReplayResult(msgspec.Struct, frozen=False): class ReplayResult(msgspec.Struct, frozen=False):
"""Return value of _replay_from_data""" """Return value of _replay_from_data"""
state: dict state: dict = {}
v: int = 0 v: int = 0
ts: datetime | None = None ts: datetime | None = None
snapts: datetime | None = None snapts: datetime | None = None
@@ -50,7 +50,7 @@ class DatabaseError(Exception):
def _replay_from_data(data: bytes, db_path: str) -> ReplayResult: def _replay_from_data(data: bytes, db_path: str) -> ReplayResult:
"""Replay database state from file data, using the last snapshot if available.""" """Replay database state from file data, using the last snapshot if available."""
resolved_path = str(Path(db_path).resolve()) resolved_path = str(Path(db_path).resolve())
result = ReplayResult(state={}) result = ReplayResult()
# Find and apply the last snapshot # Find and apply the last snapshot
snap, start_offset = SnapshotState.load(data) snap, start_offset = SnapshotState.load(data)
@@ -115,7 +115,7 @@ def load_readonly(db_path: str, *, rp_id: str = "localhost") -> DB:
class ChangeRecord(msgspec.Struct, omit_defaults=True, kw_only=True): class ChangeRecord(msgspec.Struct, omit_defaults=True, kw_only=True):
ts: datetime = msgspec.field(default_factory=lambda: datetime.now(UTC)) ts: datetime = msgspec.field(default_factory=lambda: datetime.now(UTC))
a: str # action - describes the operation (e.g., "migrate", "login", "create_user") a: str = "" # action (e.g., "migrate", "login", "create_user")
v: int = 0 # schema version after this change v: int = 0 # schema version after this change
u: str | None = None # user UUID who performed the action (None for system) u: str | None = None # user UUID who performed the action (None for system)
diff: dict diff: dict
+2 -1
View File
@@ -14,6 +14,7 @@ from paskia.db import start_background, stop_background
from paskia.db.background import flush from paskia.db.background import flush
from paskia.db.logging import configure_db_logging from paskia.db.logging import configure_db_logging
from paskia.fastapi import admin, api, auth_host, oid, ws from paskia.fastapi import admin, api, auth_host, oid, ws
from paskia.fastapi.admin.adminapp import adminapp
# Import frontend instance # Import frontend instance
from paskia.fastapi.front import frontend from paskia.fastapi.front import frontend
@@ -162,7 +163,7 @@ async def admin_root_redirect():
@app.get("/admin/", include_in_schema=False) @app.get("/admin/", include_in_schema=False)
@app.get("/auth/admin/", include_in_schema=False) @app.get("/auth/admin/", include_in_schema=False)
async def admin_root(request: Request, auth=AUTH_COOKIE): async def admin_root(request: Request, auth=AUTH_COOKIE):
return await admin.adminapp(request, auth) # Delegated to admin app return await adminapp(request, auth) # Delegated to admin app
@app.get("/auth/examples/", include_in_schema=False) @app.get("/auth/examples/", include_in_schema=False)