diff --git a/VERSION.json b/VERSION.json
index 9be816ca..d400e03c 100644
--- a/VERSION.json
+++ b/VERSION.json
@@ -1,8 +1,9 @@
{
- "version": "1.2.3",
- "last_build": "2026-04-11-1131",
- "commit": "a5e2f",
+ "version": "1.2.4",
+ "last_build": "2026-04-11-1146",
+ "commit": "b6e3f",
"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.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",
diff --git a/backend/routers/users.py b/backend/routers/users.py
index 78096b4b..11deeb28 100644
--- a/backend/routers/users.py
+++ b/backend/routers/users.py
@@ -152,18 +152,26 @@ def login(form_data: schemas.UserLogin, db: Session = Depends(get_db)):
ldap_role = authenticate_ldap(form_data.username, form_data.password)
if ldap_role:
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 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.commit()
db.refresh(user)
else:
- # Update role if it changed in LDAP
- if user.role != ldap_role:
- user.role = ldap_role
- db.commit()
- db.refresh(user)
+ # Update role if it changed in LDAP and refresh cached hash
+ user.role = ldap_role
+ user.hashed_password = new_hash
+ db.commit()
+ db.refresh(user)
else:
raise HTTPException(status_code=400, detail="Invalid username or password, or insufficient permissions")
diff --git a/dev_docs/ARCHIVE_LOGS.md b/dev_docs/ARCHIVE_LOGS.md
index aaa1dc37..7c4ef1f0 100644
--- a/dev_docs/ARCHIVE_LOGS.md
+++ b/dev_docs/ARCHIVE_LOGS.md
@@ -1,12 +1,15 @@
-### [2026-04-11 11:30] v1.2.3: System-Wide UI Homogenization
-**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.
+### [2026-04-11 11:45] v1.2.4: Offline Auth & UX Polish
+**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:**
-- `frontend/app/inventory/page.tsx`
-- `frontend/app/logs/page.tsx`
-- `frontend/components/Scanner.tsx`
-- `frontend/components/IdentityCheckOverlay.tsx`
+- `backend/routers/users.py` (LDAP offline cache)
+- `frontend/app/admin/page.tsx` (Password/Dropdown UI)
+- `frontend/app/login/page.tsx` (Password UI)
+- `frontend/app/inventory/page.tsx` (Dropdown UI)
+- `frontend/app/page.tsx` (Dropdown UI)
+- `frontend/components/IdentityCheckOverlay.tsx` (Password UI)
- `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
diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx
index e9179ded..893d61f0 100644
--- a/frontend/app/admin/page.tsx
+++ b/frontend/app/admin/page.tsx
@@ -193,7 +193,7 @@ export default function AdminPage() {
-
Local Users (Manual)
+
Local Users
@@ -250,7 +250,7 @@ export default function AdminPage() {
-
Enterprise (LDAP) Users
+
Enterprise Users (LDAP)
@@ -603,20 +603,23 @@ export default function AdminPage() {
placeholder="••••••••"
value={editUserForm.password}
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"
/>
-
+
+
+
+
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index a5b624ff..49d4e384 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -568,16 +568,19 @@ export default function Home() {
-
+
+
+
+
diff --git a/frontend/components/IdentityCheckOverlay.tsx b/frontend/components/IdentityCheckOverlay.tsx
index c0ab7ee7..74191510 100644
--- a/frontend/components/IdentityCheckOverlay.tsx
+++ b/frontend/components/IdentityCheckOverlay.tsx
@@ -137,7 +137,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
ref={enterprisePassRef}
type="password"
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"
/>
@@ -174,7 +174,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
type="password"
autoFocus
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"
/>