first push
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
type: session
|
||||
date: 2026-05-27
|
||||
project: cpas
|
||||
tags:
|
||||
- cpas
|
||||
- authentication
|
||||
- docker
|
||||
- scrypt
|
||||
- sessions
|
||||
- mpm
|
||||
---
|
||||
|
||||
# [[CPAS]] — Authentication, user management, and docs
|
||||
|
||||
## Summary
|
||||
|
||||
Added a login popup, env-bootstrapped admin account, and admin-managed user CRUD to the CPAS Violation Tracker. Replaced the long-standing `TODO [CRITICAL #1]` (no auth on any route). Updated in-app docs and the Unraid install guide.
|
||||
|
||||
## What shipped
|
||||
|
||||
- **Backend** (`auth.js`, `db/database.js`, `server.js`)
|
||||
- scrypt password hashing (Node built-in `crypto`, no new deps)
|
||||
- `users` and `sessions` tables (sessions = DB-backed, 7-day TTL, hex tokens)
|
||||
- `bootstrapAdmin()` creates/syncs admin from `ADMIN_USERNAME`/`ADMIN_PASSWORD` on every startup → password owned by Docker env
|
||||
- `requireAuth` / `requireAdmin` Express middleware
|
||||
- Public: `/api/health`, `POST /api/auth/login`. Everything else guarded by `app.use('/api', auth.requireAuth)`.
|
||||
- User mgmt: `GET/POST /api/users`, `PATCH /api/users/:id/password`, `DELETE /api/users/:id` (all admin-only). Audit log entries for login attempts and user changes.
|
||||
- **Frontend** (`client/src/auth.js`, `LoginModal.jsx`, `UserManagementModal.jsx`, `App.jsx`)
|
||||
- axios interceptors inject `Bearer` token from localStorage, auto-logout on 401
|
||||
- Auth gate in App: validates stored token via `/api/auth/me` on load, shows LoginModal if no session
|
||||
- Nav: user badge, Logout, and admin-only **Users** button
|
||||
- **Docker** — `ENV ADMIN_USERNAME=admin` and `ENV ADMIN_PASSWORD=changeme` (placeholder, must override)
|
||||
- **Docs** — in-app admin guide (`ReadmeModal.jsx`) gained an "Authentication & User Accounts" section; auth moved into Roadmap → Shipped; `README_UNRAID_INSTALL.md` adds the two new env-var rows + login verify step + troubleshooting for forgotten admin password.
|
||||
|
||||
## Key decisions
|
||||
|
||||
- Used Node's built-in `crypto.scryptSync` to **avoid adding bcrypt** as a dependency. Storage format: `scrypt$<saltHex>$<hashHex>`. Constant-time compare via `crypto.timingSafeEqual`.
|
||||
- DB-backed sessions instead of JWT or in-memory map → survives container restarts, single source of truth, easy admin revoke if needed.
|
||||
- Admin password **re-syncs from env on every startup**. Trade-off: admin can't change their own password in UI (it would be reverted on restart), but rotating the env var rotates the live credential — clean operational story.
|
||||
- Did **not** wrap `window.fetch`. All component API calls go through axios; the lone bare `fetch('/version.json')` hits a public static file. So axios interceptors are sufficient.
|
||||
- PDF downloads use `axios.get(..., { responseType: 'blob' })`, not anchor links → interceptor still attaches the token, no special-case needed.
|
||||
|
||||
## Loose ends / future work
|
||||
|
||||
- No password-change UI for non-admin users yet (admins reset for them).
|
||||
- No expired-session cleanup job (lazy prune only).
|
||||
- Only admin/user roles. Supervisor-scoped or read-only roles still open.
|
||||
|
||||
## Verification status
|
||||
|
||||
- `node --check` passes on `auth.js`, `server.js`, `db/database.js`.
|
||||
- `client && npm run build` succeeds.
|
||||
- **Could not run server locally** — `better-sqlite3` fails to compile against local Node v24. Builds fine in the Docker `node:20-alpine` image; full verification requires building and running the container.
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
type: session
|
||||
date: 2026-05-27
|
||||
tags:
|
||||
- gitea
|
||||
- git-alwisp
|
||||
- repo-maintenance
|
||||
- mpm
|
||||
---
|
||||
|
||||
# Session: Gitea Repo Description Sweep — 2026-05-27
|
||||
|
||||
## What happened
|
||||
|
||||
Used the git-personal:gitea skill to audit and update all `/jason` repos on git.alwisp.com that were missing descriptions.
|
||||
|
||||
## Key outcomes
|
||||
|
||||
- 47 total repos enumerated for `jason` user on git.alwisp.com
|
||||
- 25 repos had no description at session start
|
||||
- Read README.md and project-specific .md files (PROJECT.md, JARVIS.md, INSTALL.md, etc.) from each repo to derive accurate descriptions
|
||||
- Successfully patched 23 repos with descriptions via Gitea API PATCH
|
||||
- **stepview** clarified by Jason: self-hosted in-browser 3D STEP file viewer (OpenCascade WASM + three.js)
|
||||
- **qrknit** clarified by Jason: commercial QR code generator and URL shortener — lives under `qrknit/qrknit` org (not `jason/`)
|
||||
- **google-mcp** belongs to `mpm` org — Jason declined to update it
|
||||
|
||||
## Repo ownership notes
|
||||
|
||||
- `qrknit` is its own org: `qrknit/qrknit` on git.alwisp.com
|
||||
- `google-mcp` belongs to `mpm` org on git.alwisp.com
|
||||
|
||||
## Repos updated (previously blank)
|
||||
|
||||
agents, alwisp, breedr, codedump, codexium-odoo, cpas, email, fabdash, family-planner, inven, jarvis, memer, mempalace, mrp, mrp-qrcode, nyaa-crawler, pos, rack-planner, stellar, stepview, storybid, totalmcp, ui-tracker, qrknit (under qrknit org)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
---
|
||||
type: session
|
||||
tags: [obsidian, cowork, plugin, memory]
|
||||
updated: 2026-05-27
|
||||
---
|
||||
|
||||
# Session: Built obsidian-memory-plugin Cowork Plugin
|
||||
|
||||
**Date:** 2026-05-27
|
||||
**Topic:** Designed and built the [[obsidian-memory-plugin]] for Claude Cowork
|
||||
|
||||
## What happened
|
||||
|
||||
Jason wanted Claude to have persistent memory across Cowork sessions using his Obsidian vault (running Local REST API at 10.2.0.2:27124). We built a skill-only plugin — no MCP server — that calls the REST API directly via bash/curl. The plugin includes a lean SKILL.md and four reference files. It was packaged as obsidian-memory.plugin and installed. Vault was bootstrapped in the same session.
|
||||
|
||||
## Decisions made
|
||||
|
||||
- Skill-only plugin (no MCP): direct curl calls are simpler and sufficient for a personal plugin, and produce a single distributable file
|
||||
- API key hardcoded in skill (personal plugin, not for sharing)
|
||||
- SSL verification skipped with `-k` (self-signed Obsidian cert)
|
||||
- Temp file pattern (`cat > /tmp/obs_entry.md`) used for all multi-line writes to avoid heredoc quoting issues
|
||||
|
||||
## Artifacts produced
|
||||
|
||||
- `obsidian-memory.plugin` — installed in Cowork
|
||||
- `Projects/agents/README.md` — vault index (bootstrapped)
|
||||
- `Projects/agents/memory/profile.md` — Jason's persistent profile (empty, to be filled)
|
||||
- `Projects/agents/memory/inbox.md` — untriaged notes inbox
|
||||
- `Projects/agents/memory/projects/obsidian-memory-plugin.md` — this project's state
|
||||
|
||||
## Open threads
|
||||
|
||||
- [ ] Fill in profile.md (role, preferences, tools stack) as facts come up in conversations
|
||||
- [ ] Test memory recall in a fresh session — load README → profile → relevant project
|
||||
|
||||
## Next step
|
||||
|
||||
In the next substantive session, load memory silently at the start (README → profile) and start populating profile.md as Jason's role and preferences emerge.
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
type: session
|
||||
date: 2026-05-27
|
||||
tags: [gitea, plugin, cowork]
|
||||
---
|
||||
|
||||
# 2026-05-27 — Personal Gitea Plugin
|
||||
|
||||
## What happened
|
||||
|
||||
Built a personal Gitea plugin (`git-personal`) for git.alwisp.com, forked from the existing work plugin (`git-mpm` targeting git.mpm.to). Updated all server URLs, API key, and plugin metadata throughout 8 files.
|
||||
|
||||
Then updated the plugin's file upload patterns to avoid a known base64 encoding problem.
|
||||
|
||||
## Decisions made
|
||||
|
||||
- Personal plugin lives separately from the work plugin — same skill structure, different credentials and base URL
|
||||
- File create/update operations now use Python + temp files for base64 encoding instead of `echo -n | base64` (which caused silent corruption via line-wrapping and shell escaping)
|
||||
- Reads always use the `/raw/` endpoint rather than decoding the base64 `content` field from `/contents/`
|
||||
- JSON payloads are written to a temp file and passed via `--data-binary @file` to avoid shell escaping issues
|
||||
|
||||
## Artifacts
|
||||
|
||||
- `F:\CLAUDE\COWORK\COWORK\git-personal.plugin` — ready to install
|
||||
|
||||
## Open threads / next steps
|
||||
|
||||
- None identified
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
---
|
||||
type: session
|
||||
project: unifi-access-dashboard
|
||||
tags:
|
||||
- unifi
|
||||
- flask
|
||||
- sqlite
|
||||
- filtering
|
||||
- identity-merging
|
||||
- access
|
||||
- attendance
|
||||
updated: 2026-05-28
|
||||
---
|
||||
|
||||
# 2026-05-28 — [[UniFi-Access-Dashboard]]: tenant filtering + identity merging
|
||||
|
||||
Same-day follow-up to the multi-controller ship. Two features added back-to-back, both purely additive at the DB layer.
|
||||
|
||||
## What got built
|
||||
|
||||
**Tenant filtering** — one-click Hide/Unhide on the attendance table so building tenants (and any non-staff badge holder) don't trip the LATE warning or clutter the daily view. Filtered actors are excluded from `/api/first-badge-status` by default; a "Show filtered" toggle re-includes them as dimmed rows tagged FILTERED. A 🚫 Filtered header button opens a modal listing all filtered actors for bulk unhide.
|
||||
|
||||
**Identity merging** — same physical person on two controllers gets two UUIDs from UniFi, which produces split first-badge times (and false LATE warnings). The new `persons` + `person_members` tables let the user group N `(controller_id, actor_id)` rows under a single person with one editorial display name. The attendance query groups by `COALESCE(person_id, controller||'|'||actor)` so merged rows collapse to a single line; the Source column renders a chip per controller plus a MERGED pill. A People modal auto-suggests merges based on exact full-name matches across distinct controllers — one click per suggestion, never auto-applied.
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **Schema shape**: chose `persons` + `person_members` tables (Option A) over a `merge_group` column on `user_cache` (Option B) or a self-referential `merged_into` (Option C). Reasoning: `user_cache` is a sync cache; `persons` is the editorial layer that can grow per-person fields later (notes, department, photo) without polluting the sync table.
|
||||
- **Filtering semantics with merge**: `COALESCE(p.filtered, u.filtered, 0)` — person-level filter wins when merged, actor-level applies when not. Split-off members return unfiltered (person filter dies with the merge); simpler mental model than inheriting.
|
||||
- **Webhook ingest untouched**: both features are display-time only. No DB row is ever lost. Unmerging/unfiltering instantly restores complete history.
|
||||
- **Auto-suggestions are conservative**: only exact name matches across ≥2 distinct controllers, excluding "User xxxxx" placeholders. No fuzzy matching at suggestion time (the inline merge picker does fuzzy search for manual cases).
|
||||
|
||||
## Files touched
|
||||
|
||||
- `app.py` — added two migrations (`filtered` column on `user_cache`, `persons` + `person_members` tables), reworked `/api/first-badge-status`, added 11 new endpoints (users CRUD + persons CRUD + suggestions).
|
||||
- `static/index.html` — new Filtered modal, new People modal with merge picker, Hide/Merge buttons in an Actions column, multi-chip Source rendering, MERGED pill, Show filtered toggle.
|
||||
- `README.md` — new "Filtering tenants" and "Merging identities across controllers" sections, new troubleshooting rows, full endpoint table refresh.
|
||||
|
||||
## Sharp edges
|
||||
|
||||
- Inline merge picker is **pairwise only**. For 3+ controllers per person, merge two first then use People modal → Add member.
|
||||
- Source field in API response: kept `row.source` (single string) for backwards compat; the new arrays are `row.sources` and `row.controller_ids`. Any consumer using the old field still works.
|
||||
- `Local REST API` plugin on the vault rejects `PATCH ... Target-Type: heading` with `invalid-target` even for documented examples — the heading-addressed PATCH is broken on this server's plugin version. Used `POST` (append-to-EOF) instead. Worth investigating later but not blocking.
|
||||
|
||||
## Repo path observation
|
||||
|
||||
Working directory this session was `D:\REMOTE CODING\unifi-access-dashboard`, not the `F:\CODING\unifi-access-dashboard` path recorded as canonical in `[[unifi-access-dashboard]]`. Did not silently update the project file's path field — flag if it's a permanent move.
|
||||
|
||||
## Pre-existing code review issues (still unaddressed)
|
||||
|
||||
These were noted in earlier project memory and the filter+merge work didn't touch them:
|
||||
|
||||
1. Webhook signature fails *open* when secret is empty (`app.py:252-254`) — intentional LAN-trust mode per Jason.
|
||||
2. All `/api/*` endpoints unauthenticated — intentional per README.
|
||||
3. `first <= cutoff_end` is string-lex compare on `HH:MM:SS` — works but brittle.
|
||||
4. No index on `badge_events(date, controller_id)` — full scan per page load. Will degrade at scale.
|
||||
5. `verify=False` for self-signed UniFi certs is undocumented inline.
|
||||
|
||||
The new features inherit the same LAN-trust posture, so anyone with LAN access can also POST to `/api/persons/...` or `/api/users/...` and mutate filter/merge state. Consistent with existing design.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
---
|
||||
type: session
|
||||
date: 2026-05-28
|
||||
tags:
|
||||
- unifi-access-dashboard
|
||||
- refactor
|
||||
- multi-controller
|
||||
- access
|
||||
- mpm
|
||||
---
|
||||
|
||||
# 2026-05-28 — UniFi Access Dashboard: multi-controller refactor
|
||||
|
||||
## Context
|
||||
Single-controller Flask app needed to support a client whose on-site server has LAN reach to multiple UniFi Access controllers. They want one dashboard showing badge events from all of them.
|
||||
|
||||
## Decisions made
|
||||
- Per-controller webhook URLs (`/api/unifi-access/<controller_id>`), legacy alias kept for back-compat.
|
||||
- Auto-register webhooks from the UI when adding a controller (drops the manual Python snippet from the old README).
|
||||
- **No admin auth, plaintext tokens in SQLite** — Jason explicitly chose LAN-only trust over the recommended Fernet + ADMIN_PASSWORD setup. Documented in [[unifi-access-dashboard]] so it doesn't get re-litigated.
|
||||
- Skipped CSV export for now (recommended bundle but Jason declined).
|
||||
- Auto-sync interval shortened from 6h to 1h late in the session.
|
||||
|
||||
## Files changed
|
||||
`app.py` (major rewrite), `static/index.html` (controllers modal + Source column + filter), `requirements.txt` (+pytz), `README.md` (UI-driven setup flow), `.env.example`, deleted `static/uad-landing.html`.
|
||||
|
||||
## Folded-in fixes
|
||||
- `datetime.utcnow()` → `datetime.now(timezone.utc)`
|
||||
- Cutoff string-compare bug (now sanitized to `HH:MM`)
|
||||
- Pinned `pytz` (was imported but undeclared)
|
||||
|
||||
## Not committed
|
||||
End of session: changes are on `main` working tree only. No commit or push. Run `docker compose up -d --build` to deploy.
|
||||
|
||||
## Open items
|
||||
- Existing webhook on Access side won't be cleaned up if user removes the seeded Default controller and re-adds via UI → duplicate events. Covered in project notes as a known gotchas.
|
||||
- Cross-controller identity merge (same human across sites) is explicitly out of scope.
|
||||
Reference in New Issue
Block a user