Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1b2bcf76c | ||
|
|
1806bcab5c | ||
|
|
be177cbafc |
@@ -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; }
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user