From 9b51f87b33d6944505145ed20d2597aaa821ccd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 02:20:18 +0000 Subject: [PATCH 1/2] docs: add exhaustive technical manual (TECHNICAL_MANUAL.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 14-chapter manual covering architecture, data flow, every module, all 8 new features, the CI pipeline, and a step-by-step extension example. Written for the high-context learner — big picture first, code last, analogies throughout. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_017vTcQHgydhEt3ZFXFKJs5k --- TECHNICAL_MANUAL.md | 1143 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1143 insertions(+) create mode 100644 TECHNICAL_MANUAL.md diff --git a/TECHNICAL_MANUAL.md b/TECHNICAL_MANUAL.md new file mode 100644 index 0000000..91eb126 --- /dev/null +++ b/TECHNICAL_MANUAL.md @@ -0,0 +1,1143 @@ +# PM Ops Map — Technical Manual + +> Written for the high-context learner: big picture first, code last, analogies everywhere. + +--- + +## How to Read This Manual + +Every chapter follows the same 10-step sequence: + +1. **Big Picture** — what problem does this exist to solve? +2. **System Context** — where does it fit? +3. **Architecture** — what are the pieces and how do they talk? +4. **Constants vs Variables** — what never changes vs what does? +5. **Mental Model** — one analogy that nails the mechanism +6. **Visualize the Flow** — step-by-step input → process → output +7. **Technical Details** — code, syntax, and exact definitions +8. **WHY** — why this rule exists and what breaks without it +9. **Check Your Understanding** — conceptual questions before memorizing +10. **Practice** — progressively harder examples + +Read chapters 1 and 2 completely before skipping to any specific module chapter. They explain the foundations everything else builds on. + +--- + +--- + +# Chapter 1 — The Big Picture + +## 1.1 What Problem Does This Solve? + +A property management company on day one typically has zero documented operating structure. No record of who owns what. No maintenance tracking. No organized team responsibilities. They run on verbal agreements, group texts, and someone's personal spreadsheet. + +That spreadsheet breaks the moment the company has a second employee. + +PM Ops Map exists to solve the **zero-to-structure** problem. You open it, enter your company name, and you instantly have: + +- A complete documented map of 17 standard PM departments and 260+ tasks +- A way to assign ownership of every task to a real team member +- A maintenance work order pipeline +- A property, tenant, and vendor registry +- An exportable operations handbook you can hand to a new hire on day 1 + +The entire thing runs in a browser. No accounts. No monthly fee. No server. No database. + +--- + +## 1.2 System Context — Where Does It Live? + +``` +[GitHub Repository] + ↓ +[GitHub Pages — serves static files] + ↓ +[Your Browser — runs the entire app] + ↓ +[localStorage — saves all your data inside the browser] +``` + +The app is a **static website** — a folder of HTML, CSS, and JavaScript files. When you open it: + +1. The browser downloads the files from GitHub Pages (or your hard drive) +2. JavaScript runs entirely inside your browser tab +3. Your data is saved to `localStorage` — a private key-value store built into every browser +4. Nothing goes to a server. No API calls. No database. + +> **Analogy:** Think of it like a really smart form that saves itself inside the browser window. The form is smart enough to be a whole app, but it never phones home. + +--- + +## 1.3 The Core Constraint (and Why It Matters) + +Because there is no backend, **everything is the browser's responsibility**. This shapes every architectural decision in the codebase: + +| Need | Solution | +|------|----------| +| Save user data | `localStorage` | +| Load default structure | `config.json` (fetched once on startup) | +| Export data | Generate a file in-browser and trigger a download | +| Share data between devices | Copy/paste a JSON blob via clipboard | +| Undo an action | Keep one "snapshot" in memory before the action | + +Every feature in this app is ultimately a creative answer to "how do we do this without a server?" + +--- + +## 1.4 Check Your Understanding + +Before moving on, answer these without looking back: + +1. Where is user data stored — on a server or in the browser? +2. What happens to your data if you open the app in a different browser on the same computer? +3. Why does the app need `config.json` at all if it uses `localStorage`? + +*(Answers: 1. Browser localStorage. 2. It's gone — localStorage is per-browser, per-device. 3. localStorage holds your edits; config.json holds the original structure to start from on first load.)* + +--- + +--- + +# Chapter 2 — Architecture + +## 2.1 Big Picture + +The app is organized as a set of **ES Modules** — JavaScript files that each handle one concern and share data by importing from each other. + +> **Analogy:** Think of the app as an office building. Each floor (module) handles a specific department. To get something done, you go to the right floor. Floors can send messages to each other, but each floor manages its own work. + +--- + +## 2.2 The Module Map + +``` +config.json ← the master rulebook (not a JS module — just data) + +state.js ← the front desk (everyone comes here to read shared data) +utils.js ← the toolbox room (pure functions, no side effects) +storage.js ← the file room (reads and writes to localStorage) +ui.js ← the lobby (stats bar, notifications, onboarding) +io.js ← the loading dock (import/export/clipboard/undo) +handbook.js ← the print room (generates the operations handbook) +launchPlan.js ← the onboarding office (first-week setup guide) +templates.js ← the forms cabinet (role templates, SOP text, demo data) +stateSchema.js ← the customs inspector (validates imported data) + +views/tracking.js ← Floor 1: task tracker (the main view) +views/map.js ← Floor 2: org flow diagram (SVG visualization) +views/team.js ← Floor 3: team manager + auto-assign engine +views/workorders.js ← Floor 4: maintenance pipeline (kanban board) +views/portfolio.js ← Floor 5: property/tenant/vendor registry +views/recurring.js ← Floor 6: recurring work order templates + +app.js ← the building directory (imports everything, boots the app) +index.html ← the physical building (HTML structure, all the panels/modals) +css/style.css ← the interior design (fonts, colors, layout, animations) +``` + +--- + +## 2.3 The Dependency Rule (The Most Important Rule in the Codebase) + +**No circular dependencies.** Module A can import from module B, but then B cannot import from A. The arrows in the module graph can only point downward. + +``` +app.js + ↓ imports from +views/*.js, io.js, handbook.js, launchPlan.js, recurring.js + ↓ import from +state.js, storage.js, utils.js, ui.js + ↓ import from +state.js, utils.js ← these two import nothing else +``` + +> **WHY:** Circular imports in JavaScript ES modules cause one of the modules to receive `undefined` for some of its imported values, because JavaScript tries to resolve the import before the other module has finished loading. It's like two people each waiting for the other to walk through the door first — nobody goes anywhere. You'd get a bug that's very hard to diagnose. + +> **Analogy:** It's like a company org chart — your manager can give you work, you can ask your assistant for help, but your assistant can't also be your manager's manager. The hierarchy has to be a one-way tree. + +--- + +## 2.4 The window Globals Pattern + +HTML has inline event handlers like this: + +```html + +``` + +For `cycleTaskStatus` to work here, it has to exist on the global `window` object. But ES modules are **private by default** — they don't automatically put their functions on `window`. + +The solution: at the bottom of `app.js`, every function that an HTML button might call is explicitly registered: + +```javascript +Object.assign(window, { + cycleTaskStatus, + renderTrackingView, + // ... 60+ more +}); +``` + +> **Analogy:** It's like the building PA system. Each office (module) has its own phone system, but only the functions registered with the front desk (window) can be paged building-wide. If you add a new function that a button calls, you must register it at the front desk or the button silently does nothing. + +> **WHY this rule hurts when broken:** When a function isn't on `window`, clicking the button produces no error in the console — the browser just finds `undefined` and calls nothing. It's a silent failure, which makes it one of the most frustrating bugs to diagnose. + +--- + +## 2.5 Constants vs Variables + +**What never changes (constants):** + +| Item | Where | Value | +|------|-------|-------| +| Storage key names | `storage.js` | `'pm-ops-data-v1'`, `'pm-ops-team-v1'`, etc. | +| Status cycle order | `state.js` | `['todo', 'in-progress', 'blocked', 'done']` | +| Priority cycle order | `state.js` | `['high', 'medium', 'low']` | +| Color palette | `state.js` | 20 hardcoded hex codes | +| Department structure | `config.json` | (loaded once, then editable in memory) | + +**What changes constantly:** + +| Item | Lives in | Changes when | +|------|----------|--------------| +| Task owners, statuses, due dates | `orgData.departments[].tasks[]` | User edits in Tracking view | +| Team roster | `teamData.employees[]` | User adds/removes employees | +| Work orders | `workOrders[]` | User creates/advances/deletes work orders | +| Portfolio | `portfolio.properties/tenants/vendors[]` | User edits portfolio | +| Audit log | `auditLog[]` | Any meaningful action | + +--- + +## 2.6 Check Your Understanding + +1. If you add a new function `openSpecialPanel()` to `views/tracking.js` and hook it to a button in `index.html`, what two things do you need to do in `app.js`? +2. Why can't `state.js` import from `storage.js`? +3. What happens to a circular dependency at runtime? + +*(Answers: 1. Import it in app.js, then add it to Object.assign(window, {...}). 2. It would create a circular dependency since storage.js already imports from state.js. 3. One module receives undefined for its imported values, causing silent bugs.)* + +--- + +--- + +# Chapter 3 — The Startup Sequence + +## 3.1 Mental Model + +> **Analogy:** Imagine a restaurant opening for the day. There's a strict order: unlock the doors, check the inventory (config.json), brief the staff (state.js), load yesterday's orders (localStorage), set the tables (render views), and only then let customers in. Skip any step and the whole service is off. + +## 3.2 The Startup Flow + +``` +Browser loads index.html + ↓ +Browser parses " + ↓ +"<script>evil()</script>" +``` + +> **WHY this is critical:** Every time user-provided text (task names, employee names, notes, company name) is inserted into an HTML template literal, it must be escaped. Without this, a user who types `` as a task name could execute JavaScript in the browser — a classic XSS (Cross-Site Scripting) attack. Even though this is a single-user local app, the rule is important for when it's shared or hosted. + +> **Analogy:** It's like the security guard at the door. Every piece of user input that wants to enter the HTML building must be checked: angle brackets get rewritten as harmless text (`<`), quotation marks too (`"`). Nothing sneaks in as executable markup. + +### `isValidISODate(str)` + +Returns `true` only for valid `YYYY-MM-DD` strings: + +```javascript +isValidISODate('2026-06-26') // true +isValidISODate('2026-13-01') // false — month 13 doesn't exist +isValidISODate('tomorrow') // false +isValidISODate(null) // false +``` + +The implementation parses the date twice: once with a regex for format, and once by constructing a `Date` object and checking that `toISOString()` round-trips back to the same string. This catches edge cases like February 30 that pass the regex but aren't real dates. + +### `_downloadBlob(content, mimeType, filename)` + +The only way to trigger a file download from JavaScript in a browser without a server. Creates a temporary object URL, attaches it to a hidden anchor tag, clicks it, then immediately cleans up. + +### `shakeInput(el)` + +Adds the CSS class `shake` to an element for 400ms, triggering a CSS animation that shakes the element left-right. Used as the "validation failed" signal — instead of an alert box, the offending input shakes. + +--- + +--- + +# Chapter 12 — The XSS Defense Layer + +## 12.1 The Problem + +The app renders user data into HTML constantly. Task names, employee names, company names, work order notes — all of these appear in template literals like: + +```javascript +`
${task.name}
` +``` + +If `task.name` were ``, that HTML would execute. + +## 12.2 The Solution — escapeHtml() Everywhere + +Every place user data enters an HTML template literal, `escapeHtml()` wraps it: + +```javascript +`
${escapeHtml(task.name)}
` +``` + +The `escapeHtml` function is imported at the top of every view file. It's a convention that every contributor must follow. + +## 12.3 The jsonAttr() Function + +There's a trickier case: function calls inside HTML attributes: + +```javascript +// Dangerous — if task.name contains quotes, this breaks the attribute +`` + +// Safe — jsonAttr() JSON-encodes the value and escapes quotes for HTML attribute context +`` +``` + +`jsonAttr()` double-encodes: first `JSON.stringify` (adding surrounding quotes and escaping internal quotes), then replaces `"` with `"` for the HTML attribute context. + +--- + +--- + +# Chapter 13 — The CI Pipeline + +## 13.1 What CI Does + +CI (Continuous Integration) runs automatically on every push and pull request. It catches problems before they reach the live site. + +``` +git push to GitHub + ↓ +GitHub Actions triggers .github/workflows/ci.yml + ↓ +Step 1: npm ci ← install exact dependency versions from package-lock.json +Step 2: npm test ← run Jest unit tests +Step 3: npm audit --omit=dev --audit-level=moderate ← check for known vulnerabilities +Step 4: npm run build ← webpack production bundle (catches import errors) +``` + +## 13.2 Why --omit=dev + +All packages in this project are `devDependencies` — webpack, jest, webpack-dev-server. The deployed app is just static HTML/CSS/JS with zero npm packages at runtime. + +`npm audit` originally failed because `webpack-dev-server` (a dev tool) had vulnerabilities. Adding `--omit=dev` tells npm audit to only check runtime dependencies — of which there are none. + +> **Analogy:** Auditing a restaurant for health code violations shouldn't include the chef's personal car — only the kitchen equipment matters. `--omit=dev` tells the auditor to check only the kitchen (runtime), not the parking lot (devDependencies). + +## 13.3 The Jest Tests + +Four test files cover the pure logic: + +| File | What it tests | +|------|--------------| +| `utils.test.js` | `escapeHtml`, `isValidISODate`, `getTodayISO`, `formatDueChip`, etc. — 37 tests | +| `data.test.js` | `config.json` structure — every department has required fields, every task has name + owner | +| `stateSchema.test.js` | Import/export schema version and validation report | +| `templates.test.js` | Role template and SOP template structure | + +The `.cjs` mirror files (`utils.cjs`, `stateSchema.cjs`, `templates.cjs`) exist because Jest runs in CommonJS mode and can't natively import ES module `export` syntax. The mirrors use `module.exports =` instead of `export`. When you change `utils.js`, you must mirror the change in `utils.cjs` or the tests will test old behavior. + +--- + +--- + +# Chapter 14 — How to Add a New Feature + +This chapter walks through a real extension example to cement all the concepts above. + +## 14.1 Example: Add a "Star" Button to Tasks + +**Goal:** Let users star important tasks. Starred tasks get a ⭐ icon. Stars persist. + +**Step 1: Add the field to the data model** + +No code change needed — `orgData` tasks are plain objects. You can add any field you want in JavaScript. The field just needs to be saved and loaded. + +**Step 2: Save the field in storage.js** + +In `saveToStorage()`, add `starred` to the task payload: + +```javascript +tasks: dept.tasks.map(t => ({ + // ... existing fields + starred: t.starred || false, +})) +``` + +In `loadFromStorage()`, merge it: + +```javascript +if (typeof savedTask.starred === 'boolean') task.starred = savedTask.starred; +``` + +**Step 3: Render the button in tracking.js** + +Inside the task row template in `renderTrackingView()`: + +```javascript + +``` + +**Step 4: Add the handler function in tracking.js** + +```javascript +export function toggleTaskStar(deptId, taskIdx) { + const dept = orgData.departments.find(d => d.id === deptId); + if (!dept) return; + const task = dept.tasks[taskIdx]; + if (!task) return; + task.starred = !task.starred; + saveToStorage(); + _updateStarIcon(deptId, taskIdx, task); // update just the button, no full re-render +} +``` + +**Step 5: Register in app.js** + +Add to the import: +```javascript +import { ..., toggleTaskStar } from './views/tracking.js'; +``` + +Add to `Object.assign(window, {...})`: +```javascript +toggleTaskStar, +``` + +**Step 6: Style in style.css** + +```css +.task-star-btn { opacity: 0.3; background: none; border: none; cursor: pointer; } +.task-star-btn--starred { opacity: 1; } +``` + +That's a complete feature addition. No backend. No schema migration. No build step required to test it. + +--- + +## 14.2 Check Your Understanding (Final) + +1. Why does `toggleTaskStar` need to be in `Object.assign(window, {...})`? +2. Why does `saveToStorage()` need to explicitly include `starred` in its payload? +3. If you forget to add `starred` to `loadFromStorage()`, what happens to the user's stars after a page refresh? +4. Why is it safe to add a new field to task objects in JavaScript without any "migration"? +5. What would happen if you called `renderTrackingView()` instead of `_updateStarIcon()` inside `toggleTaskStar()`? + +*(Answers: 1. HTML onclick handlers can only call window-level functions. 2. saveToStorage builds a fresh serialized snapshot — anything not included gets lost. 3. They're lost — loadFromStorage doesn't know to restore the field. 4. JS objects are dynamic; adding a field to an in-memory object is instant. Old saved data without the field is handled by the `|| false` default in the load logic. 5. It would work but cause a full re-render of 260 tasks — much slower than updating one button's class. Both are correct; one is efficient.)* + +--- + +--- + +# Quick Reference + +## Storage Keys + +| Key | Description | +|-----|-------------| +| `pm-ops-data-v1` | Task state | +| `pm-ops-team-v1` | Team roster | +| `pm-ops-workorders-v1` | Work orders | +| `pm-ops-portfolio-v1` | Properties, tenants, vendors | +| `pm-ops-audit-v1` | Audit log | +| `pm-ops-company-name` | Company name | +| `pm-ops-profile-v1` | Operations focus + portfolio size | +| `pm-ops-nav-compact` | Nav hide/show state | +| `pm-ops-guide-dismissed` | Welcome guide dismissed | +| `pm-ops-notif-date` | Last notification date | +| `pm-ops-launch-checklist-v1` | Launch checklist progress | +| `pm-ops-backups-v1` | Last 5 auto-backup snapshots | + +## Status Values + +`todo` → `in-progress` → `blocked` → `done` + +## Priority Values + +`high` → `medium` → `low` + +## Work Order Status Values + +`submitted` → `scheduled` → `in-progress` → `completed` + +## Module Responsibility + +| Module | Owns | +|--------|------| +| `state.js` | Shared data + constants | +| `storage.js` | localStorage read/write + toasts + backups | +| `utils.js` | Pure functions (escape, date, download, shake) | +| `ui.js` | Stats bar + notifications + onboarding modal | +| `io.js` | Export/import + clipboard + undo | +| `handbook.js` | Markdown + HTML handbook generator | +| `launchPlan.js` | Beginner setup guide + data quality checks | +| `templates.js` | Role templates + SOP text + demo data | +| `stateSchema.js` | Import validation | +| `views/tracking.js` | Task list + filters + bulk mode + notes + custom fields | +| `views/map.js` | SVG org flow diagram | +| `views/team.js` | Team roster + auto-assign engine | +| `views/workorders.js` | Work order kanban board | +| `views/portfolio.js` | Property/tenant/vendor registry | +| `views/recurring.js` | Recurring work order templates | +| `app.js` | Entry point + window globals + view switcher | + +--- + +*End of Technical Manual — PM Ops Map* From 4fca521faa9aa2b81044d046e4122022cdc322a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 02:22:49 +0000 Subject: [PATCH 2/2] docs: remove TECHNICAL_MANUAL.md (distributed as PDF instead) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_017vTcQHgydhEt3ZFXFKJs5k --- TECHNICAL_MANUAL.md | 1143 ------------------------------------------- 1 file changed, 1143 deletions(-) delete mode 100644 TECHNICAL_MANUAL.md diff --git a/TECHNICAL_MANUAL.md b/TECHNICAL_MANUAL.md deleted file mode 100644 index 91eb126..0000000 --- a/TECHNICAL_MANUAL.md +++ /dev/null @@ -1,1143 +0,0 @@ -# PM Ops Map — Technical Manual - -> Written for the high-context learner: big picture first, code last, analogies everywhere. - ---- - -## How to Read This Manual - -Every chapter follows the same 10-step sequence: - -1. **Big Picture** — what problem does this exist to solve? -2. **System Context** — where does it fit? -3. **Architecture** — what are the pieces and how do they talk? -4. **Constants vs Variables** — what never changes vs what does? -5. **Mental Model** — one analogy that nails the mechanism -6. **Visualize the Flow** — step-by-step input → process → output -7. **Technical Details** — code, syntax, and exact definitions -8. **WHY** — why this rule exists and what breaks without it -9. **Check Your Understanding** — conceptual questions before memorizing -10. **Practice** — progressively harder examples - -Read chapters 1 and 2 completely before skipping to any specific module chapter. They explain the foundations everything else builds on. - ---- - ---- - -# Chapter 1 — The Big Picture - -## 1.1 What Problem Does This Solve? - -A property management company on day one typically has zero documented operating structure. No record of who owns what. No maintenance tracking. No organized team responsibilities. They run on verbal agreements, group texts, and someone's personal spreadsheet. - -That spreadsheet breaks the moment the company has a second employee. - -PM Ops Map exists to solve the **zero-to-structure** problem. You open it, enter your company name, and you instantly have: - -- A complete documented map of 17 standard PM departments and 260+ tasks -- A way to assign ownership of every task to a real team member -- A maintenance work order pipeline -- A property, tenant, and vendor registry -- An exportable operations handbook you can hand to a new hire on day 1 - -The entire thing runs in a browser. No accounts. No monthly fee. No server. No database. - ---- - -## 1.2 System Context — Where Does It Live? - -``` -[GitHub Repository] - ↓ -[GitHub Pages — serves static files] - ↓ -[Your Browser — runs the entire app] - ↓ -[localStorage — saves all your data inside the browser] -``` - -The app is a **static website** — a folder of HTML, CSS, and JavaScript files. When you open it: - -1. The browser downloads the files from GitHub Pages (or your hard drive) -2. JavaScript runs entirely inside your browser tab -3. Your data is saved to `localStorage` — a private key-value store built into every browser -4. Nothing goes to a server. No API calls. No database. - -> **Analogy:** Think of it like a really smart form that saves itself inside the browser window. The form is smart enough to be a whole app, but it never phones home. - ---- - -## 1.3 The Core Constraint (and Why It Matters) - -Because there is no backend, **everything is the browser's responsibility**. This shapes every architectural decision in the codebase: - -| Need | Solution | -|------|----------| -| Save user data | `localStorage` | -| Load default structure | `config.json` (fetched once on startup) | -| Export data | Generate a file in-browser and trigger a download | -| Share data between devices | Copy/paste a JSON blob via clipboard | -| Undo an action | Keep one "snapshot" in memory before the action | - -Every feature in this app is ultimately a creative answer to "how do we do this without a server?" - ---- - -## 1.4 Check Your Understanding - -Before moving on, answer these without looking back: - -1. Where is user data stored — on a server or in the browser? -2. What happens to your data if you open the app in a different browser on the same computer? -3. Why does the app need `config.json` at all if it uses `localStorage`? - -*(Answers: 1. Browser localStorage. 2. It's gone — localStorage is per-browser, per-device. 3. localStorage holds your edits; config.json holds the original structure to start from on first load.)* - ---- - ---- - -# Chapter 2 — Architecture - -## 2.1 Big Picture - -The app is organized as a set of **ES Modules** — JavaScript files that each handle one concern and share data by importing from each other. - -> **Analogy:** Think of the app as an office building. Each floor (module) handles a specific department. To get something done, you go to the right floor. Floors can send messages to each other, but each floor manages its own work. - ---- - -## 2.2 The Module Map - -``` -config.json ← the master rulebook (not a JS module — just data) - -state.js ← the front desk (everyone comes here to read shared data) -utils.js ← the toolbox room (pure functions, no side effects) -storage.js ← the file room (reads and writes to localStorage) -ui.js ← the lobby (stats bar, notifications, onboarding) -io.js ← the loading dock (import/export/clipboard/undo) -handbook.js ← the print room (generates the operations handbook) -launchPlan.js ← the onboarding office (first-week setup guide) -templates.js ← the forms cabinet (role templates, SOP text, demo data) -stateSchema.js ← the customs inspector (validates imported data) - -views/tracking.js ← Floor 1: task tracker (the main view) -views/map.js ← Floor 2: org flow diagram (SVG visualization) -views/team.js ← Floor 3: team manager + auto-assign engine -views/workorders.js ← Floor 4: maintenance pipeline (kanban board) -views/portfolio.js ← Floor 5: property/tenant/vendor registry -views/recurring.js ← Floor 6: recurring work order templates - -app.js ← the building directory (imports everything, boots the app) -index.html ← the physical building (HTML structure, all the panels/modals) -css/style.css ← the interior design (fonts, colors, layout, animations) -``` - ---- - -## 2.3 The Dependency Rule (The Most Important Rule in the Codebase) - -**No circular dependencies.** Module A can import from module B, but then B cannot import from A. The arrows in the module graph can only point downward. - -``` -app.js - ↓ imports from -views/*.js, io.js, handbook.js, launchPlan.js, recurring.js - ↓ import from -state.js, storage.js, utils.js, ui.js - ↓ import from -state.js, utils.js ← these two import nothing else -``` - -> **WHY:** Circular imports in JavaScript ES modules cause one of the modules to receive `undefined` for some of its imported values, because JavaScript tries to resolve the import before the other module has finished loading. It's like two people each waiting for the other to walk through the door first — nobody goes anywhere. You'd get a bug that's very hard to diagnose. - -> **Analogy:** It's like a company org chart — your manager can give you work, you can ask your assistant for help, but your assistant can't also be your manager's manager. The hierarchy has to be a one-way tree. - ---- - -## 2.4 The window Globals Pattern - -HTML has inline event handlers like this: - -```html - -``` - -For `cycleTaskStatus` to work here, it has to exist on the global `window` object. But ES modules are **private by default** — they don't automatically put their functions on `window`. - -The solution: at the bottom of `app.js`, every function that an HTML button might call is explicitly registered: - -```javascript -Object.assign(window, { - cycleTaskStatus, - renderTrackingView, - // ... 60+ more -}); -``` - -> **Analogy:** It's like the building PA system. Each office (module) has its own phone system, but only the functions registered with the front desk (window) can be paged building-wide. If you add a new function that a button calls, you must register it at the front desk or the button silently does nothing. - -> **WHY this rule hurts when broken:** When a function isn't on `window`, clicking the button produces no error in the console — the browser just finds `undefined` and calls nothing. It's a silent failure, which makes it one of the most frustrating bugs to diagnose. - ---- - -## 2.5 Constants vs Variables - -**What never changes (constants):** - -| Item | Where | Value | -|------|-------|-------| -| Storage key names | `storage.js` | `'pm-ops-data-v1'`, `'pm-ops-team-v1'`, etc. | -| Status cycle order | `state.js` | `['todo', 'in-progress', 'blocked', 'done']` | -| Priority cycle order | `state.js` | `['high', 'medium', 'low']` | -| Color palette | `state.js` | 20 hardcoded hex codes | -| Department structure | `config.json` | (loaded once, then editable in memory) | - -**What changes constantly:** - -| Item | Lives in | Changes when | -|------|----------|--------------| -| Task owners, statuses, due dates | `orgData.departments[].tasks[]` | User edits in Tracking view | -| Team roster | `teamData.employees[]` | User adds/removes employees | -| Work orders | `workOrders[]` | User creates/advances/deletes work orders | -| Portfolio | `portfolio.properties/tenants/vendors[]` | User edits portfolio | -| Audit log | `auditLog[]` | Any meaningful action | - ---- - -## 2.6 Check Your Understanding - -1. If you add a new function `openSpecialPanel()` to `views/tracking.js` and hook it to a button in `index.html`, what two things do you need to do in `app.js`? -2. Why can't `state.js` import from `storage.js`? -3. What happens to a circular dependency at runtime? - -*(Answers: 1. Import it in app.js, then add it to Object.assign(window, {...}). 2. It would create a circular dependency since storage.js already imports from state.js. 3. One module receives undefined for its imported values, causing silent bugs.)* - ---- - ---- - -# Chapter 3 — The Startup Sequence - -## 3.1 Mental Model - -> **Analogy:** Imagine a restaurant opening for the day. There's a strict order: unlock the doors, check the inventory (config.json), brief the staff (state.js), load yesterday's orders (localStorage), set the tables (render views), and only then let customers in. Skip any step and the whole service is off. - -## 3.2 The Startup Flow - -``` -Browser loads index.html - ↓ -Browser parses " - ↓ -"<script>evil()</script>" -``` - -> **WHY this is critical:** Every time user-provided text (task names, employee names, notes, company name) is inserted into an HTML template literal, it must be escaped. Without this, a user who types `` as a task name could execute JavaScript in the browser — a classic XSS (Cross-Site Scripting) attack. Even though this is a single-user local app, the rule is important for when it's shared or hosted. - -> **Analogy:** It's like the security guard at the door. Every piece of user input that wants to enter the HTML building must be checked: angle brackets get rewritten as harmless text (`<`), quotation marks too (`"`). Nothing sneaks in as executable markup. - -### `isValidISODate(str)` - -Returns `true` only for valid `YYYY-MM-DD` strings: - -```javascript -isValidISODate('2026-06-26') // true -isValidISODate('2026-13-01') // false — month 13 doesn't exist -isValidISODate('tomorrow') // false -isValidISODate(null) // false -``` - -The implementation parses the date twice: once with a regex for format, and once by constructing a `Date` object and checking that `toISOString()` round-trips back to the same string. This catches edge cases like February 30 that pass the regex but aren't real dates. - -### `_downloadBlob(content, mimeType, filename)` - -The only way to trigger a file download from JavaScript in a browser without a server. Creates a temporary object URL, attaches it to a hidden anchor tag, clicks it, then immediately cleans up. - -### `shakeInput(el)` - -Adds the CSS class `shake` to an element for 400ms, triggering a CSS animation that shakes the element left-right. Used as the "validation failed" signal — instead of an alert box, the offending input shakes. - ---- - ---- - -# Chapter 12 — The XSS Defense Layer - -## 12.1 The Problem - -The app renders user data into HTML constantly. Task names, employee names, company names, work order notes — all of these appear in template literals like: - -```javascript -`
${task.name}
` -``` - -If `task.name` were ``, that HTML would execute. - -## 12.2 The Solution — escapeHtml() Everywhere - -Every place user data enters an HTML template literal, `escapeHtml()` wraps it: - -```javascript -`
${escapeHtml(task.name)}
` -``` - -The `escapeHtml` function is imported at the top of every view file. It's a convention that every contributor must follow. - -## 12.3 The jsonAttr() Function - -There's a trickier case: function calls inside HTML attributes: - -```javascript -// Dangerous — if task.name contains quotes, this breaks the attribute -`` - -// Safe — jsonAttr() JSON-encodes the value and escapes quotes for HTML attribute context -`` -``` - -`jsonAttr()` double-encodes: first `JSON.stringify` (adding surrounding quotes and escaping internal quotes), then replaces `"` with `"` for the HTML attribute context. - ---- - ---- - -# Chapter 13 — The CI Pipeline - -## 13.1 What CI Does - -CI (Continuous Integration) runs automatically on every push and pull request. It catches problems before they reach the live site. - -``` -git push to GitHub - ↓ -GitHub Actions triggers .github/workflows/ci.yml - ↓ -Step 1: npm ci ← install exact dependency versions from package-lock.json -Step 2: npm test ← run Jest unit tests -Step 3: npm audit --omit=dev --audit-level=moderate ← check for known vulnerabilities -Step 4: npm run build ← webpack production bundle (catches import errors) -``` - -## 13.2 Why --omit=dev - -All packages in this project are `devDependencies` — webpack, jest, webpack-dev-server. The deployed app is just static HTML/CSS/JS with zero npm packages at runtime. - -`npm audit` originally failed because `webpack-dev-server` (a dev tool) had vulnerabilities. Adding `--omit=dev` tells npm audit to only check runtime dependencies — of which there are none. - -> **Analogy:** Auditing a restaurant for health code violations shouldn't include the chef's personal car — only the kitchen equipment matters. `--omit=dev` tells the auditor to check only the kitchen (runtime), not the parking lot (devDependencies). - -## 13.3 The Jest Tests - -Four test files cover the pure logic: - -| File | What it tests | -|------|--------------| -| `utils.test.js` | `escapeHtml`, `isValidISODate`, `getTodayISO`, `formatDueChip`, etc. — 37 tests | -| `data.test.js` | `config.json` structure — every department has required fields, every task has name + owner | -| `stateSchema.test.js` | Import/export schema version and validation report | -| `templates.test.js` | Role template and SOP template structure | - -The `.cjs` mirror files (`utils.cjs`, `stateSchema.cjs`, `templates.cjs`) exist because Jest runs in CommonJS mode and can't natively import ES module `export` syntax. The mirrors use `module.exports =` instead of `export`. When you change `utils.js`, you must mirror the change in `utils.cjs` or the tests will test old behavior. - ---- - ---- - -# Chapter 14 — How to Add a New Feature - -This chapter walks through a real extension example to cement all the concepts above. - -## 14.1 Example: Add a "Star" Button to Tasks - -**Goal:** Let users star important tasks. Starred tasks get a ⭐ icon. Stars persist. - -**Step 1: Add the field to the data model** - -No code change needed — `orgData` tasks are plain objects. You can add any field you want in JavaScript. The field just needs to be saved and loaded. - -**Step 2: Save the field in storage.js** - -In `saveToStorage()`, add `starred` to the task payload: - -```javascript -tasks: dept.tasks.map(t => ({ - // ... existing fields - starred: t.starred || false, -})) -``` - -In `loadFromStorage()`, merge it: - -```javascript -if (typeof savedTask.starred === 'boolean') task.starred = savedTask.starred; -``` - -**Step 3: Render the button in tracking.js** - -Inside the task row template in `renderTrackingView()`: - -```javascript - -``` - -**Step 4: Add the handler function in tracking.js** - -```javascript -export function toggleTaskStar(deptId, taskIdx) { - const dept = orgData.departments.find(d => d.id === deptId); - if (!dept) return; - const task = dept.tasks[taskIdx]; - if (!task) return; - task.starred = !task.starred; - saveToStorage(); - _updateStarIcon(deptId, taskIdx, task); // update just the button, no full re-render -} -``` - -**Step 5: Register in app.js** - -Add to the import: -```javascript -import { ..., toggleTaskStar } from './views/tracking.js'; -``` - -Add to `Object.assign(window, {...})`: -```javascript -toggleTaskStar, -``` - -**Step 6: Style in style.css** - -```css -.task-star-btn { opacity: 0.3; background: none; border: none; cursor: pointer; } -.task-star-btn--starred { opacity: 1; } -``` - -That's a complete feature addition. No backend. No schema migration. No build step required to test it. - ---- - -## 14.2 Check Your Understanding (Final) - -1. Why does `toggleTaskStar` need to be in `Object.assign(window, {...})`? -2. Why does `saveToStorage()` need to explicitly include `starred` in its payload? -3. If you forget to add `starred` to `loadFromStorage()`, what happens to the user's stars after a page refresh? -4. Why is it safe to add a new field to task objects in JavaScript without any "migration"? -5. What would happen if you called `renderTrackingView()` instead of `_updateStarIcon()` inside `toggleTaskStar()`? - -*(Answers: 1. HTML onclick handlers can only call window-level functions. 2. saveToStorage builds a fresh serialized snapshot — anything not included gets lost. 3. They're lost — loadFromStorage doesn't know to restore the field. 4. JS objects are dynamic; adding a field to an in-memory object is instant. Old saved data without the field is handled by the `|| false` default in the load logic. 5. It would work but cause a full re-render of 260 tasks — much slower than updating one button's class. Both are correct; one is efficient.)* - ---- - ---- - -# Quick Reference - -## Storage Keys - -| Key | Description | -|-----|-------------| -| `pm-ops-data-v1` | Task state | -| `pm-ops-team-v1` | Team roster | -| `pm-ops-workorders-v1` | Work orders | -| `pm-ops-portfolio-v1` | Properties, tenants, vendors | -| `pm-ops-audit-v1` | Audit log | -| `pm-ops-company-name` | Company name | -| `pm-ops-profile-v1` | Operations focus + portfolio size | -| `pm-ops-nav-compact` | Nav hide/show state | -| `pm-ops-guide-dismissed` | Welcome guide dismissed | -| `pm-ops-notif-date` | Last notification date | -| `pm-ops-launch-checklist-v1` | Launch checklist progress | -| `pm-ops-backups-v1` | Last 5 auto-backup snapshots | - -## Status Values - -`todo` → `in-progress` → `blocked` → `done` - -## Priority Values - -`high` → `medium` → `low` - -## Work Order Status Values - -`submitted` → `scheduled` → `in-progress` → `completed` - -## Module Responsibility - -| Module | Owns | -|--------|------| -| `state.js` | Shared data + constants | -| `storage.js` | localStorage read/write + toasts + backups | -| `utils.js` | Pure functions (escape, date, download, shake) | -| `ui.js` | Stats bar + notifications + onboarding modal | -| `io.js` | Export/import + clipboard + undo | -| `handbook.js` | Markdown + HTML handbook generator | -| `launchPlan.js` | Beginner setup guide + data quality checks | -| `templates.js` | Role templates + SOP text + demo data | -| `stateSchema.js` | Import validation | -| `views/tracking.js` | Task list + filters + bulk mode + notes + custom fields | -| `views/map.js` | SVG org flow diagram | -| `views/team.js` | Team roster + auto-assign engine | -| `views/workorders.js` | Work order kanban board | -| `views/portfolio.js` | Property/tenant/vendor registry | -| `views/recurring.js` | Recurring work order templates | -| `app.js` | Entry point + window globals + view switcher | - ---- - -*End of Technical Manual — PM Ops Map*