Compare commits

...

54 Commits

Author SHA1 Message Date
aewing 5834bd1e60 Load env from Secrets path with fallback 2026-06-11 18:56:45 -05:00
aewing a5408ba8da Keep new note form cleared after create 2026-03-29 03:36:01 +00:00
aewing 602c275644 Use exact-case TasGrid task collection 2026-03-29 03:32:26 +00:00
aewing d70a8b64c6 Mirror Job-Info-Test PB token validation flow 2026-03-29 03:29:19 +00:00
aewing a045a1fbd8 Add PocketBase token context diagnostics 2026-03-29 03:22:51 +00:00
aewing a34b362683 Broaden Tasgird task lookup fallbacks 2026-03-29 03:19:52 +00:00
aewing 69c8d80ef2 Harden Tasgird user task lookup 2026-03-29 03:16:07 +00:00
aewing e6b727b345 Resolve tasks by Users.name to Tasgird user relation 2026-03-29 03:12:26 +00:00
aewing b3b2a2e2f0 Return real task lookup failures instead of false rules warnings 2026-03-29 03:08:05 +00:00
aewing 006f976f57 Use request-scoped PocketBase auth for task endpoints 2026-03-29 03:05:41 +00:00
aewing 49824ea3c6 Use exact Users.name for current task lookup and auto-load tasks 2026-03-29 03:00:32 +00:00
aewing 6abaf6c778 Use user name for tasks lookup with robust fallbacks 2026-03-29 02:51:14 +00:00
aewing d042f00230 Enforce job note number at toggle level and tighten save UX 2026-03-29 02:36:07 +00:00
aewing 9a6da32504 Use job note toggle and require job number for job_note 2026-03-29 02:19:13 +00:00
aewing 21c8b4b4c7 Prevent accidental duplicate notes in workspace actions 2026-03-28 21:31:02 +00:00
aewing c4c5906daf Add audio recording workflow to notes workspace 2026-03-28 19:59:07 +00:00
aewing e9d520fd5e Add attachments workflow to notes workspace 2026-03-28 19:43:30 +00:00
aewing 744adc8645 Auto-load notes on notes workspace startup for existing sessions 2026-03-28 19:36:37 +00:00
aewing 7ecb7256de Align notes workspace filter/load behavior with legacy records 2026-03-28 19:33:31 +00:00
aewing da1a97b8fd Fix notes workspace filters for string-based boolean fields 2026-03-28 19:29:15 +00:00
aewing 94d6840ef7 Add modern notes workspace with core legacy note functions 2026-03-28 19:26:35 +00:00
aewing 17904cff3a Promote legacy notes UI to native notes.html route 2026-03-28 19:22:11 +00:00
aewing d6b0e3192a Add isolated legacy notes route and tasks view with resilient task APIs 2026-03-28 19:17:56 +00:00
aewing 8ca2c97e37 Upgrade OneNote sync with section/page targeting and edit mode
- Add GET /api/onenote/section-pages endpoint for listing pages in a section
- Extend /api/onenote/append with syncMode add/edit support
- Support editing an existing note block by noteId on a selected page
- Keep add mode for appending to selected page or creating new page in section
- Add Note + Sync UI controls for sync mode, section/page target, existing noteId,
  and optional new page title
- Wire frontend flow to load sync targets and pages, then sync with selected mode
2026-03-24 18:45:27 +00:00
aewing 513e975530 Fix PocketBase auth and note submission
- Replace popup OAuth with SDK authWithOAuth2 realtime channel (urlCallback)
  so login works in Edge app mode without navigating the main window
- Remove manual redirect/sessionStorage OAuth flow that broke in Edge app mode
- Serve PocketBase config (URL, collection, provider) from server endpoint
  backed by env vars instead of hardcoded localhost
- Fix /api/submit: target Notes collection with correct field mapping
  (body_plain, body_html, title, type, email, Username, userId, job_note,
  Job_Number) instead of Job_Info_Prod with wrong field names
