feat: offline ldap support and ui polish v1.2.4
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2.3",
|
"version": "1.2.4",
|
||||||
"last_build": "2026-04-11-1131",
|
"last_build": "2026-04-11-1146",
|
||||||
"commit": "a5e2f",
|
"commit": "b6e3f",
|
||||||
"changelog": [
|
"changelog": [
|
||||||
|
"v1.2.4: Offline LDAP auth support and UI affordance improvements (Dropdown chevrons, soft password dots)",
|
||||||
"v1.2.3: System-wide UI consistency (standardized font sizes, removed uppercase)",
|
"v1.2.3: System-wide UI consistency (standardized font sizes, removed uppercase)",
|
||||||
"v1.2.2: UI Readability refactor (Removed uppercase/tracking, increased font sizes)",
|
"v1.2.2: UI Readability refactor (Removed uppercase/tracking, increased font sizes)",
|
||||||
"v1.2.1: Git path persistence, master/dev/vX branching, and UI case-sensitivity fix",
|
"v1.2.1: Git path persistence, master/dev/vX branching, and UI case-sensitivity fix",
|
||||||
|
|||||||
@@ -152,16 +152,24 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)):
|
|||||||
ldap_role = authenticate_ldap(form_data.username, form_data.password)
|
ldap_role = authenticate_ldap(form_data.username, form_data.password)
|
||||||
if ldap_role:
|
if ldap_role:
|
||||||
authenticated = True
|
authenticated = True
|
||||||
|
# Cache hash for offline support
|
||||||
|
new_hash = get_password_hash(form_data.password)
|
||||||
|
|
||||||
# If user doesn't exist locally, create a stub for role management
|
# If user doesn't exist locally, create a stub for role management
|
||||||
if not user:
|
if not user:
|
||||||
user = models.User(username=form_data.username, role=ldap_role, origin="ldap")
|
user = models.User(
|
||||||
|
username=form_data.username,
|
||||||
|
role=ldap_role,
|
||||||
|
origin="ldap",
|
||||||
|
hashed_password=new_hash
|
||||||
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
else:
|
else:
|
||||||
# Update role if it changed in LDAP
|
# Update role if it changed in LDAP and refresh cached hash
|
||||||
if user.role != ldap_role:
|
|
||||||
user.role = ldap_role
|
user.role = ldap_role
|
||||||
|
user.hashed_password = new_hash
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
### [2026-04-11 11:30] v1.2.3: System-Wide UI Homogenization
|
### [2026-04-11 11:45] v1.2.4: Offline Auth & UX Polish
|
||||||
**Purpose:** Completed UI refactoring for all secondary pages (`Inventory`, `Logs`, `Scanner`, `IdentityCheckOverlay`). Standardized font sizes to `xs`/`sm`, removed legacy `uppercase` styling, and fixed Title Case for buttons. Handed over a fully consistent high-density UI.
|
**Purpose:** Implemented local password hash caching for LDAP users to allow login in no-network environments (basements/subsoils). Refined UI by softening password field dots and adding visual indicators (chevrons) to all dropdowns for better affordance.
|
||||||
**Modified Files:**
|
**Modified Files:**
|
||||||
- `frontend/app/inventory/page.tsx`
|
- `backend/routers/users.py` (LDAP offline cache)
|
||||||
- `frontend/app/logs/page.tsx`
|
- `frontend/app/admin/page.tsx` (Password/Dropdown UI)
|
||||||
- `frontend/components/Scanner.tsx`
|
- `frontend/app/login/page.tsx` (Password UI)
|
||||||
- `frontend/components/IdentityCheckOverlay.tsx`
|
- `frontend/app/inventory/page.tsx` (Dropdown UI)
|
||||||
|
- `frontend/app/page.tsx` (Dropdown UI)
|
||||||
|
- `frontend/components/IdentityCheckOverlay.tsx` (Password UI)
|
||||||
- `VERSION.json`
|
- `VERSION.json`
|
||||||
- `dev_docs/ARCHIVE_LOGS.md`
|
|
||||||
|
### [2026-04-11 11:30] v1.2.3: System-Wide UI Homogenization
|
||||||
|
|
||||||
### [2026-04-11 11:21] v1.2.2: UI Readability & Density Optimization
|
### [2026-04-11 11:21] v1.2.2: UI Readability & Density Optimization
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export default function AdminPage() {
|
|||||||
<div className="flex items-center justify-between px-4">
|
<div className="flex items-center justify-between px-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
|
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
|
||||||
<h3 className="text-sm font-black text-white">Local Users (Manual)</h3>
|
<h3 className="text-sm font-black text-white">Local Users</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-900/40 border border-slate-800/50 rounded-3xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
|
<div className="bg-slate-900/40 border border-slate-800/50 rounded-3xl overflow-hidden shadow-xl divide-y divide-slate-800/50">
|
||||||
@@ -250,7 +250,7 @@ export default function AdminPage() {
|
|||||||
<div className="flex items-center justify-between px-4">
|
<div className="flex items-center justify-between px-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-indigo-500" />
|
<div className="w-1.5 h-1.5 rounded-full bg-indigo-500" />
|
||||||
<h3 className="text-sm font-black text-white">Enterprise (LDAP) Users</h3>
|
<h3 className="text-sm font-black text-white">Enterprise Users (LDAP)</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-3xl overflow-hidden shadow-xl divide-y divide-indigo-500/10">
|
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-3xl overflow-hidden shadow-xl divide-y divide-indigo-500/10">
|
||||||
@@ -603,12 +603,13 @@ export default function AdminPage() {
|
|||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={editUserForm.password}
|
value={editUserForm.password}
|
||||||
onChange={(e) => setEditUserForm({ ...editUserForm, password: e.target.value })}
|
onChange={(e) => setEditUserForm({ ...editUserForm, password: e.target.value })}
|
||||||
className="w-full bg-slate-950 border border-slate-800 focus:border-primary rounded-2xl py-4 px-5 text-white outline-none transition-all"
|
className="w-full bg-slate-950 border border-slate-800 focus:border-primary rounded-2xl py-4 px-5 text-white/50 focus:text-white outline-none transition-all placeholder:text-slate-800"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-black text-slate-500 px-1">Access Role</label>
|
<label className="text-xs font-black text-slate-500 px-1">Access Role</label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
<select
|
<select
|
||||||
value={editUserForm.role}
|
value={editUserForm.role}
|
||||||
onChange={(e) => setEditUserForm({ ...editUserForm, role: e.target.value })}
|
onChange={(e) => setEditUserForm({ ...editUserForm, role: e.target.value })}
|
||||||
@@ -617,6 +618,8 @@ export default function AdminPage() {
|
|||||||
<option value="user">USER (Standard)</option>
|
<option value="user">USER (Standard)</option>
|
||||||
<option value="admin">ADMIN (Root Access)</option>
|
<option value="admin">ADMIN (Root Access)</option>
|
||||||
</select>
|
</select>
|
||||||
|
<ChevronDown size={20} className="absolute right-5 text-slate-500 pointer-events-none" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -379,16 +379,19 @@ export default function InventoryPage() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-black text-slate-500 ml-1">Category</label>
|
<label className="text-xs font-black text-slate-500 ml-1">Category</label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
<select
|
<select
|
||||||
value={editedItem.category || ''}
|
value={editedItem.category || ''}
|
||||||
onChange={e => setEditedItem({...editedItem, category: e.target.value})}
|
onChange={e => setEditedItem({...editedItem, category: e.target.value})}
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100 appearance-none"
|
||||||
>
|
>
|
||||||
<option value="">Select Category</option>
|
<option value="">Select Category</option>
|
||||||
{categoriesList.map(c => (
|
{categoriesList.map(c => (
|
||||||
<option key={c.id} value={c.name}>{c.name}</option>
|
<option key={c.id} value={c.name}>{c.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
<ChevronDown size={16} className="absolute right-4 text-slate-500 pointer-events-none" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-black text-slate-500 ml-1">Item Type</label>
|
<label className="text-xs font-black text-slate-500 ml-1">Item Type</label>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function LoginPage() {
|
|||||||
ref={enterprisePassRef}
|
ref={enterprisePassRef}
|
||||||
type="password"
|
type="password"
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||||
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white/50 focus:text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
||||||
placeholder="Enter password"
|
placeholder="Enter password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,7 +191,7 @@ export default function LoginPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
autoFocus
|
autoFocus
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||||
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white/50 focus:text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
||||||
placeholder="Enter password"
|
placeholder="Enter password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -568,16 +568,19 @@ export default function Home() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-black text-slate-500 ml-1">Category</label>
|
<label className="text-xs font-black text-slate-500 ml-1">Category</label>
|
||||||
|
<div className="relative flex items-center">
|
||||||
<select
|
<select
|
||||||
value={editedItem.category || ''}
|
value={editedItem.category || ''}
|
||||||
onChange={e => setEditedItem({...editedItem, category: e.target.value})}
|
onChange={e => setEditedItem({...editedItem, category: e.target.value})}
|
||||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100 appearance-none"
|
||||||
>
|
>
|
||||||
<option value="">Select Category</option>
|
<option value="">Select Category</option>
|
||||||
{categories.map(c => (
|
{categories.map(c => (
|
||||||
<option key={c.id} value={c.name}>{c.name}</option>
|
<option key={c.id} value={c.name}>{c.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
<ChevronDown size={16} className="absolute right-4 text-slate-500 pointer-events-none" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs font-black text-slate-500 ml-1">Item Type (e.g. SFP, Patch Cord)</label>
|
<label className="text-xs font-black text-slate-500 ml-1">Item Type (e.g. SFP, Patch Cord)</label>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
|
|||||||
ref={enterprisePassRef}
|
ref={enterprisePassRef}
|
||||||
type="password"
|
type="password"
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||||
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white/50 focus:text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
||||||
placeholder="Enter password"
|
placeholder="Enter password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -174,7 +174,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
|
|||||||
type="password"
|
type="password"
|
||||||
autoFocus
|
autoFocus
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||||
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white/50 focus:text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
|
||||||
placeholder="Enter password"
|
placeholder="Enter password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user