plugin 0.2.2

This commit is contained in:
2026-06-05 00:49:20 -05:00
parent 67acf8fd52
commit d37b248747
115 changed files with 5565 additions and 0 deletions
@@ -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.
@@ -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)
@@ -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.
@@ -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
@@ -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.
@@ -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.