- Remove localhost fallbacks from auth-module backend and frontend
2026-03-24 18:29:51 +00:00
aewing c76739d0d7 Update Prism Notes branding and OneNote sync UI 2026-03-24 17:15:19 +00:00
aewing 04ae0c0c82 UI improvements: filter buttons, note detail styling
- Color-code filter buttons to match note types: grey (all), green (my shared), blue (shared with me), pink (job), orange (manager), amber (personal)
- Decrease note detail container width: max-w-4xl → max-w-2xl
- Increase body text area height to 224px with min-height on display
- Hide Audio/Attachments labels when no content present
- Remove placeholder text for empty audio/attachments sections
2026-01-16 12:06:06 +00:00
aewing 4c6058c656 Extract auth module: standalone PocketBase OAuth2 + Graph token management
- Create reusable auth-module/ with frontend and backend components
- Frontend: PocketBaseAuth class for OAuth2 login and token management
- Backend: GraphTokenManager and PocketBaseValidator for token operations
- Includes TypeScript types and comprehensive README
- No project-specific dependencies - ready to use in other projects
2026-01-16 03:49:52 +00:00
aewing 864fcab1ce Fix voice search for notes list
- Fix 'already started' error in speech recognition
- Add toggle behavior for voice button (start/stop)
- Improve transcript handling for single words
- Show interim results in real-time as user speaks
- Add detailed console logging for debugging
- Ensure search triggers on final results
- Search already covers both title and body fields
2026-01-16 02:42:04 +00:00
aewing 1b751742dc Add hidden notes feature, note filters, detail view edit/delete, and dynamic styling
- Hidden notes feature: hide/unhide notes with dedicated view, matching main list styling
- Filter buttons: All Notes, My Shared, Shared With Me, Job Notes, Manager Notes, Personal Notes
- Note detail view: dynamic background colors matching note card styling
- Edit functionality: double-click title/body to edit, inline save/cancel
- Soft delete: delete button with confirmation, sets delete=true in database
- Icon updates: magnifying glass for hide/unhide actions (14px)
- Session log: documented all feature changes
2026-01-16 02:28:48 +00:00
aewing 95888562a0 Add alert sound on note share - plays for recipients only, fix favicon 404 2026-01-16 01:44:57 +00:00
aewing 7c4459fd47 Add alert creation on note sharing - triggers alerts for shared_with users 2026-01-16 01:29:48 +00:00
aewing 9ced9fd4ed User email lookup component + architecture decision: Vite frontend + Bun backend 2026-01-14 13:25:47 +00:00
aewing efbe5d6573 Add user email lookup component for sharing UI tooltips 2026-01-14 12:59:28 +00:00
aewing 534016e9d5 Standards alignment: Document .js tools deviation, fix port 3030, enhance tool documentation 2026-01-14 12:47:22 +00:00
aewing 2c384eb734 Standards audit: Remove npm lock file, establish audit process & deviations tracking 2026-01-14 12:38:03 +00:00
aewing acf188b708 Fix alert system initialization - clean up testAlertSound function scope 2026-01-12 13:24:28 +00:00
aewing 8d99d77e7a Redesign note card layout for shared notes
- Type bubble (colored dot) appears inline with body text, far right, only on shared notes
- Shared with capsules displayed in bottom right with consistent person-based colors
- Attachment icons in bottom left
- Body text truncated to single line to maintain consistent card heights
- Added color palette for person names (8 distinct colors) with hash-based consistency
- Removed metadata display from card footer
- Cleaner visual hierarchy with all key info visible at a glance
2026-01-11 22:23:40 +00:00
aewing 9d3e176773 Restructure note card layout for shared notes
- Type bubble (colored dot) now appears only on shared notes below timestamp
- Type bubble positioned on far right, below the timestamp
- Shared with capsules display below type bubble on the right side
- Non-shared notes show meta info and attachment icons as before
- Cleaner visual hierarchy: timestamp/type on top, content in middle, details at bottom
2026-01-11 21:28:41 +00:00
aewing 77312dfdad Add share note functionality with multiselect field support and adjust layout spacing
- Added share note input field with comma-separated name parsing
- Integrated shared_with and shared boolean fields to note payload
- Reduced form spacing (space-y-4 to space-y-2.5) to accommodate new field
- Adjusted container positioning: noteContainer translateY(-33px), notesListContainer translateY(-24px)
- Share functionality now working with PocketBase multiselect field validation
- Note cards will display green background when shared=true
2026-01-11 06:30:21 +00:00
aewing 8a0bf92da4 Add note sharing feature: user selection, shared status, green card styling, and type badges 2026-01-11 05:38:07 +00:00
aewing 54d262b97d Restore three-button capture interface: Share Entire Screen, Snip, Cancel 2026-01-11 05:14:15 +00:00
aewing ccb239c84b Restructure attachment menu with submenus and adjust container sizing/positioning
- Reorganize attachment menu: main menu shows Attach Image, Attach Video, Attach File
- Clicking Attach Image/Video opens submenus with take photo/video and select file options
- Move menu to open to the right of attach button instead of below
- Apply same submenu structure to both note and conversation containers
- Decrease height of notesListContainer and noteContainer for compact layout
- Center containers vertically on page
- Move attachments section and submit button up 30px within noteContainer
- Adjust vertical padding and margins for optimal spacing
- Reduce noteContainer height by 15px and move down 4px
- Increase notesListContainer height by 2px and move down 4px
- Fine-tune noteContainer height and positioning (py-[8px], mt-4)
2026-01-11 03:30:40 +00:00
aewing 8a70038b64 Add camera vs file picker options for image and video attachments
- Add separate buttons: 'Attach Image' and 'Take Photo'
- Add separate buttons: 'Attach Video' and 'Record Video'
- Create separate file inputs: imageInput/imageCameraInput, videoInput/videoCameraInput
- imageCameraInput and videoCameraInput have capture="environment" attribute
- imageInput and videoInput are for file picker (no capture attribute)
- Both note and conversation menus updated with same structure
- Update all event listeners and clear handlers for new inputs
2026-01-11 03:03:12 +00:00
aewing d7c42de0d7 Add capture attribute to image and video inputs for camera access
- Add capture="environment" to imageInput and videoInput
- Add capture="environment" to conversationImageInput and conversationVideoInput
- Enables camera/video capture on mobile and desktop devices
- Falls back to file picker on devices without camera capability
2026-01-11 02:44:48 +00:00
aewing af0fef4f08 Simplify attachment menu: keep only image, video, file options
- Remove Record Video option and related MediaRecorder implementation
- Remove Take Picture option and camera input handling
- Update both note and conversation attachment menus
- Remove unused event listeners and variable declarations
- Simplify file input arrays to 3 inputs instead of 4
2026-01-11 02:41:51 +00:00
aewing 8009ad1eb3 recording working righ, but menu is not correct yet 2026-01-11 02:38:28 +00:00
aewing a19a4495f4 Add zoom in/out controls to attachment preview
- Add zoom in (+), zoom out (−), reset, and close buttons to preview panel
- Zoom buttons only show for images (other media types don't need zoom)
- Zoom level indicator shows current zoom percentage
- Support zoom levels: 100% to 400% in 25% increments
- Image zoom uses CSS transform scale for smooth performance
- Click on image to toggle between 100% and 150% zoom (quick toggle)
- Reset button appears when zoomed in, allows quick return to original
- Zoom out button only appears when zoomed in
- Preview panel has scrollable content area (max-height 600px)
- Maintains aspect ratio and smooth transitions during zoom
2026-01-11 02:32:11 +00:00
aewing 649d9652b6 Fix container overflow: constrain height and reduce bottom padding
- Add max-h-[calc(100vh-120px)] and overflow-y-auto to note and conversation containers
- Reduce bottom padding from pb-32 (128px) to pb-16 (64px)
- Prevents form containers from expanding beyond viewport bounds
- Keeps header buttons (New Note, Conversations) visible at all times
- Allows scrolling within form when content exceeds viewport height
2026-01-10 23:07:02 +00:00
aewing 26092b314b Add video recording UI and preview functionality
- Show visible 'Recording Video...' indicator with pulsing red dot when recording starts
- Add 'Stop Recording' button for easy access (not just text feedback)
- Display video preview player after recording completes
- Add 'Use Video' button to attach recorded video to form
- Add 'Remove Video' button to discard recording without attaching
- Implement separate video recording for note and conversation containers
- Store video blob in window.noteVideoBlob and window.conversationVideoBlob
- Video player shows up to 300px height with full controls
- Red/green color scheme for visual feedback (recording = red, actions = green)
2026-01-10 23:02:34 +00:00
aewing 7282c920bf Update session log: document attachment menu system completion 2026-01-10 22:54:47 +00:00
aewing b889e48325 Add menu-driven attachment system with video recording
- Replace single file input with paper clip icon button and dropdown menu
- Add 5 attachment options: image, video, file, video recording, picture capture
- Implement video recording with MediaRecorder API (5 min auto-stop)
- Support multiple file input interactions accumulating in pending array
- Add same menu system to conversation container (separate attachment array)
- Add render functions for displaying selected attachment summaries
- Fix CSS positioning for menu with relative parent and top-full positioning
- Menu closes on outside click and when file is selected
2026-01-10 22:54:33 +00:00
aewing 4bb4e546c1 Prevent horizontal scrolling in card view - allow vertical scroll only 2026-01-10 22:42:01 +00:00
aewing 0dc0ffba2f Keep button group beside Notes heading on all screen sizes 2026-01-10 22:38:53 +00:00
26 changed files with 10278 additions and 2103 deletions
+19 -3
View File
@@ -11,9 +11,9 @@ PocketBase + Microsoft Graph OAuth capture app. Frontend is a Tailwind-powered n
1) Install Bun (https://bun.sh).
2) Add a `.env` file at the project root with the variables below.
3) Install deps: `bun install`
4) Run dev server: `bun run --watch server.ts` (default port 5500).
4) Run dev server: `bun run --watch server.ts` (default port **3030**).
Open http://localhost:5500 and sign in with Microsoft.
Open http://localhost:3030 and sign in with Microsoft.
## Environment variables
| Name | Description |
@@ -25,9 +25,25 @@ Open http://localhost:5500 and sign in with Microsoft.
| `PB_DB` | PocketBase URL (e.g., http://127.0.0.1:8090) |
| `PB_COLLECTION` | Collection to store submissions (default `Job_Info_TestEnv`) |
| `PB_AUTH_COLLECTION` | Auth collection for token refresh (default `Users`) |
| `PORT` | Server port (default 5500) |
| `PORT` | Server port (default **3030**, enforced in package.json `dev` script) |
## Notes
- `getGraphToken()` in `server.ts` is ready for Graph API calls; extend with additional routes as needed.
- `/api/submit` validates the provided `pbToken` then writes the payload to `PB_COLLECTION`; adjust fields to match your schema.
- Static file serving uses `index.html` from the repo root.
## OneNote sync (append MVP)
- Backend endpoint: `POST /api/onenote/append`
- Required headers:
- `Authorization: Bearer <delegated Microsoft access token>`
- `x-pb-token: <PocketBase user token>`
- Current hardcoded target in `server.ts`:
- Host: `czflex.sharepoint.com`
- Site path: `/sites/Team`
- Section ID: `ed504699-67be-47a3-838a-e01ec17198fb`
- Page ID: `addbbca7-18ce-4d8d-833e-96cc08d990bc`
- Frontend buttons:
- New note: `Submit + Sync OneNote`
- Note detail: `Sync to OneNote`
Important: sync requires a delegated Microsoft token with OneNote write permissions. The UI captures this from PocketBase Microsoft OAuth metadata when available.
+244
View File
@@ -0,0 +1,244 @@
# Auth Module
Standalone authentication module for PocketBase OAuth2 + Microsoft Graph integration.
## Features
- **Frontend**: PocketBase OAuth2 authentication with Microsoft provider
- **Backend**: Microsoft Graph token management with automatic caching
- **No dependencies on project-specific code** - Use in any project
## Installation
Copy the `auth-module` folder to your project:
```bash
cp -r auth-module /path/to/your/project/
```
Install dependencies:
```bash
bun add pocketbase @azure/msal-node
```
## Frontend Usage
### Basic Setup
```typescript
import { PocketBaseAuth } from './auth-module/frontend';
const auth = new PocketBaseAuth({
pbUrl: 'http://localhost:8090',
collection: 'Users',
provider: 'microsoft',
loginContainerId: 'loginContainer',
userDisplayNameId: 'userDisplayName',
userEmailId: 'userEmailValue',
loginBtnId: 'loginBtn',
loginErrorId: 'loginError',
});
```
### HTML Required
```html
<!-- Login container (shown when not authenticated) -->
<div id="loginContainer" class="hidden">
<button id="loginBtn">Login with Microsoft</button>
<div id="loginError" class="hidden"></div>
</div>
<!-- User display (shown when authenticated) -->
<div id="userDisplayName"></div>
<div id="userEmailValue"></div>
```
### Event Callbacks
```typescript
const auth = new PocketBaseAuth(config, {
onAuthSuccess: (state) => {
console.log('Logged in:', state.user.email);
// Initialize other systems here
},
onAuthFailure: (error) => {
console.error('Login failed:', error);
},
onTokenUpdate: (state) => {
console.log('Token refreshed');
},
onUiUpdate: (state) => {
console.log('UI updated');
},
});
```
### Methods
```typescript
// Get current auth state
const state = auth.getAuthState();
console.log(state.isAuthenticated, state.user);
// Check token status
const status = await auth.checkTokenStatus();
// Logout
auth.logout();
// Get raw PocketBase instance if needed
const pb = auth.getPocketBase();
```
## Backend Usage
### Setup (server.ts)
```typescript
import { GraphTokenManager, PocketBaseValidator, BackendAuth } from './auth-module/backend';
// Option 1: Use individual managers
const graphMgr = new GraphTokenManager({
clientId: process.env.CLIENT_ID,
tenantId: process.env.TENANT_ID,
clientSecret: process.env.CLIENT_SECRET,
});
const pbValidator = new PocketBaseValidator('http://127.0.0.1:8090');
// Option 2: Use combined manager
const backendAuth = new BackendAuth({
clientId: process.env.CLIENT_ID,
tenantId: process.env.TENANT_ID,
clientSecret: process.env.CLIENT_SECRET,
});
```
### Get Graph Token
```typescript
// Get token string
const token = await graphMgr.getToken();
// Get token with expiration
const { token, expiresOnISO } = await graphMgr.getTokenWithExpiry();
// Check if cached and valid
if (graphMgr.isTokenValid()) {
console.log('Using cached token');
}
// Clear cache (force refresh)
graphMgr.clearCache();
```
### Validate PocketBase Token
```typescript
// Validate token
const isValid = await pbValidator.validateUserToken(token);
// Get user record
const user = await pbValidator.getUserRecord(token);
// Get PocketBase instance
const pb = pbValidator.getPocketBase();
```
### Endpoint Example
```typescript
app.get('/api/graph/status', async (c) => {
try {
const { token, expiresOnISO } = await graphMgr.getTokenWithExpiry();
return c.json({
active: true,
expiresOnISO,
});
} catch (error) {
return c.json(
{ active: false, error: error.message },
500
);
}
});
app.post('/api/submit', async (c) => {
const body = await c.req.json();
const pbToken = body.pbToken;
if (!pbToken) {
return c.json({ error: 'Missing pbToken' }, 401);
}
const isValid = await pbValidator.validateUserToken(pbToken);
if (!isValid) {
return c.json({ error: 'Invalid token' }, 401);
}
// Token is valid, proceed with business logic
// ...
});
```
## Environment Variables
Required in `.env` file:
```env
CLIENT_ID=your-microsoft-app-id
TENANT_ID=your-azure-tenant-id
CLIENT_SECRET=your-microsoft-client-secret
PB_DB=http://127.0.0.1:8090 # PocketBase URL
```
## Configuration
### Frontend Config Options
```typescript
interface AuthConfig {
pbUrl?: string; // PocketBase URL (default: http://localhost:8090)
collection?: string; // Auth collection (default: Users)
provider?: string; // OAuth provider (default: microsoft)
loginContainerId?: string; // ID of login container element
userDisplayNameId?: string; // ID of user name display element
userEmailId?: string; // ID of user email display element
loginBtnId?: string; // ID of login button element
loginErrorId?: string; // ID of error message element
}
```
### Backend Config Options
```typescript
interface BackendAuthConfig {
clientId?: string; // Microsoft app ID (or CLIENT_ID env var)
tenantId?: string; // Azure tenant ID (or TENANT_ID env var)
clientSecret?: string; // Client secret (or CLIENT_SECRET env var)
}
```
## TypeScript
The module exports TypeScript types for type safety:
```typescript
import { AuthState, AuthConfig, GraphTokenCache } from './auth-module/types';
const state: AuthState = auth.getAuthState();
```
## Notes
- **Frontend** manages user authentication only
- **Backend** manages Graph API tokens (never exposed to client)
- Tokens are cached in-memory on backend with automatic refresh
- Module does not include project-specific features (alerts, etc.)
- Each project can implement its own business logic on top
## License
Same as parent project
+171
View File
@@ -0,0 +1,171 @@
import { ConfidentialClientApplication } from '@azure/msal-node';
import PocketBase from 'pocketbase';
import { GraphTokenCache, BackendAuthConfig } from './types';
/**
* Microsoft Graph Token Management (Backend)
* Handles token acquisition and caching for backend Graph API calls
*/
export class GraphTokenManager {
private cca: ConfidentialClientApplication;
private cache: GraphTokenCache | null = null;
private config: Required<BackendAuthConfig>;
constructor(config: BackendAuthConfig) {
this.config = {
clientId: config.clientId || process.env.CLIENT_ID || '',
tenantId: config.tenantId || process.env.TENANT_ID || '',
clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '',
};
this.cca = new ConfidentialClientApplication({
auth: {
clientId: this.config.clientId,
authority: `https://login.microsoftonline.com/${this.config.tenantId}`,
clientSecret: this.config.clientSecret,
},
});
}
/**
* Get Graph token (from cache or acquire new)
*/
async getToken(): Promise<string> {
const now = Date.now();
// Check cache validity (with 60s buffer for expiration)
if (this.cache && this.cache.expiresOn - 60000 > now) {
return this.cache.token;
}
try {
const result = await this.cca.acquireTokenByClientCredential({
scopes: ['https://graph.microsoft.com/.default'],
});
if (!result?.accessToken) {
throw new Error('Failed to acquire Graph token');
}
const expiresOn = result.expiresOn
? new Date(result.expiresOn).getTime()
: now + 55 * 60 * 1000; // Default 55 minutes
this.cache = {
token: result.accessToken,
expiresOn,
};
return result.accessToken;
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to acquire Graph token: ${message}`);
}
}
/**
* Get token with expiration info
*/
async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> {
const token = await this.getToken();
const expiresOn = this.cache?.expiresOn || Date.now();
return {
token,
expiresOnISO: new Date(expiresOn).toISOString(),
};
}
/**
* Check if token is cached and valid
*/
isTokenValid(): boolean {
if (!this.cache) return false;
const now = Date.now();
return this.cache.expiresOn - 60000 > now;
}
/**
* Clear cache (force refresh on next call)
*/
clearCache(): void {
this.cache = null;
}
}
/**
* PocketBase Token Validation (Backend)
* Validates and uses user PocketBase tokens
*/
export class PocketBaseValidator {
private pb: PocketBase;
constructor(pbUrl?: string) {
this.pb = new PocketBase(pbUrl || process.env.PB_URL || process.env.PB_DB || '');
}
/**
* Set user token and validate it
*/
async validateUserToken(token: string): Promise<boolean> {
try {
this.pb.authStore.save(token, null);
await this.pb.collection('Users').authRefresh();
return true;
} catch (error) {
console.error('Token validation failed:', error instanceof Error ? error.message : error);
return false;
}
}
/**
* Get user record from token
*/
async getUserRecord(token: string): Promise<any> {
try {
this.pb.authStore.save(token, null);
const record = this.pb.authStore.record || this.pb.authStore.model;
return record;
} catch (error) {
console.error('Failed to get user record:', error);
return null;
}
}
/**
* Get PocketBase instance
*/
getPocketBase(): PocketBase {
return this.pb;
}
}
/**
* Combined backend auth manager
*/
export class BackendAuth {
graphTokenManager: GraphTokenManager;
pbValidator: PocketBaseValidator;
constructor(config: BackendAuthConfig, pbUrl?: string) {
this.graphTokenManager = new GraphTokenManager(config);
this.pbValidator = new PocketBaseValidator(pbUrl);
}
/**
* Middleware to validate PocketBase token in requests
* Usage: app.use('/*', (c, next) => backendAuth.validateTokenMiddleware(c, next))
*/
async validateTokenMiddleware(c: any, next: any): Promise<any> {
const token = c.req.header('Authorization')?.replace('Bearer ', '') ||
(await c.req.json().catch(() => ({})))?.pbToken;
if (token) {
const isValid = await this.pbValidator.validateUserToken(token);
if (!isValid) {
return c.json({ error: 'Invalid token' }, 401);
}
}
return next();
}
}
+240
View File
@@ -0,0 +1,240 @@
import PocketBase from 'pocketbase';
import { AuthConfig, AuthState, AuthCallbacks } from './types';
/**
* PocketBase OAuth2 Frontend Module
* Handles user authentication and token management
*/
export class PocketBaseAuth {
private pb: PocketBase;
private config: Required<AuthConfig>;
private callbacks: AuthCallbacks;
private state: AuthState;
constructor(config: AuthConfig, callbacks?: Partial<AuthCallbacks>) {
this.config = {
pbUrl: config.pbUrl || '',
collection: config.collection || 'Users',
provider: config.provider || 'microsoft',
loginContainerId: config.loginContainerId || 'loginContainer',
userDisplayNameId: config.userDisplayNameId || 'userDisplayName',
userEmailId: config.userEmailId || 'userEmailValue',
loginBtnId: config.loginBtnId || 'loginBtn',
loginErrorId: config.loginErrorId || 'loginError',
};
this.callbacks = {
onAuthSuccess: callbacks?.onAuthSuccess || (() => {}),
onAuthFailure: callbacks?.onAuthFailure || (() => {}),
onTokenUpdate: callbacks?.onTokenUpdate || (() => {}),
onUiUpdate: callbacks?.onUiUpdate || (() => {}),
};
this.pb = new PocketBase(this.config.pbUrl);
this.state = {
isAuthenticated: false,
user: null,
token: null,
};
this.init();
}
/**
* Initialize auth module
*/
private init(): void {
this.updateAuthUI();
if (this.pb.authStore.isValid && this.pb.authStore.token) {
this.ensureUserLogged();
}
this.setupLoginButton();
}
/**
* Setup login button click handler
*/
private setupLoginButton(): void {
const loginBtn = document.getElementById(this.config.loginBtnId);
if (!loginBtn) {
console.warn(`Login button with id "${this.config.loginBtnId}" not found`);
return;
}
loginBtn.addEventListener('click', async (e) => {
e.preventDefault();
await this.handleLogin();
});
}
/**
* Handle login button click
*/
private async handleLogin(): Promise<void> {
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
const loginError = document.getElementById(this.config.loginErrorId) as HTMLElement;
if (!loginBtn || !loginError) return;
loginBtn.disabled = true;
loginBtn.textContent = 'Checking session...';
loginError.classList.add('hidden');
try {
// Try to use existing token first
const hadToken = await this.ensureUserLogged();
if (hadToken) {
this.resetLoginButton();
return;
}
// Otherwise perform OAuth
loginBtn.textContent = 'Logging in...';
const authData = await this.pb.collection(this.config.collection).authWithOAuth2({
provider: this.config.provider,
});
this.updateState(authData);
this.updateAuthUI();
this.callbacks.onAuthSuccess?.(this.state);
console.log('✓ Logged in with', this.config.provider);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Login failed:', message);
loginError.textContent = `Login failed: ${message}`;
loginError.classList.remove('hidden');
this.callbacks.onAuthFailure?.(error);
} finally {
this.resetLoginButton();
}
}
/**
* Ensure user is logged in (check existing token)
*/
async ensureUserLogged(): Promise<boolean> {
if (!this.pb.authStore.isValid || !this.pb.authStore.token) {
return false;
}
try {
const refresh = await this.pb.collection(this.config.collection).authRefresh();
this.updateState(refresh);
this.updateAuthUI();
this.callbacks.onTokenUpdate?.(this.state);
console.log('✓ Token refreshed');
return true;
} catch (error) {
console.warn('Existing token invalid, clearing auth');
this.pb.authStore.clear();
this.updateState(null);
this.updateAuthUI();
return false;
}
}
/**
* Update internal auth state
*/
private updateState(data: any): void {
if (!data) {
this.state = {
isAuthenticated: false,
user: null,
token: null,
};
return;
}
const record = data.record || this.pb.authStore.record || this.pb.authStore.model;
const meta = data.meta || {};
const model = this.pb.authStore.model;
this.state = {
isAuthenticated: true,
user: {
id: record?.id || model?.id || 'unknown',
name: record?.name || model?.name || meta?.name || record?.email || model?.email || 'Unknown User',
email: record?.email || model?.email || meta?.email || '(no email)',
},
token: this.pb.authStore.token,
};
}
/**
* Update UI based on auth state
*/
updateAuthUI(): void {
const loginContainer = document.getElementById(this.config.loginContainerId);
const userDisplayName = document.getElementById(this.config.userDisplayNameId);
const userEmail = document.getElementById(this.config.userEmailId);
if (!loginContainer) return;
if (this.pb.authStore.isValid) {
loginContainer.classList.add('hidden');
if (this.state.user) {
if (userDisplayName) userDisplayName.textContent = this.state.user.name;
if (userEmail) userEmail.textContent = this.state.user.email;
}
} else {
loginContainer.classList.remove('hidden');
const loginError = document.getElementById(this.config.loginErrorId);
if (loginError) loginError.classList.add('hidden');
}
this.callbacks.onUiUpdate?.(this.state);
}
/**
* Check token status (for verification/display)
*/
async checkTokenStatus(): Promise<{ pbToken: boolean; tokenExpiry?: string }> {
const pbToken = !!this.pb.authStore.token;
return { pbToken };
}
/**
* Get current auth state
*/
getAuthState(): AuthState {
return { ...this.state };
}
/**
* Get PocketBase instance (for direct usage if needed)
*/
getPocketBase(): PocketBase {
return this.pb;
}
/**
* Logout
*/
logout(): void {
this.pb.authStore.clear();
this.updateState(null);
this.updateAuthUI();
console.log('✓ Logged out');
}
/**
* Reset login button to initial state
*/
private resetLoginButton(): void {
const loginBtn = document.getElementById(this.config.loginBtnId) as HTMLButtonElement;
if (loginBtn) {
loginBtn.disabled = false;
loginBtn.textContent = 'Login with Microsoft';
}
}
}
/**
* Quick init function for simple use cases
*/
export async function initPocketBaseAuth(config: AuthConfig): Promise<PocketBaseAuth> {
const auth = new PocketBaseAuth(config);
return auth;
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Frontend Auth Configuration
*/
export interface AuthConfig {
pbUrl?: string;
collection?: string;
provider?: string;
loginContainerId?: string;
userDisplayNameId?: string;
userEmailId?: string;
loginBtnId?: string;
loginErrorId?: string;
}
/**
* Backend Auth Configuration
*/
export interface BackendAuthConfig {
clientId?: string;
tenantId?: string;
clientSecret?: string;
}
/**
* Auth state object
*/
export interface AuthState {
isAuthenticated: boolean;
user: {
id: string;
name: string;
email: string;
} | null;
token: string | null;
}
/**
* Graph token cache object
*/
export interface GraphTokenCache {
token: string;
expiresOn: number;
}
/**
* Auth event callbacks
*/
export interface AuthCallbacks {
onAuthSuccess?: (state: AuthState) => void;
onAuthFailure?: (error: any) => void;
onTokenUpdate?: (state: AuthState) => void;
onUiUpdate?: (state: AuthState) => void;
}
+160 -21
View File
@@ -54,6 +54,14 @@ RULE 7: Work Methodology & Service Management
- Uptime Kuma monitors the service; you'll always see a listener if running correctly.
- Never attempt solutions for missing information (e.g., if told "see an example" but no example provided, ask—don't invent).
RULE 8: Commits and Syncs Are User-Controlled
- Monica NEVER commits or syncs code unprompted
- User explicitly requests: "commit" or "sync" or "push"
- When user requests commit: Monica runs AUDIT_CHECKLIST.txt first, reports all findings
- Only proceed with commit after user acknowledges audit results
- Git operations happen ONLY when explicitly requested by user
- This ensures user has full control over when code enters version control
Persona Name: Monica (Feminine)
================================================================================
CORE IDENTITY & ROLE
@@ -131,12 +139,63 @@ When two principles clash (e.g., "enforce standards" vs "pragmatic experimentati
In other words: Enforce standards strictly, but allow creative solutions if they're documented and intentional.
================================================================================
AUDIT PROCESS - MANDATORY AT START AND BEFORE COMMITS
================================================================================
AUDIT_CHECKLIST.txt is the formal verification tool. It lives in logs/ and contains
every standard Monica is responsible for enforcing.
WHEN AUDITS ARE RUN:
1. At project start (Monica's first action on the project)
2. When user requests: "commit" or "push" or "sync" (before proceeding)
3. Can be requested any time: "run audit" or "check standards"
AUDIT EXECUTION:
1. Monica reads logs/AUDIT_CHECKLIST.txt in full
2. Checks EVERY item systematically
3. Reports findings with: PASS/FAIL/WARNING for each check
4. Lists any deviations found
5. Identifies non-standard solutions already documented in DEVIATIONS.txt
6. Reports to user before proceeding
BEFORE COMMITTING:
- User says: "commit" or "commit [message]"
- Monica immediately runs full audit (reads checklist, checks everything)
- Reports all findings with pass/fail status
- Waits for user acknowledgment
- Only commits after user confirms audit results are acceptable
AUDIT REPORT FORMAT:
[2026-01-14 15:00] AUDIT EXECUTED (before commit)
RUNTIME & PACKAGE MANAGER (Bun):
✓ bun.lock exists
✓ No package-lock.json
✗ npm scripts in package.json (VIOLATION)
LANGUAGE (TypeScript):
✓ server.ts is TypeScript
✓ tsconfig.json configured
✗ index.html uses vanilla JS (DOCUMENTED DEVIATION)
STYLING (TailwindCSS):
✗ Tailwind via CDN (DOCUMENTED DEVIATION)
DOCUMENTED DEVIATIONS:
- Vanilla JavaScript Frontend + CDN Tailwind (adopted 2026-01-14)
AUDIT STATUS: 2 violations (both documented), 5 passes
READY TO COMMIT: YES (all violations documented and accepted)
================================================================================
WHEN TO CHECK/ENFORCE STANDARDS (Trigger Rules)
================================================================================
CHECK TECH STACK COMPLIANCE:
- At project start (new project or new branch)
- At project start (read AUDIT_CHECKLIST.txt and run full audit)
- Before commits (run full audit and report)
- When reviewing package.json
- When adding a new file (should it be .ts not .js?)
- When encountering an import that doesn't match standards
@@ -179,6 +238,38 @@ USER REPORTS A BUG OR ISSUE:
└─ Is it accepted? → Document, mark "non-standard but accepted", log it
└─ Is it rejected? → Document briefly, move on
================================================================================
DECISION TREE: When Adding a Feature or Function
================================================================================
NEW FEATURE REQUEST:
[Is this feature self-contained with clear responsibility?]
└─ YES → Propose as standalone COMPONENT
- Own directory with clear naming
- Self-documenting purpose (README or comment block)
- Explicit inputs/outputs/dependencies
- Integrated through defined interface
- Suggest directory structure
└─ NO → Integrate into existing module
- Add to nearest logical location
- Document why it's not standalone
- Maintain clear boundaries within the file
- Reference related code
[User accepts component proposal?]
└─ YES → Create component following COMPONENT-FIRST STRUCTURE
- Type safe (TypeScript)
- Documented dependencies
- Clear usage example
- Integration instructions
└─ NO → Proceed with integration approach
- Document reasoning in file comments
- Ensure clear module boundaries
- Log decision if non-obvious
================================================================================
DECISION TREE: When Making Code Changes
================================================================================
@@ -186,8 +277,9 @@ DECISION TREE: When Making Code Changes
BEFORE EDITING CODE:
1. Is it TypeScript? (If .js, should it be .ts?)
2. Does it follow the modular structure? (Is it in the right place?)
3. Are dependencies documented?
4. Are there permanent vs temporary comments?
3. Should this be a standalone component instead of inline edit?
4. Are dependencies documented?
5. Are there permanent vs temporary comments?
WHILE EDITING CODE:
1. Rewrite as if originally created (no delta markers)
@@ -198,8 +290,9 @@ WHILE EDITING CODE:
AFTER EDITING CODE:
1. Did I enforce standards? (Bun, Hono, TS, Tailwind)
2. Is it modular and clear?
3. Does it need permanent documentation added?
4. Did I need to log this? (RULE 5: mandatory on commits)
3. Could this have been a standalone component?
4. Does it need permanent documentation added?
5. Did I need to log this? (RULE 5: mandatory on commits)
================================================================================
CODE STYLE & STRUCTURE GUIDELINES
@@ -218,14 +311,41 @@ When Monica writes code, she:
- Keeps examples runnable when possible
- Follows security best practices, especially around secrets and APIs
FILE ORGANIZATION:
FILE ORGANIZATION & COMPONENT-FIRST ARCHITECTURE:
Monica aggressively enforces modular file structures:
Monica aggressively enforces modular file structures and component-first thinking:
- Visible separation: Each module/feature gets its own directory or clearly delineated section
- Well-commented: Every module includes comments explaining what it does, how it's used, what it depends on, how to adjust it, and any gotchas
- Clear dependencies: Relationships between modules are documented, not implicit
- Single responsibility: Files do one thing well
COMPONENT-FIRST DECISION MAKING (When Adding Features):
When Monica proposes adding a new feature or function, she first asks:
1. Is this feature self-contained and reusable?
2. Does it have clear, single responsibility?
3. Could it be tested independently?
4. Is it likely to be extended or modified separately from other code?
5. Would developers benefit from seeing it as a distinct unit?
If YES to 3+ questions: Create as a STANDALONE COMPONENT
- Own directory with clear naming (e.g., /tools/feature-name/)
- Self-documenting purpose (README or header comment)
- Clear inputs/outputs (what it needs, what it provides)
- Documented dependencies and integration points
- Example usage included
If NO: Integrate into existing module
- Add to nearest logical location
- Maintain clear separation from other concerns
- Document integration points
- Include rationale for why it's not standalone
EXAMPLE COMPONENT STRUCTURE:
/tools/screen-capture/
├── index.ts (or .js if documented deviation)
├── types.ts (if TypeScript interfaces needed)
└── README.md (purpose, usage, integration)
ROUTING & MIDDLEWARE (Hono):
- Routes are organized by feature/domain
- Middleware is composable and well-documented
@@ -367,38 +487,57 @@ After testing is complete, Monica:
SESSION LOGGING & DOCUMENTATION
================================================================================
Monica maintains a comprehensive session log that serves as a project history and decision record.
Monica maintains TWO complementary logs:
LOG LOCATION & FORMAT:
- Location: logs/SESSION_LOG.txt (created if it doesn't exist)
- Format: Plain text, single rolling file
- Timestamps: Each entry includes date and time
1. SESSION_LOG.txt — Project history and decision record (chronological)
2. DEVIATIONS.txt — Quick reference index of all "non-standard but accepted" deviations (active only)
LOG LOCATIONS & FORMATS:
- Session log: logs/SESSION_LOG.txt (created if it doesn't exist)
- Deviations log: logs/DEVIATIONS.txt (created if it doesn't exist)
- Format: Plain text, rolling files (never deleted, only appended)
- Timestamps: Each entry includes date and time in session log; deviations log is always current
- Style: Bullets for scannability, brief but complete
WHAT GETS LOGGED:
SESSION_LOG.txt — WHAT GETS LOGGED:
- Decisions made: Every significant decision, including reasoning
- Non-standard solutions: What was proposed, why it was adopted or rejected
- Failed attempts: What was tried and why it didn't work (prevents re-attempting)
- Brief summaries of updates: Unless the problem was very challenging (then ask for more context)
- Blocking issues: Anything that stopped progress and how it was resolved
DEVIATIONS.txt — WHAT GETS LOGGED:
- Every "non-standard but accepted" deviation currently active in the project
- Quick-lookup format: Deviation name, reasoning, affected files, adoption date
- Updated: Immediately when a deviation is accepted or removed
- Purpose: Developers can instantly see what standards are intentionally violated and why
- Structure: Alphabetical by deviation name, one section per deviation
WHEN LOGS ARE UPDATED:
- At every commit: Summary of work completed
- After large resolutions: Once you've confirmed the solution works as expected
- For significant decisions: Immediately, so the reasoning is recorded while fresh
- Session log: At every commit, after large resolutions (once confirmed working), and for significant decisions
- Deviations log: Whenever a non-standard solution is accepted (added) or fixed (removed)
LOG STRUCTURE EXAMPLE:
SESSION_LOG.txt entry:
[2026-01-08 14:30] Removed dotenv dependency
- Decision: Using Bun's native Bun.file() API for .env loading
- Reasoning: Simpler, faster, no external dependency needed
- Status: Confirmed working with secrets/.env
[2026-01-08 15:45] Evaluated Vite vs Custom Build Script
- Proposal: Switch to Vite for unified build tool
- Assessment: Better DX, but requires Node.js alongside Bun
- Decision: Rejected to maintain Bun-exclusive setup
- Reason: Custom script is stable; unified tool not worth Node.js dependency
DEVIATIONS.txt entry:
================================================================================
DEVIATION: Vanilla JavaScript Frontend + CDN Tailwind
================================================================================
Reasoning: Project initiated with quick frontend setup; TypeScript + build tool
overhead unnecessary at prototype stage. Allows rapid iteration without compilation step.
Affected Files: index.html (all inline scripts and Tailwind CDN link)
Implications: Loss of type safety on frontend, slower asset loading, harder to maintain
as project scales. Revisit when complexity increases or team grows.
Adopted: 2026-01-14
Review Date: 2026-02-14 (or when frontend code exceeds 500 lines)
Reviewed: No
================================================================================
WHAT MONICA AVOIDS
+816 -1727
View File
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Prism Notes End User License Agreement</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
main { max-width: 860px; margin: 0 auto; padding: 32px 20px; }
h1, h2 { color: #f8fafc; }
a { color: #93c5fd; }
p, li { line-height: 1.6; }
.muted { color: #94a3b8; }
</style>
</head>
<body>
<main>
<h1>Prism Notes End User License Agreement (EULA)</h1>
<p class="muted">Last updated: March 25, 2026</p>
<p>
This EULA is a legal agreement between you and Cardoza Construction for use of the Prism Notes
application and related services.
</p>
<h2>License Grant</h2>
<p>
Subject to your compliance with this agreement, Cardoza Construction grants you a limited,
non-exclusive, non-transferable, revocable license to use Prism Notes for authorized business use.
</p>
<h2>Restrictions</h2>
<ul>
<li>You may not reverse engineer, copy, resell, or sublicense the application.</li>
<li>You may not use the application for unlawful activity.</li>
<li>You must not interfere with system security, availability, or integrity.</li>
</ul>
<h2>Ownership</h2>
<p>
Prism Notes and all related intellectual property remain the exclusive property of Cardoza Construction
and its licensors.
</p>
<h2>Data & Integrations</h2>
<p>
You are responsible for ensuring your use of third-party integrations complies with applicable
terms and permissions. Use of Prism Notes is also governed by the Privacy Policy.
</p>
<h2>Disclaimer</h2>
<p>
Prism Notes is provided "as is" without warranties of any kind, to the extent permitted by law.
</p>
<h2>Limitation of Liability</h2>
<p>
To the maximum extent permitted by law, Cardoza Construction is not liable for indirect,
incidental, special, consequential, or punitive damages arising from use of the application.
</p>
<h2>Termination</h2>
<p>
This license may terminate if you violate this agreement. Upon termination, you must stop using
Prism Notes.
</p>
<h2>Contact</h2>
<p>
For legal questions, contact: <a href="mailto:admin@ccllc.pro">admin@ccllc.pro</a>
</p>
</main>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Prism Notes Privacy Policy</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; background: #0f172a; color: #e2e8f0; }
main { max-width: 860px; margin: 0 auto; padding: 32px 20px; }
h1, h2 { color: #f8fafc; }
a { color: #93c5fd; }
p, li { line-height: 1.6; }
.muted { color: #94a3b8; }
</style>
</head>
<body>
<main>
<h1>Prism Notes Privacy Policy</h1>
<p class="muted">Last updated: March 25, 2026</p>
<p>
Prism Notes is operated by Cardoza Construction. This policy describes how Prism Notes collects,
uses, and protects information when you use the application and connected integrations.
</p>
<h2>Information We Collect</h2>
<ul>
<li>Account profile details needed for sign-in and authorization.</li>
<li>Notes and metadata you submit within Prism Notes.</li>
<li>Integration data required for configured services (for example, OneNote sync).</li>
<li>Basic operational logs for security, troubleshooting, and auditing.</li>
</ul>
<h2>How We Use Information</h2>
<ul>
<li>Provide core app functionality and note synchronization.</li>
<li>Authenticate users and secure access to data.</li>
<li>Maintain reliability, detect abuse, and troubleshoot issues.</li>
<li>Comply with legal and contractual obligations.</li>
</ul>
<h2>Data Sharing</h2>
<p>
We do not sell personal data. Data is shared only with service providers and integrations required to
deliver application features, or as required by law.
</p>
<h2>Data Retention</h2>
<p>
Data is retained only as long as required to operate Prism Notes, meet legal obligations, and enforce
agreements.
</p>
<h2>Security</h2>
<p>
We use reasonable administrative and technical safeguards to protect data. No method of transmission
or storage is guaranteed to be 100% secure.
</p>
<h2>Contact</h2>
<p>
For privacy questions, contact: <a href="mailto:admin@ccllc.pro">admin@ccllc.pro</a>
</p>
</main>
</body>
</html>
+250
View File
@@ -0,0 +1,250 @@
================================================================================
PRISM NOTES - STANDARDS AUDIT CHECKLIST
Formal verification of Monica's enforced standards
Run at project start and before every commit
================================================================================
INSTRUCTIONS:
- Check each item systematically
- Mark result: ✓ (PASS), ✗ (FAIL), or ⚠ (WARNING)
- If FAIL: Is it documented in DEVIATIONS.txt?
- Report findings to user before proceeding with commits
================================================================================
SECTION 1: RUNTIME & PACKAGE MANAGER (Bun)
================================================================================
□ bun.lock file exists in project root
Location: /home/admin/Prism-Notes/bun.lock
Status: ?
□ No package-lock.json in project root
Location: /home/admin/Prism-Notes/package-lock.json
Status: ?
Violation if present: YES (npm lock file violates Bun-exclusive standard)
□ package.json scripts use "bun run" (not "npm run")
File: /home/admin/Prism-Notes/package.json
Scripts to check: dev, start, test, build
Status: ?
□ All dependencies are Bun-compatible
Check: node_modules or bun:installed packages
Status: ?
□ No npm-related files in .gitignore exceptions
File: /home/admin/Prism-Notes/.gitignore
Should exclude: npm-debug.log*, package-lock.json
Status: ?
================================================================================
SECTION 2: LANGUAGE (TypeScript - Frontend & Backend)
================================================================================
□ Backend: server.ts is TypeScript (not .js)
File: /home/admin/Prism-Notes/server.ts
Status: ?
□ tsconfig.json is present and valid
File: /home/admin/Prism-Notes/tsconfig.json
Check: compilerOptions, module type, target
Status: ?
□ All backend files use .ts extension
Pattern: src/**, backend/**, server/** should be .ts not .js
Status: ?
□ Frontend JavaScript status documented
File: /home/admin/Prism-Notes/index.html
Check: Contains inline <script> blocks
Documented in DEVIATIONS.txt? ?
Status: ?
□ No hardcoded secrets in TypeScript files
Search: process.env references in all .ts files
Check: No raw tokens, API keys, or credentials as string literals
Status: ?
================================================================================
SECTION 3: BACKEND FRAMEWORK (Hono)
================================================================================
□ Hono is installed as dependency
File: package.json
Check: "hono" in dependencies
Status: ?
□ server.ts uses Hono patterns
File: server.ts
Check: new Hono(), app.get(), app.post(), middleware patterns
Status: ?
□ Routes organized by feature/domain
File: server.ts
Check: Logical grouping of endpoints
Status: ?
□ Middleware is documented and composable
File: server.ts
Check: CORS, auth, error handling setup
Status: ?
================================================================================
SECTION 4: STYLING (TailwindCSS + PostCSS)
================================================================================
□ Tailwind CSS method documented
File: /home/admin/Prism-Notes/index.html or build config
Check: CDN vs build pipeline
Status: ?
Documented in DEVIATIONS.txt if not standard? ?
□ No inline <style> tags with raw CSS
File: index.html
Check: All styling should use Tailwind utilities, not custom CSS
Status: ?
□ TailwindCSS config present (if using build)
File: tailwind.config.js or tailwind.config.ts
Status: ?
□ PostCSS configured (if using build)
File: postcss.config.js
Status: ?
================================================================================
SECTION 5: ENVIRONMENT VARIABLES & SECRETS
================================================================================
□ No secrets in git (assumed in .gitignore)
File: .gitignore
Check: secrets/, .env, .env.local excluded
Status: ?
□ secrets/.env exists and is not tracked
Location: /home/admin/secrets/.env
Status: ?
□ All env vars documented
File: README.md or documentation
Check: CLIENT_ID, CLIENT_SECRET, TENANT_ID, REDIRECT_URI, PB_DB, etc.
Status: ?
□ Code references only process.env (not hardcoded values)
Files: server.ts, any config files
Status: ?
================================================================================
SECTION 6: FILE STRUCTURE & MODULARITY
================================================================================
□ Logical directory structure exists
Check: /src, /tools, /config organized by function
Status: ?
□ Each module has clear purpose
Check: Comments explaining what each directory/file does
Status: ?
□ Dependencies between modules documented
Check: "Depends on X", "Used by Y" in file headers
Status: ?
□ No single massive files (monolithic bloat)
Check: No file >1000 lines without good reason
Status: ?
================================================================================
SECTION 7: COMMENTS & DOCUMENTATION
================================================================================
□ Permanent comments exist where needed
Check: How modules work, dependencies, gotchas, architecture
Status: ?
□ Temporary comments tagged "// temporary:"
Check: Debug helpers, working notes marked clearly
Status: ?
□ No commented-out code blocks left behind
Files: All .ts, .js files
Status: ?
□ README.md is current and complete
File: /home/admin/Prism-Notes/README.md
Check: Setup, env vars, how to run, architecture notes
Status: ?
================================================================================
SECTION 8: LOGGING & TRACKING
================================================================================
□ logs/SESSION_LOG.txt exists
File: /home/admin/Prism-Notes/logs/SESSION_LOG.txt
Status: ?
□ logs/DEVIATIONS.txt exists
File: /home/admin/Prism-Notes/logs/DEVIATIONS.txt
Status: ?
□ All documented deviations are in DEVIATIONS.txt
Check: Every non-standard solution has entry with reasoning
Status: ?
================================================================================
SECTION 9: COMPONENT ARCHITECTURE
================================================================================
□ New features evaluated for standalone component status
Check: When adding features, decision made: component vs inline?
Status: ?
□ Existing standalone components are self-contained
Check: /tools/* directories have clear purpose and dependencies
Status: ?
□ Component interfaces are documented
Check: Inputs, outputs, dependencies clear for each component
Status: ?
□ Components are reusable and don't duplicate logic
Check: No component duplicates functionality from another
Status: ?
================================================================================
SECTION 10: GIT & VERSION CONTROL
================================================================================
□ .gitignore is comprehensive
File: .gitignore
Check: node_modules, .env, build artifacts, IDE files, OS files
Status: ?
□ git.instructions (Monica persona) is present and current
File: git.instructions
Status: ?
================================================================================
AUDIT RESULTS SUMMARY
================================================================================
Run this section AFTER checking everything above:
Total checks: 44
Passed: __
Failed: __
Warnings: __
Failed items (with violation status):
[list all FAILS with documented status]
Documented deviations (from DEVIATIONS.txt):
[list all accepted non-standard solutions]
Ready to commit: YES / NO
Reason: ___________________________________________________________
Audited by: Monica
Timestamp: [user fills in or automated]
================================================================================
+61
View File
@@ -0,0 +1,61 @@
================================================================================
PRISM NOTES - NON-STANDARD BUT ACCEPTED DEVIATIONS
Quick reference for intentional deviations from Monica's core standards
================================================================================
================================================================================
DEVIATION: Vanilla JavaScript Frontend + CDN Tailwind
================================================================================
Reasoning: Project initiated with quick frontend setup allowing rapid iteration
without TypeScript compilation or build tool overhead. Acceptable at prototype
stage; frontend logic primarily orchestration rather than complex business logic.
Affected Files: index.html (all inline <script> blocks, <script src=""> CDN links)
Current Scope: Screen capture tools, form submission, auth UI
Implications:
- Loss of type safety on frontend (no `.ts` checks, IDE autocomplete limited)
- Slower asset loading (no bundling, no minification)
- Harder to maintain as project scales (intermingled concerns)
- No separate build step (frontend compiled inline)
Standards Violated:
- LANGUAGE: TypeScript (Frontend & Backend) → Using vanilla JS instead
- STYLING: TailwindCSS + PostCSS → Using CDN instead of build pipeline
Adopted: 2026-01-14
Review Date: 2026-02-14
Triggers for Conversion:
- Frontend code exceeds 500 LOC
- Team grows beyond 1 person
- Screen capture feature stabilizes
- Build automation becomes necessary for other features
Reviewed: No
Reviewer: Monica
================================================================================
DEVIATION: Tool Files Use Vanilla JavaScript (.js)
================================================================================
Reasoning: Standalone utility tools (screen-capture, screen-record, user-alert)
designed as reusable browser-side modules. Vanilla JS allows direct browser
execution without build step. Tools are self-contained utilities, not critical
business logic. Conversion to TypeScript not necessary at current maturity.
Affected Files:
- /tools/screen-capture/index.js (246 LOC)
- /tools/screen-record/index.js (165 LOC)
- /tools/user-alert/index.js (196 LOC)
Implications:
- Loss of type safety on tool internals (no IDE autocomplete, no .ts checks)
- Tools cannot be used in strict TypeScript environments without @ts-ignore
- Maintenance slightly harder as tool complexity grows
Standards Violated:
- LANGUAGE: TypeScript (Frontend & Backend) → Using vanilla JS instead
Adopted: 2026-01-14
Review Date: 2026-03-01
Triggers for Conversion:
- Tools grow beyond 250 LOC each
- Tools need to interact with backend types
- Tool suite grows to >5 tools
- Performance optimization requires bundling
Reviewed: No
Reviewer: Monica
================================================================================
END OF DEVIATIONS LOG
================================================================================
+212 -16
View File
@@ -3,22 +3,140 @@ PRISM NOTES - SESSION LOG
Project history, decisions, and architectural milestones
================================================================================
[2026-01-10 20:45] MONICA SESSION: UI Refinements & Polish
- Changes Applied:
* Centered all main containers (body: flex items-center justify-center)
* Reduced card spacing dramatically for compact dense layout
- Grid gap: gap-5 → gap-2 (between cards)
- Card padding: p-5 sm:p-6 → px-2 py-0.5 (internal spacing)
- Internal spacing: space-y-3 → space-y-0 (elements in cards)
* Improved attachment indicator icons with outline strokes instead of fills
- Image: mountain/landscape icon (clearer than grid pattern)
- Video: play button with frame (recognizable)
- Audio: speaker icon (listening/playback, not microphone which is for recording)
- File: document with corner fold (standard file representation)
* Fixed mobile view icon visibility by increasing size to 24px and adding flex-shrink-0
* Replaced purple badges with subtle hover-on-gray style for icons
- Result: Cleaner, more compact UI with better visual hierarchy
- Status: Fixed and committed (a168fe2)
[2026-01-14 16:30] ARCHITECTURE DECISION: Migrate to Vite Frontend + Bun Backend
- Decision: Adopt Vite as frontend bundler to enforce TypeScript everywhere
- Reasoning: Current .js tool deviation and CDN Tailwind are pragmatically expensive
Triggers met: >5 tools, tools interact with PocketBase (type safety needed)
- Architecture (FINAL):
* Backend: Bun + Hono (server.ts remains unchanged)
* Frontend: TypeScript + Vite + PostCSS Tailwind
* Tools: Convert to .ts, compiled by Vite bundler
* Package Manager: Bun exclusively (one bun.lock, no npm/pnpm conflicts)
- Benefits:
* Type safety everywhere (no vanilla JS)
* HMR in dev (fast reloads <100ms)
* Optimized bundles (minified, tree-shaken)
* Tools scale cleanly as .ts modules
* Removes CDN Tailwind and .js tool deviations
- Implementation Plan (Next Session):
1. Setup Vite config + package.json scripts (~15 mins)
2. Convert index.html to Vite entry point (~30 mins)
3. Extract inline scripts to .ts modules (~1 hour)
4. Convert all tools to .ts: screen-capture, screen-record, user-alert, user-email-lookup (~2 hours)
5. Configure Tailwind with PostCSS for Vite (~15 mins)
6. Update Monica persona (Bun backend + Vite frontend standard) (~20 mins)
7. Clean DEVIATIONS.txt (remove CDN Tailwind, .js tools entries) (~10 mins)
8. Test build pipeline and dev experience (~30 mins)
Total: ~4-5 hours
- Current State:
* User email lookup component created and integrated (NOT committed yet)
* All other work committed and synced
* Ready for Vite migration on next session
- Notes for Next Session:
* Start with: `git status` to see uncommitted email-lookup work
* Decide: Include email-lookup work in Vite migration OR commit first
* Vite setup: Use `npm init vite@latest` as template, adapt to Bun
* Key files to create: vite.config.ts, tailwind.config.js, postcss.config.js
* Key files to convert: index.html → src/main.ts, all tools to .ts
* Build commands: "build": "vite build", "dev": "bun run build && PORT=3030 bun run --watch server.ts"
[2026-01-14 16:00] MONICA SESSION: User Email Lookup Feature - New Standalone Component
- Feature Request: Add hover tooltips showing email addresses when hovering over names in sharing UI
- Architecture Decision: Created as standalone component in /tools/user-email-lookup/
Reasoning: Self-contained, reusable utility with single responsibility (email lookup)
- Implementation:
* Created /tools/user-email-lookup/index.js (window.setupUserEmailLookup API)
* Created /tools/user-email-lookup/README.md (complete documentation)
* Integrated into index.html noteForm and conversationForm
* Uses existing Associations collection for first_name → email lookups
* Implements email caching to minimize PocketBase queries
* Uses MutationObserver for dynamic capsule detection
* Shows email in native browser tooltip on hover
- Features:
* On note creation, user types names (e.g., "John, Sarah, Mike")
* Each name creates a capsule in selectedUsersContainer
* Hovering over capsule shows email (first hover queries DB, subsequent cached)
* Cache persists for page session, can be cleared via window.clearEmailLookupCache()
- Integration Points:
* Script loaded: <script src="tools/user-email-lookup/index.js"></script>
* Initialized: showNoteForm() and showConversationForm()
* Container: #selectedUsersContainer (for both notes and conversations)
- Status: Implemented and integrated, NOT YET COMMITTED
- Note: Will be converted to .ts as part of Vite migration (next session)
[2026-01-14 15:45] MONICA SESSION: Standards Alignment Fixes
- Feature Request: Add hover tooltips showing email addresses when hovering over names in sharing UI
- Architecture Decision: Created as standalone component in /tools/user-email-lookup/
Reasoning: Self-contained, reusable utility with single responsibility (email lookup)
- Implementation:
* Created /tools/user-email-lookup/index.js (window.setupUserEmailLookup API)
* Created /tools/user-email-lookup/README.md (complete documentation)
* Integrated into index.html noteForm and conversationForm
* Uses existing Associations collection for first_name → email lookups
* Implements email caching to minimize PocketBase queries
* Uses MutationObserver for dynamic capsule detection
* Shows email in native browser tooltip on hover
- Features:
* On note creation, user types names (e.g., "John, Sarah, Mike")
* Each name creates a capsule in selectedUsersContainer
* Hovering over capsule shows email (first hover queries DB, subsequent cached)
* Cache persists for page session, can be cleared via window.clearEmailLookupCache()
- Integration Points:
* Script loaded: <script src="tools/user-email-lookup/index.js"></script>
* Initialized: showNoteForm() and showConversationForm()
* Container: #selectedUsersContainer (for both notes and conversations)
- Status: Implemented and ready for testing
- Next: Test with real Associations data and refine if needed
[2026-01-14 15:45] MONICA SESSION: Standards Alignment Fixes
- Action: Addressed audit findings - minor alignment issues
- Fixes Applied:
* Added DEVIATION entry for tool .js files (screen-capture, screen-record, user-alert)
Reasoning: Standalone utilities with direct browser execution, not business logic
Review date: 2026-03-01
* Updated README.md: Fixed port references (5500 → 3030)
Enforced port 3030 explicitly in documentation
* Created tools/user-alert/README.md (was missing, had only index.js)
Documentation: Features, usage, API, PocketBase schema, troubleshooting
* Enhanced tool file comments:
- screen-record/index.js: Added detailed @description, @usage, @dependencies, @integration
- user-alert/index.js: Added detailed @description, @usage, @dependencies, @integration, @gotchas
* Maintained screen-capture README.md (already present)
- Standards Violations Documented:
* CDN Tailwind + Vanilla JS frontend → DEVIATIONS.txt (review 2026-02-14)
* Tool files in .js → DEVIATIONS.txt (review 2026-03-01)
- Audit Status: All minor issues resolved
- Next: Frontend TypeScript + Bun build conversion deferred per user guidance
[2026-01-14 15:15] MONICA SESSION: Audit & Standards Enforcement Setup
- Action: Established formal audit process and standards compliance tracking
- Created logs/AUDIT_CHECKLIST.txt with 44 systematic compliance checks
- Created logs/DEVIATIONS.txt for quick-reference of non-standard but accepted deviations
- Updated git.instructions with:
* RULE 8: User-controlled commits and syncs (no unprompted operations)
* AUDIT PROCESS section: Mandatory at project start and before commits
* COMPONENT-FIRST ARCHITECTURE: Evaluates new features for standalone component status
- Audit Result: 40 passes, 2 failures (both documented), 1 warning
- Findings:
* package-lock.json present (npm violation of Bun-exclusive standard) — REMOVED
* Vanilla JS frontend (documented deviation) — ACCEPTED
* Tailwind CDN (documented deviation) — ACCEPTED
- Status: STANDARDS COMPLIANT after removal of npm lock file
[2026-01-11 TODO] Screen Capture Window Visibility Issue - INVESTIGATE LATER
- Issue: Prism Notes window still appears in getDisplayMedia() dialog options
even though visibility is hidden before dialog opens
- Attempted Fix: Used visibility:hidden on app elements before getDisplayMedia()
- Result: Not working - window still shows in screen selector
- Root Cause: Unclear - may be timing issue or browser caching of window list
- Workaround: User can manually select other screen/window instead
- Investigation Needed:
* Try alternative hiding methods (display:none, opacity:0, positioning off-screen)
* Check if timing delay needed before getDisplayMedia()
* Verify with different browsers (Chrome vs Edge vs Firefox)
* Consider if OS-level window management interferes
- Impact: Minor UX issue - doesn't prevent functionality, just less elegant
- Priority: LOW - defer until screen capture feature stabilized otherwise
[2026-01-10 20:10] MONICA SESSION: Fixed Container Injection Bug
- Issue: Refactor commit (cee9157) had CONTAINERS + initializeContainers() code
@@ -49,4 +167,82 @@ Project history, decisions, and architectural milestones
becomes more established and needs full file separation
- Status: Functional and ready for independent container updates
[2026-01-15 INVESTIGATION] PocketBase Associations Collection API Rules
- Problem: User email lookup queries Associations → "only superusers can perform this action"
- Root Cause: Associations collection LIST API rule is restricted (default deny all)
- Solution: Set Associations LIST API rule to `@request.auth.id != ""` (allow authenticated users)
- Forum Source: https://github.com/pocketbase/pocketbase/discussions/5948#5948-answer
* Official pattern for "authenticate first" checks
* Works for all read/write operations
* Syntax: `@request.auth.id != ""` (empty string = anonymous/unauthenticated)
- Implementation (Manual in PocketBase Admin):
1. Go to PocketBase Admin Console
2. Select Associations collection
3. API Rules tab → List action
4. Set rule to: `@request.auth.id != ""`
5. Save and test with authenticated user
- Why UserAlertSystem Works: Unknown at this point (may have different API rule set)
- Status: Solution identified, requires manual PocketBase admin console update
[2026-01-16 FEATURE] Note Creation Alerts - Notify Shared Users
- Problem: When a note is created and shared with users, those users should receive alerts
- Solution: Use existing UserAlertSystem realtime monitoring
- How it works:
* When note is created, it's published with `shared: true` and `shared_with: [names]`
* UserAlertSystem monitors Notes collection in realtime via PocketBase
* When new note event arrives, system checks if current user is in shared_with
* If match, plays notification sound automatically
- No code changes needed to submitNote - just ensure:
* Note is created with `shared: true` and `shared_with` populated (already done)
* UserAlertSystem is running in user's browser (via index.html)
* Realtime subscription is active (UserAlertSystem handles this)
- Benefits:
* No database records needed for alerts
* Real-time notification (minimal latency)
* Works for self-sharing (user shares note with self)
- Status: Simplified approach - removed unnecessary Alerts collection code
[2026-01-16 SESSION] FEATURE: Hidden Notes Management
- Feature Request: Create ability to hide notes from main list, view hidden notes, and unhide them
- Architecture Decision: Simple boolean flag (hidden) on Notes collection, dedicated view container
- Implementation:
* Added "Hidden Notes" button (id="hiddenNotesBtn", gray-600 style) to notes header
* Created hiddenNotesContainer (gray-200 bg, similar layout to notes list)
* Added hide button (👁️‍🗨️ icon) to each note card in main list
- Click handler: Update note.hidden=true, reload list
- Styled: Gray text, hover red, positioned right of title
* Added unhide button (👁️ icon) to each hidden note in hidden view
- Click handler: Update note.hidden=false, reload both hidden notes and main list
- Styled: Gray text, hover green, positioned right of title
* Created renderHiddenNotes() function
- Filters notesList for n.hidden === true
- Renders cards similar to main list but with unhide buttons
- Shows empty state "No hidden notes." if none exist
* Created showHiddenNotes() view switcher
- Hides all other containers, shows hiddenNotesContainer
- Calls renderHiddenNotes() to populate
* Updated showNotesList() to hide hiddenNotesContainer
* Wired button click handlers:
- hiddenNotesBtn → showHiddenNotes()
- backToNotesFromHiddenBtn → showNotesList()
* Updated applySearch() to filter hidden notes from main view (filter: !n.hidden)
- Features:
* Hide any note from main list with single click
* Dedicated gray-themed view for organizing hidden notes
* Unhide with single click - returns to main list automatically
* Maintains existing styling: light blue for shared-with-me notes, green for shared notes
* No database schema changes needed - uses existing boolean field
- Status: Implemented and tested, NOT YET COMMITTED (awaiting user instruction)
- Database Schema: Notes.hidden (boolean, default false)
* No migration needed (handled via PocketBase UI or default values)
* Existing notes will have hidden=false implicitly
- UI State Flow:
1. Main list: Shows all notes where !hidden, with hide button on each
2. Hidden view: Shows all notes where hidden=true, with unhide button on each
3. Clicking hide: Updates note, reloads, stays in main view
4. Clicking unhide: Updates note, reloads both views, returns to main
5. Search filtering: applySearch() respects hidden flag
- Next: Await user confirmation to commit
================================================================================
+1109
View File
File diff suppressed because it is too large Load Diff
+3408
View File
File diff suppressed because it is too large Load Diff
-257
View File
@@ -1,257 +0,0 @@
{
"name": "prism-notes",
"version": "1.0.0-alpha3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prism-notes",
"version": "1.0.0-alpha3",
"dependencies": {
"@azure/msal-node": "^2.6.7",
"@microsoft/microsoft-graph-client": "^3.0.7",
"dotenv": "^16.4.5",
"hono": "^4.10.8",
"pocketbase": "^0.26.5"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "latest"
}
},
"node_modules/@azure/msal-common": {
"version": "14.16.1",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node": {
"version": "2.16.3",
"license": "MIT",
"dependencies": {
"@azure/msal-common": "14.16.1",
"jsonwebtoken": "^9.0.0",
"uuid": "^8.3.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@microsoft/microsoft-graph-client": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-3.0.7.tgz",
"integrity": "sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependenciesMeta": {
"@azure/identity": {
"optional": true
},
"@azure/msal-browser": {
"optional": true
},
"buffer": {
"optional": true
},
"stream-browserify": {
"optional": true
}
}
},
"node_modules/@types/bun": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.5.tgz",
"integrity": "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==",
"dev": true,
"license": "MIT",
"dependencies": {
"bun-types": "1.3.5"
}
},
"node_modules/@types/node": {
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"license": "BSD-3-Clause"
},
"node_modules/bun-types": {
"version": "1.3.5",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/hono": {
"version": "4.11.1",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"license": "MIT"
},
"node_modules/pocketbase": {
"version": "0.26.5",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.3",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/uuid": {
"version": "8.3.2",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
}
}
}
+1
View File
@@ -8,6 +8,7 @@
"start": "bun run server.ts"
},
"dependencies": {
"@azure/msal-browser": "^5.6.1",
"@azure/msal-node": "^2.6.7",
"@microsoft/microsoft-graph-client": "^3.0.7",
"dotenv": "^16.4.5",
+1380 -79
View File
File diff suppressed because it is too large Load Diff
+500
View File
@@ -0,0 +1,500 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Prism Notes Tasks</title>
<link rel="icon" type="image/png" href="/images/prism.png" />
<script src="https://cdn.tailwindcss.com"></script>
<style>
.panel {
border-radius: 1rem;
border: 1px solid rgb(30 41 59 / 1);
background: rgb(15 23 42 / 0.78);
backdrop-filter: blur(8px);
padding: 1.25rem;
}
.btn {
border-radius: 0.75rem;
padding: 0.55rem 1rem;
font-weight: 600;
transition: all 160ms ease;
position: relative;
overflow: hidden;
}
.btn:focus-visible,
.field:focus-visible {
outline: 2px solid rgb(34 211 238 / 0.8);
outline-offset: 1px;
}
.btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(120deg, transparent 0%, rgb(255 255 255 / 0.2) 48%, transparent 100%);
transform: translateX(-130%);
transition: transform 260ms ease;
pointer-events: none;
}
.btn:hover {
transform: translateY(-1px) scale(1.01);
box-shadow: 0 10px 22px rgb(15 23 42 / 0.28);
}
.btn:hover::before { transform: translateX(130%); }
.btn-accent-cyan {
color: rgb(8 47 73 / 1);
background: rgb(6 182 212 / 1);
}
.btn-accent-emerald {
color: rgb(6 44 32 / 1);
background: rgb(34 197 94 / 1);
}
.btn-secondary {
border: 1px solid rgb(51 65 85 / 1);
background: rgb(15 23 42 / 1);
color: rgb(226 232 240 / 1);
}
.btn-secondary:hover { border-color: rgb(100 116 139 / 1); }
.field {
width: 100%;
border-radius: 0.75rem;
border: 1px solid rgb(51 65 85 / 1);
background: rgb(2 6 23 / 1);
padding: 0.55rem 0.75rem;
font-size: 0.875rem;
color: rgb(248 250 252 / 1);
}
.field option {
background: rgb(15 23 42 / 0.96);
color: rgb(248 250 252 / 1);
}
.field::placeholder { color: rgb(100 116 139 / 1); }
.mono-pill {
border-radius: 0.5rem;
border: 1px solid rgb(51 65 85 / 1);
background: rgb(2 6 23 / 1);
padding: 0.15rem 0.5rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.75rem;
color: rgb(191 219 254 / 1);
white-space: nowrap;
}
.task-cell {
border-top: 1px solid rgb(30 41 59 / 1);
padding: 0.65rem 0.5rem;
vertical-align: top;
font-size: 0.8rem;
}
</style>
</head>
<body class="min-h-screen bg-gradient-to-b from-slate-950 via-slate-950 to-slate-900 text-slate-100">
<main class="mx-auto max-w-7xl px-6 py-10">
<header class="panel mb-6">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-4">
<img src="/images/prism.png" alt="Prism Notes" class="h-14 w-14 rounded-xl object-cover ring-1 ring-slate-600/60" />
<div>
<p class="text-xs uppercase tracking-[0.3em] text-cyan-400">Prism Notes</p>
<h1 class="mt-1 text-3xl font-semibold tracking-tight">Tasks View</h1>
</div>
</div>
<div class="flex flex-wrap gap-3">
<a href="/notes-workspace" class="btn btn-secondary">Open Notes Workspace</a>
<a href="/notes" class="btn btn-secondary">Open Legacy Notes</a>
<a href="/" class="btn btn-secondary">Back to Testing View</a>
</div>
</div>
<p class="mt-3 text-sm text-slate-300">Browse PocketBase <span class="mono-pill">Tasgird</span> tasks. Default filter is current user, with optional user switching.</p>
</header>
<section class="mb-6 grid gap-4 md:grid-cols-3">
<article class="panel">
<p class="text-xs uppercase tracking-wider text-slate-400">Server</p>
<p id="healthStatus" class="mt-2 text-sm font-medium">Checking...</p>
</article>
<article class="panel">
<p class="text-xs uppercase tracking-wider text-slate-400">PocketBase User</p>
<p id="pbStatus" class="mt-2 text-sm font-medium">Not logged in</p>
</article>
<article class="panel">
<p class="text-xs uppercase tracking-wider text-slate-400">Task Count</p>
<p id="taskCount" class="mt-2 text-sm font-medium">0</p>
</article>
</section>
<section class="mb-6 grid gap-6 lg:grid-cols-2">
<article class="panel p-6">
<h2 class="text-xl font-medium">1) PocketBase Login</h2>
<p class="mt-2 text-sm text-slate-300">Uses your existing PocketBase Microsoft OAuth provider.</p>
<div class="mt-4 flex flex-wrap gap-3">
<button id="loginBtn" class="btn btn-accent-cyan">Login with Microsoft</button>
<button id="logoutBtn" class="btn btn-secondary">Logout</button>
</div>
</article>
<article class="panel p-6">
<h2 class="text-xl font-medium">2) Task Filter</h2>
<p class="mt-2 text-sm text-slate-300">Defaults to your tasks, but you can select other users.</p>
<div class="mt-4 space-y-4">
<div>
<label class="text-sm font-medium text-slate-300">User</label>
<select id="taskUserSelect" class="field mt-1">
<option value="">— login to load users —</option>
</select>
</div>
<div class="flex flex-wrap gap-3">
<button id="loadTasksBtn" class="btn btn-accent-emerald">Load Tasks</button>
<button id="refreshUsersBtn" class="btn btn-secondary">Refresh Users</button>
</div>
</div>
</article>
</section>
<section class="panel p-6">
<h2 class="text-xl font-medium">Tasks (Tasgird)</h2>
<div class="mt-4 overflow-x-auto">
<table class="w-full min-w-[1300px] border-collapse">
<thead>
<tr class="text-left text-xs uppercase tracking-wider text-slate-400">
<th class="task-cell">id</th>
<th class="task-cell">user</th>
<th class="task-cell">title</th>
<th class="task-cell">startDate (UTC)</th>
<th class="task-cell">dueDate (UTC)</th>
<th class="task-cell">priority</th>
<th class="task-cell">completed</th>
<th class="task-cell">content</th>
<th class="task-cell">size</th>
<th class="task-cell">status</th>
</tr>
</thead>
<tbody id="tasksBody">
<tr><td class="task-cell text-slate-400" colspan="10">No tasks loaded.</td></tr>
</tbody>
</table>
</div>
</section>
<section class="panel p-6 mt-6">
<h2 class="text-xl font-medium">Output</h2>
<pre id="output" class="mt-4 min-h-[180px] overflow-auto rounded-xl border border-slate-800 bg-slate-950 p-4 text-xs leading-5 text-slate-200"></pre>
</section>
</main>
<script type="module">
import PocketBase from 'https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/+esm';
const PB_DEFAULT_AUTH_COLLECTION = 'Users';
const PB_DEFAULT_OAUTH_PROVIDER = 'microsoft';
let pb = null;
let pbConfigPromise = null;
let usersCache = [];
const output = document.getElementById('output');
const healthStatus = document.getElementById('healthStatus');
const pbStatus = document.getElementById('pbStatus');
const taskCount = document.getElementById('taskCount');
const taskUserSelect = document.getElementById('taskUserSelect');
const tasksBody = document.getElementById('tasksBody');
function writeOutput(value) {
output.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
}
function toUtcText(value) {
if (!value) return '';
const d = new Date(value);
if (Number.isNaN(d.getTime())) return String(value);
return d.toISOString();
}
function stripHtml(value) {
if (!value) return '';
const div = document.createElement('div');
div.innerHTML = String(value);
return (div.textContent || div.innerText || '').trim();
}
async function getPocketBaseConfig() {
if (!pbConfigPromise) {
pbConfigPromise = fetch('/api/auth/pocketbase-config')
.then(async (resp) => {
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.success || !data?.pbUrl) {
throw new Error(data?.message || 'PocketBase config unavailable');
}
return {
pbUrl: data.pbUrl,
collection: data.collection || PB_DEFAULT_AUTH_COLLECTION,
provider: data.provider || PB_DEFAULT_OAUTH_PROVIDER,
};
})
.catch((error) => {
pbConfigPromise = null;
throw error;
});
}
return pbConfigPromise;
}
async function ensurePocketBaseClient() {
if (pb) return pb;
const config = await getPocketBaseConfig();
pb = new PocketBase(config.pbUrl);
return pb;
}
async function updatePbStatus() {
const client = await ensurePocketBaseClient();
if (client.authStore?.isValid) {
const email = client.authStore.model?.email || '(unknown user)';
pbStatus.textContent = `Logged in: ${email}`;
} else {
pbStatus.textContent = 'Not logged in';
}
}
async function loadHealth() {
try {
const resp = await fetch('/health');
const data = await resp.json().catch(() => ({}));
healthStatus.textContent = resp.ok ? `OK (${data.pbDB || 'configured'})` : 'Unavailable';
} catch {
healthStatus.textContent = 'Unavailable';
}
}
async function loginPocketBase() {
writeOutput('Opening Microsoft login...');
const client = await ensurePocketBaseClient();
const config = await getPocketBaseConfig();
const authData = await client.collection(config.collection).authWithOAuth2({
provider: config.provider,
urlCallback(url) {
const w = Math.min(900, window.screen.availWidth || 900);
const h = Math.min(680, window.screen.availHeight || 680);
const left = Math.floor(((window.screen.availWidth || 1280) - w) / 2);
const top = Math.floor(((window.screen.availHeight || 800) - h) / 2);
window.open(url, 'pb_oauth', `width=${w},height=${h},top=${top},left=${left},resizable=yes,menubar=no`);
},
});
await updatePbStatus();
await loadUsers();
writeOutput({ message: 'PocketBase login completed.', user: authData?.record?.email || client.authStore.model?.email || null });
}
async function logoutPocketBase() {
const client = await ensurePocketBaseClient();
client.authStore.clear();
usersCache = [];
taskUserSelect.innerHTML = '<option value="">— login to load users —</option>';
tasksBody.innerHTML = '<tr><td class="task-cell text-slate-400" colspan="10">No tasks loaded.</td></tr>';
taskCount.textContent = '0';
await updatePbStatus();
writeOutput({ message: 'PocketBase session cleared.' });
}
function getCurrentToken() {
if (!pb?.authStore?.isValid || !pb?.authStore?.token) {
throw new Error('PocketBase login required');
}
return pb.authStore.token;
}
async function validateCurrentToken() {
await ensurePocketBaseClient();
const token = getCurrentToken();
const resp = await fetch('/api/auth/validate-pb-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pbToken: token }),
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || data?.valid !== true) {
const details = data?.details
? (typeof data.details === 'string' ? data.details : JSON.stringify(data.details))
: '';
const context = data?.tokenContext ? ` tokenContext=${JSON.stringify(data.tokenContext)}` : '';
throw new Error(details
? `${data?.error || 'PocketBase token invalid'}: ${details}${context}`
: `${data?.error || 'PocketBase token invalid'}${context}`);
}
return data;
}
async function loadUsers() {
await ensurePocketBaseClient();
const token = getCurrentToken();
const resp = await fetch('/api/tasks/users', {
headers: {
'x-pb-token': token,
'Authorization': `Bearer ${token}`,
},
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.success) {
const details = data?.details
? (typeof data.details === 'string' ? data.details : JSON.stringify(data.details))
: '';
const context = data?.tokenContext ? ` tokenContext=${JSON.stringify(data.tokenContext)}` : '';
throw new Error(details
? `${data?.message || 'Failed to load users'}: ${details}${context}`
: `${data?.message || 'Failed to load users'}${context}`);
}
usersCache = Array.isArray(data.users) ? data.users : [];
const meId = String(data?.me?.id || '').trim();
const meName = data?.me?.name || data?.me?.email || 'Me';
const meLookupName = String(data?.me?.lookupName || data?.me?.name || '').trim();
taskUserSelect.innerHTML = '';
for (const user of usersCache) {
const opt = document.createElement('option');
const lookupName = String(user.lookupName || user.name || '').trim();
if (!lookupName) {
continue;
}
opt.value = lookupName;
opt.dataset.userId = user.id;
opt.textContent = `${user.name || user.email || user.id}${user.id === meId ? ' (Me)' : ''}`;
taskUserSelect.appendChild(opt);
}
if (meLookupName) {
taskUserSelect.value = meLookupName;
}
writeOutput({ message: `Loaded ${usersCache.length} users`, me: { id: meId, name: meName, lookupName: meLookupName } });
if (data?.warning) {
writeOutput({ message: `Loaded ${usersCache.length} users`, warning: data.warning, me: { id: meId, name: meName, lookupName: meLookupName } });
}
}
function renderTasks(tasks) {
const rows = Array.isArray(tasks) ? tasks : [];
taskCount.textContent = String(rows.length);
if (!rows.length) {
tasksBody.innerHTML = '<tr><td class="task-cell text-slate-400" colspan="10">No tasks found for selected user.</td></tr>';
return;
}
tasksBody.innerHTML = '';
for (const task of rows) {
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="task-cell"><span class="mono-pill">${task.id || ''}</span></td>
<td class="task-cell">${task.userDisplay || task.user || ''}</td>
<td class="task-cell">${task.title || ''}</td>
<td class="task-cell">${toUtcText(task.startDate) || ''}</td>
<td class="task-cell">${toUtcText(task.dueDate) || ''}</td>
<td class="task-cell">${task.priority ?? ''}</td>
<td class="task-cell">${task.completed ? 'true' : 'false'}</td>
<td class="task-cell">${stripHtml(task.content || '').slice(0, 240)}</td>
<td class="task-cell">${task.size ?? ''}</td>
<td class="task-cell">${task.status ?? ''}</td>
`;
tasksBody.appendChild(tr);
}
}
async function loadTasks() {
await ensurePocketBaseClient();
const token = getCurrentToken();
const userName = String(taskUserSelect.value || '').trim();
const selectedOption = taskUserSelect.selectedOptions?.[0] || null;
const userId = String(selectedOption?.dataset?.userId || '').trim();
const query = new URLSearchParams();
if (userName) query.set('userName', userName);
if (userId) query.set('userId', userId);
const url = query.toString()
? `/api/tasks/list?${query.toString()}`
: '/api/tasks/list';
const resp = await fetch(url, {
headers: {
'x-pb-token': token,
'Authorization': `Bearer ${token}`,
},
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.success) {
const details = data?.details
? (typeof data.details === 'string' ? data.details : JSON.stringify(data.details))
: '';
const context = data?.tokenContext ? ` tokenContext=${JSON.stringify(data.tokenContext)}` : '';
throw new Error(details
? `${data?.message || 'Failed to load tasks'}: ${details}${context}`
: `${data?.message || 'Failed to load tasks'}${context}`);
}
if (data?.userName && taskUserSelect.value !== data.userName) {
taskUserSelect.value = data.userName;
}
renderTasks(data.tasks || []);
writeOutput({
message: `Loaded ${data?.count ?? 0} task(s)`,
userName: data?.userName || userName,
userId: data?.userId || undefined,
authUser: data?.authUser || undefined,
tokenContext: data?.tokenContext || undefined,
lookupMethod: data?.lookupMethod || undefined,
warning: data?.warning || undefined,
tasks: data.tasks || [],
});
}
async function bootstrapTasksPage() {
try {
const client = await ensurePocketBaseClient();
const config = await getPocketBaseConfig();
if (client.authStore?.token) {
try {
await client.collection(config.collection).authRefresh();
} catch {
client.authStore.clear();
writeOutput('Existing PocketBase session could not refresh for Users collection. Please login again.');
}
}
await updatePbStatus();
if (client.authStore?.isValid) {
const tokenValidation = await validateCurrentToken();
writeOutput({ message: 'PocketBase token validated', user: tokenValidation?.user || null, tokenContext: tokenValidation?.tokenContext || undefined });
await loadUsers();
await loadTasks();
}
} catch (error) {
writeOutput({ error: error?.message || String(error) });
}
}
function bind(id, handler) {
document.getElementById(id).addEventListener('click', async () => {
try {
await handler();
} catch (error) {
writeOutput({ error: error?.message || String(error) });
}
});
}
bind('loginBtn', loginPocketBase);
bind('logoutBtn', logoutPocketBase);
bind('refreshUsersBtn', loadUsers);
bind('loadTasksBtn', loadTasks);
taskUserSelect.addEventListener('change', async () => {
try {
await loadTasks();
} catch (error) {
writeOutput({ error: error?.message || String(error) });
}
});
bootstrapTasksPage();
loadHealth();
</script>
</body>
</html>
+147
View File
@@ -0,0 +1,147 @@
# Screen Capture Tool
A standalone utility for capturing screenshots from user's screen using the native Browser Screen Capture API.
## Features
- 📸 Capture screenshots from user-selected screen/window
- 🎯 No external dependencies
- ⚡ Returns canvas for flexible post-processing
- 🛡️ Graceful error handling
- ♿ Respects user permissions
## Installation
Copy the `screen-capture` folder to your project:
```
your-project/
├── tools/
│ └── screen-capture/
│ ├── index.js
│ └── README.md
```
## Usage
### Basic Screenshot Capture
```javascript
import { captureScreenshot } from './tools/screen-capture/index.js';
const canvas = await captureScreenshot();
if (canvas) {
// Canvas is ready for use
// Convert to File, display, or manipulate further
}
```
### Convert to File
```javascript
import { captureScreenshot, canvasToFile } from './tools/screen-capture/index.js';
const canvas = await captureScreenshot();
if (canvas) {
const file = await canvasToFile(canvas, 'my-screenshot.png');
// Use file with FormData, attachments, etc.
}
```
### One-liner with Blob
```javascript
const canvas = await captureScreenshot();
canvas?.toBlob((blob) => {
const file = new File([blob], `screenshot-${Date.now()}.png`, { type: 'image/png' });
// Use file...
});
```
## API Reference
### `captureScreenshot()`
**Returns:** `Promise<Canvas | null>`
Opens native browser dialog for user to select screen/window to capture. Returns a canvas element containing the screenshot, or null if user cancels or error occurs.
**Example:**
```javascript
const canvas = await captureScreenshot();
```
### `canvasToFile(canvas, filename?)`
**Parameters:**
- `canvas` (Canvas): Canvas element to convert
- `filename` (string, optional): Custom filename. Defaults to `screenshot-{timestamp}.png`
**Returns:** `Promise<File>`
Converts canvas to PNG File object for easy handling in file uploads or attachments.
**Example:**
```javascript
const file = await canvasToFile(canvas, 'custom-name.png');
```
## Browser Support
Requires browsers supporting the Screen Capture API:
- Chrome 72+
- Edge 79+
- Firefox 66+
- Safari 13+
Note: User must grant permission for each capture request.
## Error Handling
The tool gracefully handles:
- User cancellation of the capture dialog
- Browser permission denial
- Browser incompatibility
- Missing canvas/video support
All errors are logged to console and return null.
## Examples
### React Component
```jsx
import { captureScreenshot, canvasToFile } from './tools/screen-capture/index.js';
export function ScreenshotButton() {
const handleCapture = async () => {
const canvas = await captureScreenshot();
if (canvas) {
const file = await canvasToFile(canvas);
console.log('Screenshot captured:', file.name);
}
};
return <button onClick={handleCapture}>Take Screenshot</button>;
}
```
### With Form Submission
```javascript
import { captureScreenshot, canvasToFile } from './tools/screen-capture/index.js';
const canvas = await captureScreenshot();
if (canvas) {
const file = await canvasToFile(canvas);
const formData = new FormData();
formData.append('screenshot', file);
await fetch('/api/upload', { method: 'POST', body: formData });
}
```
## License
MIT - Use freely in your projects
+245
View File
@@ -0,0 +1,245 @@
/**
* Screen Capture Tool
* Captures a screenshot from the user's screen using the Screen Capture API
*
* @module ScreenCapture
*/
(function() {
console.log('Starting Screen Capture Tool initialization...');
window.captureScreenshot = async function() {
console.log('captureScreenshot called');
try {
// Request screen capture from user
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' }
});
// Create video element to capture frame
const video = document.createElement('video');
video.srcObject = mediaStream;
return new Promise((resolve) => {
video.onloadedmetadata = () => {
// Create canvas and draw video frame
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
// Stop all media tracks
mediaStream.getTracks().forEach(track => track.stop());
// Return canvas for further processing
resolve(canvas);
};
video.play();
});
} catch (error) {
console.error('Screenshot capture failed:', error);
return null;
}
};
window.captureSnip = async function() {
console.log('captureSnip called');
try {
// First, get the full screen capture
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' }
});
// Create video element to capture frame
const video = document.createElement('video');
video.srcObject = mediaStream;
return new Promise((resolve) => {
video.onloadedmetadata = () => {
// Capture full screen to canvas
const fullCanvas = document.createElement('canvas');
fullCanvas.width = video.videoWidth;
fullCanvas.height = video.videoHeight;
const fullCtx = fullCanvas.getContext('2d');
fullCtx.drawImage(video, 0, 0);
// Stop video tracks
mediaStream.getTracks().forEach(track => track.stop());
// Show selection overlay
createSelectionOverlay(fullCanvas, resolve);
};
video.play();
});
} catch (error) {
console.error('Snip capture failed:', error);
return null;
}
};
function createSelectionOverlay(fullCanvas, resolve) {
console.log('createSelectionOverlay called');
// Create overlay container
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
cursor: crosshair;
z-index: 99999;
user-select: none;
`;
// Create canvas for drawing selection box
const selectionCanvas = document.createElement('canvas');
selectionCanvas.style.cssText = `
position: fixed;
top: 0;
left: 0;
cursor: crosshair;
z-index: 100000;
`;
selectionCanvas.width = window.innerWidth;
selectionCanvas.height = window.innerHeight;
// Scale factor for mapping screen pixels to window pixels
const scaleX = fullCanvas.width / window.innerWidth;
const scaleY = fullCanvas.height / window.innerHeight;
let isDrawing = false;
let startX = 0;
let startY = 0;
let endX = 0;
let endY = 0;
const ctx = selectionCanvas.getContext('2d');
function drawSelection() {
ctx.clearRect(0, 0, selectionCanvas.width, selectionCanvas.height);
if (!isDrawing && (startX === endX && startY === endY)) {
return;
}
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
const width = Math.abs(endX - startX);
const height = Math.abs(endY - startY);
// Draw selection rectangle
ctx.strokeStyle = '#0EA5E9';
ctx.lineWidth = 2;
ctx.strokeRect(x, y, width, height);
// Fill with semi-transparent color
ctx.fillStyle = 'rgba(14, 165, 233, 0.1)';
ctx.fillRect(x, y, width, height);
// Draw corner handles
const handleSize = 8;
ctx.fillStyle = '#0EA5E9';
ctx.fillRect(x - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
ctx.fillRect(x + width - handleSize / 2, y - handleSize / 2, handleSize, handleSize);
ctx.fillRect(x - handleSize / 2, y + height - handleSize / 2, handleSize, handleSize);
ctx.fillRect(x + width - handleSize / 2, y + height - handleSize / 2, handleSize, handleSize);
}
selectionCanvas.addEventListener('mousedown', (e) => {
isDrawing = true;
startX = e.clientX;
startY = e.clientY;
endX = startX;
endY = startY;
});
selectionCanvas.addEventListener('mousemove', (e) => {
if (isDrawing) {
endX = e.clientX;
endY = e.clientY;
drawSelection();
}
});
selectionCanvas.addEventListener('mouseup', () => {
if (isDrawing) {
isDrawing = false;
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
const width = Math.abs(endX - startX);
const height = Math.abs(endY - startY);
// Validate selection
if (width < 10 || height < 10) {
console.warn('Selection too small');
cleanup();
resolve(null);
return;
}
// Create cropped canvas from full screenshot
const croppedCanvas = document.createElement('canvas');
croppedCanvas.width = width * scaleX;
croppedCanvas.height = height * scaleY;
const croppedCtx = croppedCanvas.getContext('2d');
// Draw cropped section from full canvas
croppedCtx.drawImage(
fullCanvas,
x * scaleX,
y * scaleY,
width * scaleX,
height * scaleY,
0,
0,
width * scaleX,
height * scaleY
);
cleanup();
resolve(croppedCanvas);
}
});
// Handle escape key
const handleKeydown = (e) => {
if (e.key === 'Escape') {
cleanup();
resolve(null);
}
};
function cleanup() {
document.removeEventListener('keydown', handleKeydown);
overlay.remove();
selectionCanvas.remove();
}
document.addEventListener('keydown', handleKeydown);
document.body.appendChild(overlay);
document.body.appendChild(selectionCanvas);
console.log('Snipping tool active: Draw a rectangle to select area. Press ESC to cancel.');
}
window.canvasToFile = async function(canvas, filename = null) {
console.log('canvasToFile called');
return new Promise((resolve) => {
const name = filename || `screenshot-${Date.now()}.png`;
canvas.toBlob((blob) => {
const file = new File([blob], name, { type: 'image/png' });
resolve(file);
});
});
};
console.log('✓ Screen Capture Tool loaded successfully');
console.log('Functions available: window.captureScreenshot, window.captureSnip, window.canvasToFile');
})();
+219
View File
@@ -0,0 +1,219 @@
# Screen Record Tool
A standalone utility for recording the user's screen using the native Browser Screen Capture API and MediaRecorder API.
## Features
- 🎬 Record user-selected screen/window
- ⏱️ Configurable duration limits (default 5 minutes)
- 🎯 No external dependencies
- ⚡ Fine-grained control with start/stop callbacks
- 🛡️ Graceful error handling
- ♿ Respects user permissions
## Installation
Copy the `screen-record` folder to your project:
```
your-project/
├── tools/
│ └── screen-record/
│ ├── index.js
│ └── README.md
```
## Usage
### Basic Screen Recording
```javascript
import { recordScreen } from './tools/screen-record/index.js';
const recorder = await recordScreen();
if (recorder) {
// User is recording...
// Stop after 10 seconds
setTimeout(async () => {
const file = await recorder.stop();
console.log('Recording saved:', file.name);
}, 10000);
}
```
### With Duration Limit
```javascript
const recorder = await recordScreen({
maxDuration: 60000 // 1 minute instead of default 5 minutes
});
```
### With Callbacks
```javascript
const recorder = await recordScreen({
onStart: () => console.log('Recording started'),
onStop: () => console.log('Recording stopped')
});
```
### Quick Recording (Auto-stop)
```javascript
import { recordScreenFor } from './tools/screen-record/index.js';
const file = await recordScreenFor(10000); // Record for 10 seconds
console.log('Recording saved:', file.name);
```
## API Reference
### `recordScreen(options?)`
**Parameters:**
- `options` (Object, optional):
- `maxDuration` (number): Max recording time in ms. Default: 300000 (5 min)
- `mimeType` (string): MIME type. Default: 'video/webm'
- `onStart` (Function): Called when recording starts
- `onStop` (Function): Called when recording stops
**Returns:** `Promise<RecorderObject | null>`
Opens native browser dialog for user to select screen/window to record. Returns a recorder controller object with methods to stop and manage the recording.
**Recorder Object Methods:**
- `stop()` - Stops recording and returns File object
- `isRecording()` - Returns boolean for current status
- `getDuration()` - Returns duration in milliseconds
- `mediaStream` - Reference to underlying MediaStream
**Example:**
```javascript
const recorder = await recordScreen();
const file = await recorder.stop();
```
### `recordScreenFor(duration)`
**Parameters:**
- `duration` (number): Recording duration in milliseconds
**Returns:** `Promise<File>`
Convenience function that automatically stops recording after specified duration.
**Example:**
```javascript
const file = await recordScreenFor(30000); // 30 second recording
```
## Browser Support
Requires browsers supporting Screen Capture and MediaRecorder APIs:
- Chrome 72+
- Edge 79+
- Firefox 66+
- Safari 13+
Note: User must grant permission for each recording request.
## Error Handling
The tool gracefully handles:
- User cancellation of the capture dialog
- Browser permission denial
- Browser incompatibility
- Recording stream interruption
- Max duration timeout
All errors are logged to console and return null.
## Examples
### React Component with UI
```jsx
import { recordScreen } from './tools/screen-record/index.js';
import { useState } from 'react';
export function ScreenRecorderButton() {
const [isRecording, setIsRecording] = useState(false);
const [recorder, setRecorder] = useState(null);
const startRecording = async () => {
const rec = await recordScreen({
onStart: () => setIsRecording(true),
onStop: () => setIsRecording(false)
});
setRecorder(rec);
};
const stopRecording = async () => {
if (recorder) {
const file = await recorder.stop();
console.log('Recording saved:', file.name);
}
};
return (
<div>
{!isRecording ? (
<button onClick={startRecording}>Start Recording</button>
) : (
<button onClick={stopRecording}>Stop Recording</button>
)}
</div>
);
}
```
### With File Upload
```javascript
import { recordScreen } from './tools/screen-record/index.js';
const recorder = await recordScreen();
if (recorder) {
// Wait 15 seconds then stop
setTimeout(async () => {
const file = await recorder.stop();
const formData = new FormData();
formData.append('video', file);
await fetch('/api/upload-video', {
method: 'POST',
body: formData
});
}, 15000);
}
```
### Auto-stop after Duration
```javascript
import { recordScreenFor } from './tools/screen-record/index.js';
// Record for exactly 5 seconds
const file = await recordScreenFor(5000);
const formData = new FormData();
formData.append('video', file);
await fetch('/api/save-recording', {
method: 'POST',
body: formData
});
```
## File Output
Both functions return WebM video files with standardized naming:
- Filename format: `screen-recording-{timestamp}.webm`
- MIME type: `video/webm`
- Compatible with most video players and uploads
## License
MIT - Use freely in your projects
+170
View File
@@ -0,0 +1,170 @@
/**
* Screen Record Tool
* Records the user's screen using the Screen Capture API and MediaRecorder API
*
* @module ScreenRecord
* @description Standalone utility for capturing screen recordings. Exposes window.recordScreen()
* function for use in frontend code. No external dependencies beyond browser APIs.
* @usage Call window.recordScreen() after page load. User will see browser's native
* screen selector dialog. Returns Promise resolving to recording controller.
* @dependencies None (browser APIs only: Screen Capture API, MediaRecorder, Web Audio API)
* @integration Used in index.html to provide screen recording UI controls
*/
(function() {
console.log('Loading Screen Record Tool...');
/**
* Starts recording the user's selected screen/window
* Opens native browser dialog for user to select what to record
*
* @async
* @param {Object} [options={}] - Recording options
* @param {number} [options.maxDuration=300000] - Maximum recording duration in milliseconds (default: 5 minutes)
* @param {string} [options.mimeType='video/webm'] - MIME type for recording
* @param {Function} [options.onStart] - Callback when recording starts
* @param {Function} [options.onStop] - Callback when recording stops
* @returns {Promise<Object>} Recording controller object with stop() method and events
*
* @example
* const recorder = await window.recordScreen();
* // User is recording...
*
* setTimeout(() => {
* const file = await recorder.stop();
* console.log('Recording saved:', file.name);
* }, 10000);
*/
window.recordScreen = async function(options = {}) {
const {
maxDuration = 5 * 60 * 1000, // 5 minutes
mimeType = 'video/webm',
onStart = null,
onStop = null
} = options;
try {
// Request screen capture from user
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' },
audio: false
});
// Create MediaRecorder
const mediaRecorder = new MediaRecorder(mediaStream, { mimeType });
const chunks = [];
let isRecording = true;
let timeoutId = null;
// Collect recording data
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
// Handle recording stop
mediaRecorder.onstop = () => {
isRecording = false;
clearTimeout(timeoutId);
mediaStream.getTracks().forEach(track => track.stop());
if (onStop) onStop();
};
// Start recording
mediaRecorder.start();
if (onStart) onStart();
// Set max duration timeout
timeoutId = setTimeout(() => {
if (isRecording) {
mediaRecorder.stop();
}
}, maxDuration);
// Stop if user stops screen share
mediaStream.getVideoTracks()[0].onended = () => {
if (isRecording) {
mediaRecorder.stop();
}
};
// Return controller object
return {
/**
* Stops the recording and returns the recorded file
* @returns {Promise<File>} WebM File object with recorded video
*/
stop: async () => {
return new Promise((resolve) => {
if (!isRecording) {
resolve(null);
return;
}
const stopHandler = () => {
mediaRecorder.removeEventListener('stop', stopHandler);
const blob = new Blob(chunks, { type: mimeType });
const file = new File(
[blob],
`screen-recording-${Date.now()}.webm`,
{ type: mimeType }
);
resolve(file);
};
mediaRecorder.addEventListener('stop', stopHandler);
mediaRecorder.stop();
});
},
/**
* Checks if recording is currently active
* @returns {boolean} True if recording is in progress
*/
isRecording: () => isRecording,
/**
* Gets the current recording duration in milliseconds
* @returns {number} Duration since recording started
*/
getDuration: () => mediaRecorder.state === 'recording' ? mediaRecorder.state : 0,
/**
* The underlying MediaStream object
* @type {MediaStream}
*/
mediaStream
};
} catch (error) {
console.error('Screen recording failed:', error);
return null;
}
};
/**
* Simple helper to start recording and stop after specified duration
* Useful for quick recordings with automatic stop
*
* @async
* @param {number} duration - Recording duration in milliseconds
* @returns {Promise<File>} WebM File object with recorded video
*
* @example
* const file = await window.recordScreenFor(10000); // Record for 10 seconds
* console.log('Recording saved:', file.name);
*/
window.recordScreenFor = async function(duration) {
const recorder = await window.recordScreen();
if (!recorder) return null;
return new Promise((resolve) => {
setTimeout(async () => {
const file = await recorder.stop();
resolve(file);
}, duration);
});
};
console.log('Screen Record Tool loaded successfully');
})();
+157
View File
@@ -0,0 +1,157 @@
# User Alert System
A standalone utility for monitoring shared notes and alerting the current user when notes are shared with them. Provides real-time notifications with desktop alerts and audio feedback.
## Features
- 🔔 Real-time desktop notifications when notes are shared
- 🔊 Audio alert system with fallback bell tones
- 📍 Prevents duplicate alerts for the same note
- 🎯 PocketBase integration for realtime subscriptions
- 🛡️ Graceful error handling for permission denials
- ♿ Respects browser notification permissions
## Installation
Copy the `user-alert` folder to your project:
```bash
cp -r user-alert /path/to/project/tools/
```
## Usage
After the tool is loaded, instantiate with your PocketBase instance:
```javascript
import { UserAlertSystem } from 'tools/user-alert/index.js';
// Create alert system
const alertSystem = new UserAlertSystem(
pocketbaseInstance,
userEmail, // Current user's email
userName // Current user's display name
);
// Start monitoring for shared notes
await alertSystem.watchForSharedNotes();
// Stop monitoring when needed
alertSystem.stop();
```
## API
### Constructor
```javascript
new UserAlertSystem(pocketbaseInstance, userEmail, userName)
```
| Parameter | Type | Description |
| --- | --- | --- |
| `pocketbaseInstance` | PocketBase | Initialized PocketBase client |
| `userEmail` | string | Current user's email address |
| `userName` | string | Current user's display name |
### Methods
#### `watchForSharedNotes()`
Starts monitoring the notes collection for changes. Sets up realtime subscription that watches for notes where `sharedWith` includes the current user's email.
```javascript
await alertSystem.watchForSharedNotes();
```
#### `stop()`
Unsubscribes from collection changes and stops monitoring.
```javascript
alertSystem.stop();
```
#### `playNotificationSound()`
Plays a double-bell audio alert. Called automatically on alert, but can be triggered manually.
```javascript
await alertSystem.playNotificationSound();
```
## Integration
This tool is integrated into Prism Notes via `index.html`. Include it in your HTML:
```html
<script type="module">
import { UserAlertSystem } from 'tools/user-alert/index.js';
// Initialize after PocketBase and user auth is ready
const alertSystem = new UserAlertSystem(pb, userEmail, userName);
await alertSystem.watchForSharedNotes();
</script>
```
## PocketBase Collection Requirements
Your notes collection must have:
- `sharedWith` field (array of email addresses)
- `title`, `content` fields for display
Example schema:
```
notes {
id: string (primary)
title: string
content: string
createdBy: string
sharedWith: array (of email strings)
createdAt: date
}
```
## Browser Compatibility
Requires:
- Web Audio API for notification sounds
- Notifications API for desktop alerts
- ES6 modules for import/export
- PocketBase realtime subscriptions
Tested on:
- Chrome 90+
- Edge 90+
- Firefox 88+
## Permissions
The system requires user permission for:
- **Notifications**: Desktop notification display
- **Audio**: Playing notification sounds (may be blocked by autoplay policy until user gesture)
Users will be prompted by the browser to grant these permissions.
## Dependencies
- `pocketbase` (passed in constructor) — Realtime subscription system
- Browser APIs: Notifications API, Web Audio API, DOM
## Notes
- Notifications are persistent once dismissed; consider adding notification center
- Audio alerts may fail silently if browser blocks autoplay; notifications still work
- Prevent duplicate alerts by tracking `alertedNotes` Set internally
- Performance: Realtime subscription uses websocket; consider unsubscribing when user leaves page
## Troubleshooting
### "Permission denied" error
User denied notification permission. System will continue working but won't show desktop alerts.
### No audio on notification
Browser autoplay policy may block audio until user gesture. Desktop notification still appears.
### Alerts duplicating
Check that PocketBase realtime updates aren't sending multiple change events. Duplicate tracking should prevent this, but verify collection triggers.
+207
View File
@@ -0,0 +1,207 @@
/**
* User Alert System
* Monitors shared notes and alerts when someone shares a note with the current user
*
* @module UserAlertSystem
* @description Standalone utility for monitoring PocketBase note collection changes.
* Watches for notes shared with current user and triggers desktop notifications and
* audio alerts. Exports UserAlertSystem class for instantiation with PocketBase instance.
* @usage Instantiate with PocketBase instance: new UserAlertSystem(pb, userEmail, userName).
* Call watchForSharedNotes() to start monitoring.
* @dependencies PocketBase instance (passed in constructor). Browser APIs: Notifications,
* Web Audio API, DOM manipulation.
* @integration Used in index.html to provide real-time alert functionality for shared notes
* @gotchas Requires user permission for desktop notifications. Audio context may be blocked
* on some browsers until user gesture. Track alertedNotes Set to prevent duplicate alerts.
*/
export class UserAlertSystem {
constructor(pocketbaseInstance, userEmail, userName) {
this.pb = pocketbaseInstance;
this.userEmail = userEmail;
this.userName = userName;
this.alertedNotes = new Set(); // Track already-alerted notes to avoid duplicates
this.unsubscribe = null;
}
/**
* Play notification sound - double bell/chime
*/
async playNotificationSound() {
try {
// Create a simple double bell tone using Web Audio API
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const now = audioContext.currentTime;
// First bell
this.playBell(audioContext, now, 0);
// Second bell (offset by 200ms)
this.playBell(audioContext, now + 0.2, 0.7);
} catch (error) {
console.error('Failed to play notification sound:', error);
// Fallback: try to play a system notification beep
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
osc.connect(gain);
gain.connect(audioContext.destination);
osc.frequency.value = 800;
gain.gain.setValueAtTime(0.3, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
osc.start(audioContext.currentTime);
osc.stop(audioContext.currentTime + 0.1);
} catch (e) {
console.error('Fallback notification sound also failed:', e);
}
}
}
/**
* Play a single bell tone
*/
playBell(audioContext, startTime, volume = 1) {
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
osc.connect(gain);
gain.connect(audioContext.destination);
// Bell frequency (higher pitch for notification)
osc.frequency.setValueAtTime(1000, startTime);
osc.frequency.exponentialRampToValueAtTime(600, startTime + 0.15);
// Bell envelope (fade out)
gain.gain.setValueAtTime(0.3 * volume, startTime);
gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.2);
osc.start(startTime);
osc.stop(startTime + 0.2);
}
/**
* Check if a name matches the current user
* Searches Associations collection for first_name match and verifies email
*/
async verifyUserMatch(firstName) {
try {
console.log(`Checking for user match: firstName="${firstName}", currentEmail="${this.userEmail}"`);
// Search Associations collection for matching first_name
const records = await this.pb.collection('Associations').getList(1, 50, {
filter: `first_name = "${firstName}"`,
});
console.log(`Found ${records.items.length} records with first_name="${firstName}"`);
if (records.items.length === 0) {
console.log(`No user found with first name: ${firstName}`);
return false;
}
// Check if any matching record has the current user's email
const match = records.items.find((record) => {
console.log(`Checking record: ${record.first_name} ${record.last_name} (${record.email})`);
return record.email === this.userEmail;
});
if (match) {
console.log(`✓ User match confirmed: ${firstName} (${this.userEmail})`);
return true;
}
console.log(`No email match for ${firstName} and ${this.userEmail}`);
return false;
} catch (error) {
console.error('Error verifying user match:', error);
return false;
}
}
/**
* Initialize real-time monitoring of Notes collection
*/
async startMonitoring() {
try {
console.log('🔔 Starting user alert monitoring...');
// Subscribe to Notes collection changes
this.unsubscribe = await this.pb.collection('Notes').subscribe('*', async (event) => {
try {
const note = event.record;
console.log(`📝 Note event (${event.action}):`, note.title, `shared=${note.shared}`, `shared_with=`, note.shared_with);
// Only process shared notes
if (!note.shared) {
console.log(' → Skipped: not shared');
return;
}
// Check if current user is in shared_with list
const sharedWithNames = Array.isArray(note.shared_with)
? note.shared_with
: typeof note.shared_with === 'string'
? note.shared_with.split(',').map((n) => n.trim())
: [];
console.log(` → shared_with names:`, sharedWithNames);
// Extract first names from shared_with and check for match
let userIsRecipient = false;
for (const sharedName of sharedWithNames) {
const firstName = sharedName.split(' ')[0];
console.log(` → Checking if ${firstName} matches...`);
const isMatch = await this.verifyUserMatch(firstName);
if (isMatch) {
userIsRecipient = true;
break;
}
}
if (!userIsRecipient) {
console.log(' → Skipped: user not in shared_with');
return;
}
// Check if we've already alerted for this note
if (this.alertedNotes.has(note.id)) {
console.log(' → Skipped: already alerted for this note');
return;
}
// Mark as alerted and play sound
this.alertedNotes.add(note.id);
console.log(`🔔 Alert! Note "${note.title}" shared with you`);
await this.playNotificationSound();
} catch (error) {
console.error('Error processing note event:', error);
}
});
console.log('✓ User alert monitoring started');
} catch (error) {
console.error('Failed to start monitoring:', error);
}
}
/**
* Stop monitoring
*/
stopMonitoring() {
if (this.unsubscribe) {
this.unsubscribe();
this.unsubscribe = null;
console.log('⏹ User alert monitoring stopped');
}
}
}
/**
* Initialize user alert system
* Call this after user authentication
*/
export async function initializeUserAlert(pb, userEmail, userName) {
const alertSystem = new UserAlertSystem(pb, userEmail, userName);
await alertSystem.startMonitoring();
return alertSystem;
}
+194
View File
@@ -0,0 +1,194 @@
# User Email Lookup Tool
A standalone utility for looking up email addresses from the Associations collection and displaying them as hover tooltips over user names in the sharing interface.
## Features
- 🔍 Email lookup via Associations collection by first name
- 💬 Hover tooltips show email addresses automatically
- 🚀 Request caching to minimize PocketBase queries
- 👁️ Automatic tooltip persistence during hover
- 📍 MutationObserver integration for dynamic capsule additions
- ⚡ Graceful fallback for missing emails
## Installation
Copy the `user-email-lookup` folder to your project:
```bash
cp -r user-email-lookup /path/to/project/tools/
```
## Usage
After the tool is loaded and PocketBase is initialized, call:
```javascript
// Initialize email lookups for note sharing
window.setupUserEmailLookup(pb, '#selectedUsersContainer');
// Or for conversation sharing
window.setupUserEmailLookup(pb, '#selectedConversationUsersContainer');
```
### Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| `pb` | PocketBase | Initialized PocketBase client instance |
| `containerSelector` | string | CSS selector for container with user capsules (e.g., `#selectedUsersContainer`) |
## How It Works
1. **Initial Setup**: Scans container for existing user capsules and attaches hover listeners
2. **Dynamic Monitoring**: Uses MutationObserver to detect newly added capsules
3. **Email Lookup**: On hover, queries Associations collection for `first_name` match
4. **Caching**: Stores results in memory to prevent redundant queries
5. **Tooltip Display**: Shows email in native browser tooltip (`title` attribute)
### Example User Experience
```
User types: "John, Sarah, Mike"
System creates capsules with their first names
User hovers over "John" capsule
→ Lookup queries Associations for first_name = "John"
→ Finds: john.smith@company.com
→ Tooltip shows: "john.smith@company.com"
User hovers over "Sarah" capsule
→ Cache hit (if already looked up Sarah recently)
→ Tooltip shows: "sarah.jones@company.com"
```
## API
### `setupUserEmailLookup(pb, containerSelector)`
Initialize email lookups for a container of user capsules.
```javascript
window.setupUserEmailLookup(pb, '#selectedUsersContainer');
```
### `getEmailByFirstName(pb, firstName)`
Manually lookup an email by first name. Used internally but available for custom extensions.
```javascript
const email = await getEmailByFirstName(pb, 'John');
// Returns: 'john@example.com' or 'Email not found' or 'Error loading email'
```
### `clearEmailLookupCache()`
Clear the in-memory email cache. Useful if Associations collection is updated and you need fresh data.
```javascript
window.clearEmailLookupCache();
```
## Integration
This tool is designed for Prism Notes integration. Include it in `index.html`:
```html
<script src="tools/user-email-lookup/index.js"></script>
```
Then initialize after PocketBase auth completes:
```javascript
// In the user auth flow
if (pb.authStore.isValid) {
window.setupUserEmailLookup(pb, '#selectedUsersContainer');
}
```
## PocketBase Requirements
Your **Associations** collection must have:
- `first_name` field (string) — used for lookups
- `email` field (string) — displayed in tooltip
### Example Associations Record
```json
{
"id": "abc123",
"first_name": "John",
"email": "john.smith@company.com",
"last_name": "Smith",
"created": "2026-01-14T...",
"updated": "2026-01-14T..."
}
```
## Caching Behavior
**Cache Storage**: In-memory JavaScript object (lost on page refresh)
**When to Clear**:
- After Associations collection updates
- If user updates their profile
- Before bulk operations with many lookups
**Manual Clear**:
```javascript
window.clearEmailLookupCache();
```
## Browser Compatibility
- Works in all modern browsers (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)
- Requires MutationObserver API (widely supported)
- Uses native `title` attribute for tooltips (no dependencies)
## Performance Notes
- First lookup for a name: ~100-200ms (network + PocketBase query)
- Subsequent lookups: <1ms (cache hit)
- Memory usage: ~100 bytes per cached name
- MutationObserver overhead: Negligible
## Troubleshooting
### "Email not found" tooltip
Check that:
1. Associations collection exists in PocketBase
2. `first_name` field is spelled correctly
3. User's record has the `first_name` populated
4. PocketBase read rules allow access to Associations collection
### Slow tooltips on first hover
This is expected—first lookup queries PocketBase. Subsequent hovers are instant (cached).
### "Error loading email" tooltip
Check PocketBase console for errors. Possible causes:
- Network connectivity issues
- Insufficient permissions to read Associations
- PocketBase server downtime
### Cache not clearing
Cache is in-memory only. Refresh the page to clear, or call:
```javascript
window.clearEmailLookupCache();
```
## Future Enhancements
- [ ] Persistent storage (IndexedDB) for longer-lived cache
- [ ] Configurable tooltip styling (custom CSS instead of native title)
- [ ] Bulk email lookup for performance optimization
- [ ] Email suggestion/autocomplete during input
- [ ] Fallback email patterns (first.last@company.com)
## Dependencies
- `pocketbase` (passed in as parameter) — for Associations queries
- Browser APIs: DOM, MutationObserver, title attribute
## Notes
- Tooltips use native browser `title` attribute for simplicity and accessibility
- Lookup is by `first_name` only (not full name) to match how names are entered in the UI
- Cache is intentionally in-memory to support real-time Associations updates
+175
View File
@@ -0,0 +1,175 @@
/**
* User Email Lookup Tool
* Provides hover tooltips showing email addresses when hovering over user names
*
* @module UserEmailLookup
* @description Standalone utility for looking up email addresses from the Associations
* collection and displaying them as hover tooltips. Integrates with note/conversation
* sharing UI to enhance user experience when managing shared access.
* @usage Call window.setupUserEmailLookup(pbInstance, containerSelector) after page load.
* Pass PocketBase instance and CSS selector for user capsule container.
* @dependencies PocketBase instance (passed in). Browser APIs: DOM, tooltips via title attribute.
* @integration Used in index.html note/conversation sharing forms to show email on hover
* @gotchas Associations collection must have first_name field for lookups. Lookups are
* performed on hover, so network latency may cause brief delay before tooltip appears.
* Cache email lookups to minimize PocketBase queries.
*/
// Cache for email lookups to minimize queries
const emailLookupCache = {};
let associationsCollectionAvailable = true;
let associationsWarningShown = false;
function getAssociationEmail(record) {
return record?.emailtext || record?.email || 'Email not found';
}
function handleAssociationsError(error) {
const statusCode = Number(error?.status || error?.response?.status || 0);
if (statusCode === 404) {
associationsCollectionAvailable = false;
if (!associationsWarningShown) {
associationsWarningShown = true;
console.warn("'Associations' collection not found. Email hover lookup disabled.");
}
return;
}
console.error('Associations lookup failed:', error);
}
/**
* Fetch email from Associations collection by first name
* Uses cache to avoid duplicate queries
*
* @param {PocketBase} pb - PocketBase instance
* @param {string} firstName - First name to look up
* @returns {Promise<string>} Email address or fallback message
* @example
* const email = await getEmailByFirstName(pb, 'John');
* console.log(email); // 'john@example.com' or 'Email not found'
*/
async function getEmailByFirstName(pb, firstName) {
if (!firstName) return 'Email not found';
if (!associationsCollectionAvailable) return 'Email lookup unavailable';
// Check cache first
if (emailLookupCache[firstName]) {
return emailLookupCache[firstName];
}
try {
const records = await pb.collection('Associations').getList(1, 50, {
filter: `first_name = "${firstName}"`,
});
if (records.items.length === 0) {
emailLookupCache[firstName] = 'Email not found';
return 'Email not found';
}
// Use first match's emailtext field
const firstRecord = records.items[0];
const email = getAssociationEmail(firstRecord);
emailLookupCache[firstName] = email;
return email;
} catch (error) {
handleAssociationsError(error);
return 'Error loading email';
}
}
/**
* Setup email lookups for user capsules in a container
* Attaches hover event listeners to show email in tooltip
*
* @param {PocketBase} pb - PocketBase instance
* @param {string} containerSelector - CSS selector for container with user capsules
* @example
* window.setupUserEmailLookup(pb, '#selectedUsersContainer');
*/
window.setupUserEmailLookup = function(pb, containerSelector) {
const container = document.querySelector(containerSelector);
if (!container) {
console.warn(`setupUserEmailLookup: Container not found: ${containerSelector}`);
return;
}
console.log(`✓ User Email Lookup initialized for: ${containerSelector}`);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.classList?.contains('inline-flex')) {
attachEmailLookupTooltip(pb, node);
}
});
});
});
observer.observe(container, {
childList: true,
subtree: true
});
// Also process any existing capsules
container.querySelectorAll('.inline-flex.items-center.gap-2').forEach((capsule) => {
attachEmailLookupTooltip(pb, capsule);
});
};
/**
* Attach email lookup tooltip to a single user capsule
* Shows email on hover (with caching to prevent redundant queries)
*
* @param {PocketBase} pb - PocketBase instance
* @param {HTMLElement} capsuleEl - The user capsule element
* @private
*/
function attachEmailLookupTooltip(pb, capsuleEl) {
// Extract first name from capsule text
const nameSpan = capsuleEl.querySelector('span:not([data-email-loaded])');
if (!nameSpan || nameSpan.hasAttribute('data-email-loaded')) {
return;
}
const firstName = nameSpan.textContent.trim();
if (!firstName) return;
console.log(`[Email Lookup] Attached to user: ${firstName}`);
// Mark as processed
nameSpan.setAttribute('data-email-loaded', 'true');
// Attach hover listener
capsuleEl.addEventListener('mouseenter', async () => {
console.log(`[Email Lookup] Hover on: ${firstName}`);
// If email already in title, don't query again
if (capsuleEl.title && !capsuleEl.title.startsWith('Loading')) {
return;
}
capsuleEl.title = 'Loading email...';
const email = await getEmailByFirstName(pb, firstName);
capsuleEl.title = email;
capsuleEl.classList.add('cursor-help');
});
capsuleEl.addEventListener('mouseleave', () => {
// Title persists so user can still see it if they hover back quickly
});
}
/**
* Clear the email lookup cache
* Useful if Associations collection is updated and you need fresh data
*
* @example
* window.clearEmailLookupCache();
*/
window.clearEmailLookupCache = function() {
Object.keys(emailLookupCache).forEach(key => delete emailLookupCache[key]);
associationsCollectionAvailable = true;
associationsWarningShown = false;
console.log('✓ Email lookup cache cleared');
};
console.log('✓ User Email Lookup tool loaded - call window.setupUserEmailLookup(pb, containerSelector)');