DOC20_ADDENDUM_B_MS_WORD_VIEWER_CAPABILITIES_R3.md
Current Specs/DOC20/DOC20_ADDENDUM_B_MS_WORD_VIEWER_CAPABILITIES_R3.md
# DOC20 Addendum B: Document Viewer & Editor Capabilities (R3)
**Date:** 2026-04-10
**Version:** R3 — copy-on-write system policy for agent edits, tracked changes always in copies, enforcement at system level
**Status:** Front-end build specification — designed for immediate implementation before backend/EC specs are complete
**Scope:** Document viewing and editing capabilities for .docx (and .pdf) files within Q Dashboard's Electron app
**Dependencies:** OnlyOffice Document Server (bundled), Microsoft Graph API, Azure AD app registration
---
## 0. Implementation Context
**This addendum is intended for the front-end build phase.** It does not rely on any ELNOR backend infrastructure (EC, OpenClaw, DOC16 M365 integration). Everything described here runs locally in Electron or connects directly to Microsoft's APIs using OAuth tokens stored locally.
When the full backend is built later:
- DOC16 Entry 16.7 will add Elnor's `elnor@schallfirm.com` agentic identity for M365 — this layers on top without changing anything built here
- EC will manage document state and metadata — the Document Viewer component doesn't change, it just gets richer context
- DOC72 will extract entities from documents — this reads the same files Q already accesses
**Nothing built in this addendum needs to be reworked later.**
---
## 1. Architecture Overview
### 1.1 Three-Tier Document Editor
When a user clicks a .docx file anywhere in Q (browser results, Places, bookmarks bar, file drop, chat attachment), the Document Viewer opens with OnlyOffice as the default editor. Two additional editing options are available via toolbar buttons.
| Tier | Engine | Capability | When to Use |
|------|--------|-----------|-------------|
| **Default Editor** | OnlyOffice Document Editor | Full local editing — tracked changes, comments, styles, line numbering, tables, footers, headers. Works offline. No internet required. | Default on click. Handles 90% of cases. |
| **Word Online** | Microsoft Word Online (in Q web tab) | Full Microsoft rendering engine. Perfect formatting fidelity. Requires internet + OneDrive. | Click "Open in Word Online" button. For formatting-critical final documents. |
| **Desktop Word** | Native Microsoft Word app | Full native Word. Launches externally. | Click "Open in Desktop Word" button. Nuclear option. |
### 1.2 File Location Detection
Every .docx file falls into one of three categories based on its local path:
| Location | Detection | OnlyOffice Edit | Word Online Edit |
|----------|-----------|-----------------|-----------------|
| **Firm OneDrive** | Path contains `OneDrive-SharedLibraries-schallfirm.com` | ✅ Opens immediately (local file) | ✅ Resolves to SharePoint URL |
| **Personal OneDrive** | Path contains `OneDrive-Personal` or `OneDrive` (user's personal sync) | ✅ Opens immediately (local file) | ✅ Resolves to SharePoint URL |
| **Local-only** | Path does not match any OneDrive sync folder | ✅ Opens immediately (local file) | ✅ Auto-uploads to ELNOR Working folder first |
Note: OnlyOffice opens ALL files immediately regardless of location. Word Online requires OneDrive, so local-only files need uploading first.
### 1.3 PDF Viewing
| Format | Engine | Capabilities |
|--------|--------|-------------|
| .pdf | PDF.js | View, zoom, search, text selection, clip-to-note, print. No annotation (future: evaluate PSPDFKit for annotation/redaction). |
---
## 2. OnlyOffice Integration
### 2.1 Architecture
OnlyOffice has two components that must be bundled with Q:
1. **OnlyOffice Document Server** — a backend service (Node.js) that handles document rendering, conversion, and real-time collaboration. Runs as a child process on localhost.
2. **OnlyOffice Document Editor** — a web-based frontend loaded in an iframe, pointed at the local Document Server.
```
Q Electron App
├── Main Process
│ ├── Electron BrowserWindow
│ └── OnlyOffice Document Server (child process, localhost:8443)
└── Renderer Process
└── Document Viewer Component
└── <iframe src="http://localhost:8443/editor?file=..." />
```
### 2.2 Document Server Lifecycle
The Document Server runs as a managed child process:
```typescript
// src/services/onlyoffice.ts
import { spawn, ChildProcess } from "child_process";
import path from "path";
let serverProcess: ChildProcess | null = null;
const ONLYOFFICE_PORT = 8443;
export async function startDocumentServer(): Promise<void> {
if (serverProcess) return; // Already running
const serverPath = path.join(
process.resourcesPath, // Electron resources dir
"onlyoffice-server",
"server.js"
);
serverProcess = spawn("node", [serverPath], {
env: {
...process.env,
PORT: String(ONLYOFFICE_PORT),
DATA_DIR: path.join(app.getPath("userData"), "onlyoffice-data"),
},
stdio: "pipe",
});
// Wait for server to be ready
await waitForPort(ONLYOFFICE_PORT, 10000); // 10s timeout
}
export function stopDocumentServer(): void {
if (serverProcess) {
serverProcess.kill("SIGTERM");
serverProcess = null;
}
}
// In main.ts:
app.on("ready", async () => {
await startDocumentServer();
createMainWindow();
});
app.on("before-quit", () => {
stopDocumentServer();
});
```
**Startup sequencing:**
1. Electron app launches
2. Main process starts OnlyOffice Document Server as child process
3. Waits for Document Server to be ready (port 8443 responding)
4. Creates main BrowserWindow
5. Document Viewer iframe points to localhost:8443
**Port allocation:** Use port 8443 (configurable). If the port is in use, try 8444, 8445, etc.
**Graceful shutdown:** On app quit, send SIGTERM to the Document Server process. It shuts down cleanly.
### 2.3 Document Editor Integration
The Document Viewer component embeds OnlyOffice via iframe with the JavaScript API:
```typescript
// src/components/Content/DocumentView.tsx
import { useEffect, useRef } from "react";
interface OnlyOfficeConfig {
document: {
fileType: string; // "docx", "xlsx", "pptx"
key: string; // Unique document key (hash of file path + modified time)
title: string; // Filename
url: string; // Local file URL served by Document Server
};
editorConfig: {
mode: "edit"; // Always edit mode — OnlyOffice handles viewing too
callbackUrl: string; // Callback for save events
user: {
id: string;
name: string; // "Will Schall"
};
customization: {
autosave: true;
chat: false; // Disable OnlyOffice's built-in chat (we have our own)
comments: true; // Enable document comments
compactHeader: true; // Compact toolbar for integration
compactToolbar: true;
feedback: false;
forcesave: true; // Allow manual save trigger
help: false;
hideRightMenu: false;
logo: { image: "", imageEmbedded: "" }, // Remove OnlyOffice branding
reviewDisplay: "markup", // Show tracked changes
toolbarNoTabs: false; // Show full tabbed toolbar
uiTheme: "theme-light", // Match Q's light theme
};
};
}
```
### 2.4 Theming
OnlyOffice supports custom themes. Match Q's design language:
```typescript
customization: {
uiTheme: "theme-light",
// OnlyOffice supports CSS customization via the Document Server config
// Override colors to match Q's palette:
// Primary: #31588c (accentBtn)
// Background: #FFFFFF (bgPanel)
// Text: #1A1D21 (textPri)
}
```
The editor will look like a native part of Q, not a foreign widget.
### 2.5 File Operations
**Opening a file:**
```typescript
async function openInOnlyOffice(filePath: string): Promise<OnlyOfficeConfig> {
const fileName = path.basename(filePath);
const fileExt = path.extname(filePath).slice(1); // "docx"
const fileKey = await computeFileKey(filePath); // hash of path + mtime
// Document Server needs to access the file. Options:
// 1. Serve the file via a local HTTP endpoint
// 2. Copy to Document Server's data directory
// We use option 1: serve via Electron's protocol handler
return {
document: {
fileType: fileExt,
key: fileKey,
title: fileName,
url: `http://localhost:${ONLYOFFICE_PORT}/files/${encodeURIComponent(filePath)}`,
},
editorConfig: {
mode: "edit",
callbackUrl: `http://localhost:${ONLYOFFICE_PORT}/callback`,
user: { id: "will", name: "Will Schall" },
customization: { /* ... as above */ },
},
};
}
```
**Saving:**
OnlyOffice calls the `callbackUrl` when the user saves or the document auto-saves. The callback handler writes the updated file back to the original path:
```typescript
// Document Server callback handler
app.post("/callback", async (req, res) => {
const { status, url, key } = req.body;
if (status === 2) { // Document saved
// Download the updated file from Document Server
const updatedFile = await fetch(url);
const buffer = await updatedFile.buffer();
// Write back to original path
const originalPath = resolveKeyToPath(key);
await fs.writeFile(originalPath, buffer);
// If file is on OneDrive, sync happens automatically via OneDrive client
}
res.json({ error: 0 }); // Acknowledge
});
```
### 2.6 Bundling OnlyOffice
**Installation:** OnlyOffice Document Server can be installed via npm or as a standalone package.
```bash
# Option 1: npm package (lighter, but may need additional setup)
npm install @onlyoffice/documentserver
# Option 2: Download standalone server
# Package into app/resources/onlyoffice-server/ during build
```
**Build integration:** In `electron-builder.yml`:
```yaml
extraResources:
- from: "onlyoffice-server/"
to: "onlyoffice-server"
filter:
- "**/*"
```
**Size:** ~200MB for the Document Server. The total Q Dashboard app will be ~250-300MB (Electron ~100MB + OnlyOffice ~200MB). For comparison, Microsoft Word is ~2GB. This is not a concern.
**Performance:** The Document Server is idle when no document is open. It uses ~50MB RAM at rest. When editing a document, RAM usage is proportional to document complexity — typically 100-300MB for a legal brief. This does not affect Q's normal operation (browsing, chatting, notes) because the server is a separate process.
---
## 3. OneDrivePathResolver
### 3.1 Purpose
A utility module that maps local file paths to Word Online editing URLs. Used when the user clicks "Open in Word Online" for full Microsoft rendering fidelity.
### 3.2 File Path → Account Detection
```typescript
interface OneDriveAccount {
id: string; // "firm" | "personal"
displayName: string; // "Schall Firm" | "Personal"
syncFolder: string; // Local sync root path
driveId: string; // Microsoft Graph drive ID
tenantId: string; // Azure AD tenant ID
accessToken: string; // OAuth token (refreshable)
}
const KNOWN_SYNC_ROOTS = [
{
pattern: /OneDrive-SharedLibraries-schallfirm\.com/,
accountId: "firm",
},
{
pattern: /OneDrive(?:-Personal)?(?!-SharedLibraries)/,
accountId: "personal",
},
];
function detectFileAccount(filePath: string): OneDriveAccount | null {
for (const root of KNOWN_SYNC_ROOTS) {
if (root.pattern.test(filePath)) {
return accounts.find(a => a.id === root.accountId) || null;
}
}
return null; // Local-only file
}
```
### 3.3 OneDrive Path → Word Online URL Resolution
```typescript
async function resolveToWordOnlineUrl(filePath: string, account: OneDriveAccount): Promise<string> {
const relativePath = filePath.replace(account.syncFolder, "");
const encodedPath = relativePath.split("/").map(encodeURIComponent).join("/");
// Use Graph API to get the item's webUrl
const response = await fetch(
`https://graph.microsoft.com/v1.0/drives/${account.driveId}/root:${encodedPath}`,
{ headers: { Authorization: `Bearer ${account.accessToken}` } }
);
const item = await response.json();
return `${item.webUrl}?action=edit`;
}
```
### 3.4 Local-Only File Upload
```typescript
async function uploadAndEdit(filePath: string): Promise<string> {
const targetAccount = getUploadTargetAccount(); // Setting: "firm" | "personal"
const fileBuffer = await fs.readFile(filePath);
const fileName = path.basename(filePath);
// PUT creates the ELNOR Working folder automatically if it doesn't exist
const uploadUrl = `https://graph.microsoft.com/v1.0/drives/${targetAccount.driveId}/root:/ELNOR Working/${encodeURIComponent(fileName)}:/content`;
const response = await fetch(uploadUrl, {
method: "PUT",
headers: {
Authorization: `Bearer ${targetAccount.accessToken}`,
"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
},
body: fileBuffer,
});
const item = await response.json();
return `${item.webUrl}?action=edit`;
}
```
### 3.5 Working Folder
**Name:** `ELNOR Working` (root of target OneDrive)
**Auto-creation:** Graph API creates intermediate folders automatically on PUT.
**Which account:** Configurable in Settings. Default: personal OneDrive.
**Cleanup:** See §7.3.
---
## 4. Document Viewer Toolbar
### 4.1 Toolbar Layout
When a .docx is open in the Document Viewer:
```
[Open in Word Online] [Open in Desktop Word] | [Save] [Save As] [Print] | [Clip] [Ask Elnor] | [Comments (3)] [Zoom ±]
```
- **Open in Word Online** — resolves file to Word Online URL, opens in Q web tab. If local-only file, uploads first (shows "Uploading..." toast, ~2-5 sec).
- **Open in Desktop Word** — `shell.openPath(filePath)` launches native Word.
- **Save** — triggers OnlyOffice force-save callback, writes file back to original path.
- Other buttons are the existing DOC20 §6.16 Document Viewer toolbar.
### 4.2 Button States
| File Location | OnlyOffice (default) | Open in Word Online | Open in Desktop Word |
|---------------|---------------------|--------------------|--------------------|
| Firm OneDrive | ✅ Opens immediately | ✅ Opens immediately | ✅ Opens immediately |
| Personal OneDrive | ✅ Opens immediately | ✅ Opens immediately | ✅ Opens immediately |
| Local-only | ✅ Opens immediately | ✅ Uploads first (~2-5s) | ✅ Opens immediately |
| No internet | ✅ Works (local) | ❌ Grayed out, tooltip: "Requires internet" | ✅ Works |
---
## 5. Multi-Account Handling
### 5.1 Two OneDrive Accounts
Will uses two Microsoft accounts:
- **Firm:** `wbrody@schallfirm.com` — OneDrive for Business via SharePoint
- **Personal:** personal Microsoft account — OneDrive Personal
Both accounts sync to separate local folders on macOS. Q maintains separate OAuth tokens for each.
### 5.2 Account Detection per File
The local file path determines which account to use automatically:
```
~/Library/CloudStorage/OneDrive-SharedLibraries-schallfirm.com/Paramount/Expert_Reports/Sanli.docx
→ Firm account → firm SharePoint URL
~/Library/CloudStorage/OneDrive-Personal/Legal Research/Brief Draft.docx
→ Personal account → personal OneDrive URL
~/Desktop/Local Notes/scratch.docx
→ No account → upload to [configured account]/ELNOR Working/
```
### 5.3 Browser Session Management
Word Online uses Microsoft account sessions in Q's web browser (Electron webview). You can be signed into both accounts simultaneously in different tabs. If not signed in, Microsoft prompts for sign-in. Q doesn't need to handle this — it's standard browser behavior.
---
## 6. Elnor Document Integration
### 6.1 Copy-on-Write System Policy
**HARD RULE: Elnor NEVER modifies the original file. All agent edits produce a copy.**
This is a **system-level policy**, not a UI button. It doesn't matter how the user asks Elnor to edit a document — chat panel, Ask Elnor panel, comments, floating palette, standing instruction, email command, room conversation. Any time any agent's action results in modifying a .docx file, the system intercepts and enforces the copy-on-write rule:
1. Elnor reads the original file (e.g., `Sanli_Expert_Report.docx`)
2. Elnor makes modifications programmatically
3. Elnor writes the result as a **new file** in the same directory: `Sanli_Expert_Report_E1.docx`
4. If Elnor edits again (from the original or from E1), the next copy is `_E2.docx`
5. The original file is never touched
**Copy naming convention:**
```
{original_filename}_E{N}.{ext}
Examples:
Sanli_Expert_Report.docx → Sanli_Expert_Report_E1.docx
Sanli_Expert_Report_E1.docx → Sanli_Expert_Report_E2.docx
Brief_Draft.docx → Brief_Draft_E1.docx
```
The `E` prefix stands for "Elnor edit." The counter increments by scanning the directory for existing `_E{N}` files with the same base name.
**Where the copy is saved:**
- Same directory as the original file
- If the original is on OneDrive, the copy appears there too (via local sync)
- If the original is local-only, the copy is local-only
### 6.2 Tracked Changes — Mandatory in Copies
**All edits in the copy MUST be written as tracked changes.** The copy is not a clean rewrite — it's the original document with Elnor's modifications marked as tracked revisions.
When the user opens `_E1.docx` in OnlyOffice, Word Online, or desktop Word, they see:
- The full original document content
- Elnor's additions highlighted as insertions (author: "Elnor", timestamp)
- Elnor's deletions shown as strikethroughs
- Accept/reject each change individually, like any human reviewer's tracked changes
**Implementation:** Use the `docx` npm library (or `python-docx` on the backend) to write tracked change XML. The .docx format stores tracked changes as `<w:ins>` (insertion) and `<w:del>` (deletion) elements with `w:author` and `w:date` attributes.
```typescript
import { Document, Paragraph, TextRun, TrackRevisionInsert } from "docx";
const revision = new TrackRevisionInsert({
author: "Elnor", // Or the active agent's display name
date: new Date().toISOString(),
children: [
new TextRun("This text was added by Elnor as a tracked change."),
],
});
```
### 6.3 Elnor Reading Documents
Elnor reads .docx files directly from disk. This works now without any backend — Elnor can read any file the Electron app can access. For OneDrive-synced files, Elnor reads the local sync copy. Reading is always allowed regardless of edit policy settings.
### 6.4 Elnor Editing Workflow — End to End
Regardless of how the user initiates the request:
1. User asks (via any channel): "Revise paragraph 3 to strengthen the loss causation argument"
2. If `require_confirmation` is enabled in Settings → toast dialog: "Elnor wants to edit Sanli_Expert_Report.docx. Allow?" → [Allow] [Block]
3. If allowed (or confirmation is disabled):
a. Elnor reads `Sanli_Expert_Report.docx` from disk
b. Elnor identifies paragraph 3 and generates the revision
c. Elnor scans directory for existing `_E{N}` files → determines next number (e.g., E1)
d. Elnor writes `Sanli_Expert_Report_E1.docx` with all modifications as tracked changes
e. Toast notification: "Elnor created Sanli_Expert_Report_E1.docx — 3 tracked changes" (clickable → opens the file)
f. If `auto_open_copy` is enabled → Q opens the copy in the Document Viewer
4. User reviews tracked changes in OnlyOffice (or Word Online / desktop Word)
5. User accepts/rejects changes
6. If satisfied, user can rename E1 to replace the original (manual action, never automatic)
**Channels that can trigger this workflow:**
- Chat panel message: "Edit the expert report to fix the discount rate section"
- Ask Elnor panel with document context
- Right-panel comment: "@Elnor revise this paragraph"
- Floating palette chat
- Standing instruction / automated task
- Email instruction (via DOC16 email processing)
- Room conversation where a document is the review target
All channels go through the same copy-on-write enforcement.
### 6.5 Elnor Commenting
Elnor can add comments to documents via two mechanisms:
1. **Q's right-panel comment system** (DOC20 §6.6) — comments anchored to text positions, visible alongside the document in Q's comment panel. These are Q-internal and not written into the .docx file. No copy-on-write needed — Q comments don't modify the file.
2. **Document-internal comments** — Elnor writes comments directly into the .docx file's comment XML. These appear in OnlyOffice, Word Online, and desktop Word as native document comments. **Document-internal comments follow the copy-on-write rule** — comments are written to a copy (`_E{N}.docx`), not the original. This ensures the original is never modified even by comment insertion.
Both can coexist. Q comments for analysis workflow (fast, non-destructive); document comments for collaboration (when the file will be shared with others).
### 6.6 Edit Notifications
When Elnor creates an edited copy:
1. **Toast notification:** "Elnor created [filename]_E1.docx — [N] tracked changes"
- Clickable: opens the copy in the Document Viewer
- Persists for 5 seconds (longer than standard 1.8s toast because it's actionable)
2. **Notices inbox:** Added to the Floating Palette's Notices inbox (DOC20 §6.25) with:
- Source: "Elnor"
- Title: "Document edited: [filename]"
- Description: "[N] tracked changes in [filename]_E1.docx"
- Action: click to open
3. **If the original document is open in the Document Viewer:** A banner appears at the top: "Elnor created a revised copy: [filename]_E1.docx — [Open copy]"
4. **Activity feed:** Logged as an activity event visible in the Activity page
### 6.7 System-Level Enforcement
The copy-on-write policy is enforced at the system level, not in individual UI components:
```typescript
// src/services/agentFilePolicy.ts
interface AgentEditPolicy {
enabled: boolean; // Master toggle — if false, all agent edits blocked
mode: "always_copy" | "in_place"; // Default: "always_copy"
require_confirmation: boolean; // Show "Allow edit?" dialog
auto_open_copy: boolean; // Auto-open the copy after creation
copy_suffix: string; // Default: "_E"
}
async function enforceEditPolicy(
agentName: string,
originalPath: string,
editFn: (inputBuffer: Buffer) => Promise<Buffer>
): Promise<{ outputPath: string; changeCount: number }> {
const policy = getAgentEditPolicy();
if (!policy.enabled) {
throw new AgentEditBlockedError("Agent document editing is disabled in Settings");
}
// Confirmation dialog (if enabled)
if (policy.require_confirmation) {
const allowed = await showConfirmDialog(
`${agentName} wants to edit ${path.basename(originalPath)}. Allow?`,
["Allow", "Block"]
);
if (!allowed) throw new AgentEditBlockedError("User declined");
}
// Read original
const originalBuffer = await fs.readFile(originalPath);
// Apply edits
const editedBuffer = await editFn(originalBuffer);
// Determine output path
let outputPath: string;
if (policy.mode === "always_copy") {
const nextN = await getNextCopyNumber(originalPath, policy.copy_suffix);
const ext = path.extname(originalPath);
const base = path.basename(originalPath, ext);
outputPath = path.join(path.dirname(originalPath), `${base}${policy.copy_suffix}${nextN}${ext}`);
} else {
// "in_place" mode — advanced, not default
outputPath = originalPath;
}
// Write
await fs.writeFile(outputPath, editedBuffer);
// Count tracked changes in the output
const changeCount = countTrackedChanges(editedBuffer);
// Notifications
showToast(`${agentName} created ${path.basename(outputPath)} — ${changeCount} tracked changes`, {
duration: 5000,
onClick: () => openInDocumentViewer(outputPath),
});
addNotice({
source: agentName,
title: `Document edited: ${path.basename(originalPath)}`,
desc: `${changeCount} tracked changes in ${path.basename(outputPath)}`,
action: () => openInDocumentViewer(outputPath),
});
// Auto-open
if (policy.auto_open_copy) {
openInDocumentViewer(outputPath);
}
return { outputPath, changeCount };
}
async function getNextCopyNumber(originalPath: string, suffix: string): Promise<number> {
const dir = path.dirname(originalPath);
const ext = path.extname(originalPath);
const base = path.basename(originalPath, ext).replace(new RegExp(`${suffix}\\d+$`), "");
const files = await fs.readdir(dir);
let maxN = 0;
for (const f of files) {
const match = f.match(new RegExp(`^${escapeRegex(base)}${suffix}(\\d+)${escapeRegex(ext)}$`));
if (match) maxN = Math.max(maxN, parseInt(match[1]));
}
return maxN + 1;
}
```
Every agent editing pathway (EC commands, OpenClaw skills, direct API calls) must route through `enforceEditPolicy()`. No agent code may call `fs.writeFile()` on a document path directly.
---
## 7. Working Folder Management
### 7.1 ELNOR Working Folder
**Purpose:** Temporary storage for local-only files that need to be opened in Word Online.
**Name:** `ELNOR Working` (configurable in Settings)
**Location:** Root of the target OneDrive (firm or personal, configurable)
**Auto-creation:** Created automatically on first upload. No manual setup.
### 7.2 Auto-Sync Back
When a user edits a local-only file via Word Online (which uploaded it to ELNOR Working), changes need to sync back to the original local path.
**Implementation:**
```typescript
interface WorkingFolderEntry {
originalPath: string; // Where the file came from (e.g., ~/Desktop/brief.docx)
oneDrivePath: string; // Where it was uploaded (ELNOR Working/brief.docx)
uploadedAt: string; // ISO timestamp
driveItemId: string; // Graph API item ID for change detection
lastSyncedVersion: string; // ETag or version ID
}
// Store entries in local SQLite or JSON file
const workingFolderIndex: WorkingFolderEntry[] = [];
// After Word Online editing, detect changes and sync back
async function syncBackToOriginal(entry: WorkingFolderEntry): Promise<void> {
const account = getUploadTargetAccount();
// Check if the OneDrive version has changed
const item = await graphApi.getItem(account, entry.driveItemId);
if (item.eTag !== entry.lastSyncedVersion) {
// Download updated file from OneDrive
const content = await graphApi.downloadFile(account, entry.driveItemId);
// Write back to original local path
await fs.writeFile(entry.originalPath, content);
// Update sync record
entry.lastSyncedVersion = item.eTag;
showToast(`Synced changes back to ${path.basename(entry.originalPath)}`);
}
}
// Run sync check periodically (every 30 seconds when Word Online tab is open)
// Or trigger on tab focus change
```
### 7.3 Cleanup
Working folder files accumulate over time. Cleanup options:
**Automatic cleanup (configurable):**
```typescript
Settings > Document Editing > Working folder cleanup:
○ Never (manual only)
○ After 7 days
○ After 30 days (default)
○ After 90 days
// Cleanup runs on app startup
async function cleanupWorkingFolder(): Promise<void> {
const maxAge = getCleanupMaxAge(); // from settings
if (maxAge === "never") return;
const account = getUploadTargetAccount();
const items = await graphApi.listFolder(account, "ELNOR Working");
for (const item of items) {
const age = Date.now() - new Date(item.lastModifiedDateTime).getTime();
if (age > maxAge) {
await graphApi.deleteItem(account, item.id);
}
}
}
```
**Manual cleanup:** Settings page has a "Clean up now" button that shows the working folder contents with file sizes and ages, and lets the user select which to delete.
---
## 8. RendererCapabilities Registry
```typescript
// src/config/renderers.ts
export const renderers: Record<string, RendererConfig> = {
pdf: {
engine: "pdfjs",
capabilities: ["view", "zoom", "search", "select", "clip", "print"],
editEngine: null,
editCapabilities: [],
notes: "No inline editing. Annotation/redaction via PSPDFKit (future evaluation).",
},
docx: {
engine: "onlyoffice",
capabilities: ["view", "edit", "zoom", "search", "print", "tracked-changes", "comments", "styles"],
editEngine: "onlyoffice",
editCapabilities: ["full-formatting", "tracked-changes", "comments", "line-numbering", "tables", "headers-footers"],
wordOnline: true, // "Open in Word Online" available
desktopWord: true, // "Open in Desktop Word" available
},
xlsx: {
engine: "onlyoffice",
capabilities: ["view", "edit", "zoom", "search", "print", "formulas", "charts"],
editEngine: "onlyoffice",
editCapabilities: ["full-formatting", "formulas", "charts", "pivot-tables"],
notes: "OnlyOffice handles .xlsx natively. Same Document Server.",
},
pptx: {
engine: "onlyoffice",
capabilities: ["view", "edit", "zoom", "print", "animations"],
editEngine: "onlyoffice",
editCapabilities: ["slides", "transitions", "speaker-notes"],
notes: "OnlyOffice handles .pptx natively. Same Document Server.",
},
// Future:
// images: { engine: "native", capabilities: ["view", "zoom"] },
// txt/md: { engine: "tiptap", capabilities: ["view", "edit"] },
};
```
**Note:** OnlyOffice handles .docx, .xlsx, AND .pptx with the same Document Server. By bundling OnlyOffice, you get editing capability for all three Office formats, not just Word. This is a significant bonus — spreadsheets and presentations also open in Q with full editing.
---
## 9. Azure AD App Registration — Setup Instructions
### 9.1 What You're Setting Up
An Azure AD application registration that allows Q Dashboard to authenticate with Microsoft's Graph API. This gives Q permission to read file metadata, upload files, and get Word Online URLs.
### 9.2 Step-by-Step
#### Step 1: Access Azure Portal
1. Go to https://portal.azure.com
2. Sign in with your **firm** account (`wbrody@schallfirm.com`)
3. If you don't have Azure portal access with this account, use your personal Microsoft account
#### Step 2: Register a New Application
1. Search for "App registrations" in the top search bar
2. Click **"+ New registration"**
3. Fill in:
- **Name:** `Q Dashboard - ELNOR`
- **Supported account types:** Select **"Accounts in any organizational directory and personal Microsoft accounts"** — this allows both firm and personal
- **Redirect URI:** Select "Public client/native (mobile & desktop)" and enter: `http://localhost:3847/auth/callback`
4. Click **Register**
#### Step 3: Note Your IDs
- **Application (client) ID** — copy this
- **Directory (tenant) ID** — copy this
#### Step 4: Configure API Permissions
1. Left sidebar → **"API permissions"**
2. **"+ Add a permission"** → **"Microsoft Graph"** → **"Delegated permissions"**
3. Add:
- `Files.ReadWrite` — read/write user's OneDrive files
- `Files.ReadWrite.All` — read/write all files user has access to (SharePoint/firm OneDrive)
- `Sites.Read.All` — read SharePoint sites (for URL resolution)
- `User.Read` — basic profile info
4. Click **"Add permissions"**
5. If you have admin rights, click **"Grant admin consent"**. If not, your IT admin may need to approve.
#### Step 5: Configure Authentication
1. Left sidebar → **"Authentication"**
2. Under "Advanced settings," toggle **"Allow public client flows"** to **Yes**
3. Click **Save**
#### Step 6: No Client Secret Needed
Desktop apps use PKCE (Proof Key for Code Exchange). No client secret.
### 9.3 Configure Q Dashboard
```typescript
// src/config/m365.ts
export const M365_CONFIG = {
clientId: "YOUR-APPLICATION-CLIENT-ID-HERE",
tenantId: "common", // "common" allows both firm and personal
redirectUri: "http://localhost:3847/auth/callback",
scopes: [
"Files.ReadWrite",
"Files.ReadWrite.All",
"Sites.Read.All",
"User.Read",
"offline_access", // for refresh tokens
],
};
```
### 9.4 OAuth Flow in Electron
Use `@azure/msal-node` for authentication:
```bash
npm install @azure/msal-node
```
Flow:
1. User clicks "Sign in to Microsoft" (first time only, per account)
2. Electron opens Microsoft login in a popup BrowserWindow
3. User authenticates
4. Microsoft redirects with auth code
5. Q exchanges for access + refresh tokens via PKCE
6. Tokens stored in Electron's `safeStorage` (macOS Keychain backed)
7. Access token used for Graph API calls, auto-refreshed
### 9.5 Testing
1. Launch Q → Settings → Document Editing → Microsoft Accounts
2. Sign in firm account → "Connected: wbrody@schallfirm.com"
3. Sign in personal account → "Connected: [personal email]"
4. Open .docx from firm OneDrive → should open in OnlyOffice editor
5. Click "Open in Word Online" → should open Word Online in Q web tab
6. Open local .docx → click "Open in Word Online" → should upload to ELNOR Working, then open Word Online
---
## 10. Settings
```
Settings > Document Editing
├─ Microsoft Accounts
│ ├─ Firm: wbrody@schallfirm.com [Connected ✓] [Sign out]
│ └─ Personal: [email] [Connected ✓] [Sign out]
├─ Default document editor
│ ● OnlyOffice (built-in, works offline) — recommended
│ ○ Word Online (opens in browser tab, requires internet)
│ ○ Desktop Word (opens externally)
├─ Upload destination for local files
│ ○ Personal OneDrive (default)
│ ○ Firm OneDrive (schallfirm.com)
├─ Working folder name: [ELNOR Working ]
├─ Working folder cleanup
│ ○ Never
│ ○ After 7 days
│ ● After 30 days (default)
│ ○ After 90 days
│ [Clean up now] — shows contents, lets user select files to delete
├─ Auto-sync local files
│ ☑ Sync changes back to original path when editing local files via Word Online
├─ Agent Document Editing
│ ├─ Agent editing: ◉ Enabled ○ Disabled (blocks ALL agent edits to documents)
│ ├─ Edit mode: ◉ Always create copy (recommended) ○ Allow in-place (advanced — agent modifies original)
│ ├─ Copy naming suffix: [_E ] (produces filename_E1.docx, filename_E2.docx, etc.)
│ ├─ Require confirmation: ☑ Show "Allow edit?" dialog before agent creates copy
│ ├─ Auto-open copy: ☑ Open the edited copy in Document Viewer after creation
│ └─ Tracked changes: ☑ Always use tracked changes (recommended — cannot be unchecked when "Always create copy" is selected)
└─ OnlyOffice theme
● Match Q theme (default)
○ OnlyOffice default
```
**Note on "Allow in-place" mode:** This is an advanced setting that removes the copy-on-write protection. When enabled, agents modify the original file directly (still with tracked changes). This is for experienced users who trust the system and want a faster workflow. The confirmation dialog is forced ON when in-place mode is selected — it cannot be disabled in this mode.
---
## 11. Edge Cases & Error Handling
| Scenario | Behavior |
|----------|----------|
| No internet, user clicks "Open in Word Online" | Button grayed out, tooltip: "Requires internet connection" |
| No internet, user opens .docx normally | OnlyOffice opens it locally — works perfectly offline |
| File is open in both OnlyOffice and Word Online | Two separate copies. User should pick one editing surface. Warning toast if detected. |
| File is open in OnlyOffice and desktop Word | Same risk. Warning toast: "This file is also open in Microsoft Word." |
| Upload fails (quota, permissions) | Toast: "Upload failed: [reason]. You can still edit locally in OnlyOffice." |
| OnlyOffice Document Server fails to start | Toast: "Document editor unavailable. Using read-only preview." Falls back to docx-preview for viewing, "Open in Desktop Word" for editing. |
| User not signed into Microsoft | "Open in Word Online" shows "Sign in to Microsoft" → redirects to Settings |
| Large file (>250MB) | Graph API: use `createUploadSession` for resumable upload. OnlyOffice: handles large files natively. |
| File already exists in ELNOR Working | Graph API replaces it (creates new version). Previous version in OneDrive version history. |
| Firm IT blocks Graph API permissions | Word Online for firm files will fail. OnlyOffice and Desktop Word still work. Toast explains. |
| OnlyOffice crashes/hangs | Monitor child process health. If unresponsive for >30s, kill and restart. Show recovery toast. |
| Multiple users editing same OneDrive file | OnlyOffice: local only, no conflict detection with remote editors. Word Online: handles multi-user natively. |
---
## Cross-Doc References
| Document | Relevance |
|----------|-----------|
| DOC16 Entry 16.7 | M365 deep integration — Elnor's agentic identity, Cloudflare Tunnel. Elnor's Graph API editing uses this infrastructure when built. |
| DOC20 §6.16 | Document Viewer specification — toolbar, right panel, RendererCapabilities |
| DOC20 §6.19 | Q Browser — web tab behavior for Word Online |
| DOC20 §6.6 | Comments system — Q-internal comments on documents |
| DOC20 §6.25 | Notification system — Elnor edit notifications |
| DOC11 | OpenClaw Gateway — Elnor's runtime for document editing commands |
| DOC72 | Entity graph — document entity extraction |