Changeset 0.15.0 (#71)
This commit is contained in:
530
docs/REFACTOR-0.10.3.md
Normal file
530
docs/REFACTOR-0.10.3.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# v0.10.3 — Frontend Refactor Plan
|
||||
|
||||
## Problem
|
||||
|
||||
Two monolith files accumulate every feature:
|
||||
|
||||
| File | Lines | Role |
|
||||
|----------|------:|-----------------------------------|
|
||||
| app.js | 2,940 | All application logic + listeners |
|
||||
| ui.js | 2,582 | All rendering + admin + settings |
|
||||
| api.js | 575 | HTTP client (fine) |
|
||||
| debug.js | 580 | Debug logger (fine) |
|
||||
| events.js| 327 | EventBus WebSocket (fine) |
|
||||
| **Total**| **7,004** | |
|
||||
|
||||
The v0.10.2 syntax bug (orphan brace nesting everything from notes
|
||||
through banners) is a direct consequence: in a 2,940-line file, a
|
||||
misplaced insertion is invisible.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Vanilla JS** — no build step, no bundler, no ES modules.
|
||||
- **Global functions** — HTML `onclick` handlers call ~25 named globals.
|
||||
These must stay on `window`. No renaming.
|
||||
- **Load order via `<script>` tags** — dependencies load first.
|
||||
- **Service worker** — `sw.js` pre-caches the file list; must be updated.
|
||||
- **Cache busting** — all `<script>` tags use `?v=%%APP_VERSION%%`.
|
||||
- **Docker entrypoint** — `COPY src/` covers everything. No changes needed.
|
||||
- **CI syntax check** — already runs `node --check` on `src/js/*.js`.
|
||||
|
||||
## Architecture
|
||||
|
||||
The `UI` object stays as a single global with methods. Domain files extend
|
||||
it via `Object.assign(UI, { ... })`. The `App` state object stays in
|
||||
`app.js`. All functions stay global.
|
||||
|
||||
**Load order** (dependency graph, top to bottom):
|
||||
|
||||
```
|
||||
vendor/marked.min.js, vendor/purify.min.js
|
||||
↓
|
||||
debug.js, events.js
|
||||
↓
|
||||
api.js
|
||||
↓
|
||||
ui-format.js ← esc(), markdown rendering (used by everything)
|
||||
↓
|
||||
ui-core.js ← UI object: sidebar, chat list, messages, streaming, model selector
|
||||
↓
|
||||
ui-settings.js ← Object.assign(UI, { settings/team methods })
|
||||
ui-admin.js ← Object.assign(UI, { admin modal methods })
|
||||
↓
|
||||
tokens.js ← Tokens object, context tracking
|
||||
notes.js ← Notes panel
|
||||
chat.js ← Chat ops, send, regen, edit, branch, summarize
|
||||
settings-handlers.js ← Provider CRUD, command palette, save handlers
|
||||
admin-handlers.js ← Admin actions, presets, team management
|
||||
↓
|
||||
app.js ← State, init, boot, auth, listeners (orchestrator)
|
||||
```
|
||||
|
||||
## File Breakdown
|
||||
|
||||
### Unchanged (3 files, 1,482 lines)
|
||||
|
||||
| File | Lines | Notes |
|
||||
|------|------:|-------|
|
||||
| api.js | 575 | Clean. Self-contained HTTP client. |
|
||||
| debug.js | 580 | Clean. Debug logger + modal. |
|
||||
| events.js | 327 | Clean. EventBus WebSocket. |
|
||||
|
||||
### From ui.js → 4 files
|
||||
|
||||
#### ui-format.js (~280 lines)
|
||||
Extract from ui.js bottom section + helpers.
|
||||
|
||||
```
|
||||
Formatting (L2241-2462):
|
||||
_renderMarkdown(text)
|
||||
_renderCodeBlocks(html)
|
||||
_copyCodeBlock(btn)
|
||||
_formatTokenCount(n)
|
||||
_formatCost(c)
|
||||
|
||||
Helpers (L2519-2583):
|
||||
esc(s) ← global function, used everywhere
|
||||
_formatTimestamp(ts)
|
||||
_formatRelativeTime(ts)
|
||||
_truncate(s, n)
|
||||
openSidePanel(panelId) ← global, called from notes/search
|
||||
closeSidePanel() ← global, HTML onclick
|
||||
|
||||
Side Panel Resize (L2463-2518):
|
||||
_initSidePanelResize()
|
||||
```
|
||||
|
||||
**Why separate:** Formatting is a pure utility layer. Every other UI file
|
||||
depends on `esc()` and `_renderMarkdown()`. Loading first eliminates
|
||||
forward references.
|
||||
|
||||
#### ui-core.js (~650 lines)
|
||||
The `UI` object definition with core rendering methods.
|
||||
|
||||
```
|
||||
const UI = {
|
||||
// Sidebar (L189-216)
|
||||
toggleSidebar(), restoreSidebar(),
|
||||
toggleUserMenu(), closeUserMenu(),
|
||||
|
||||
// Chat List (L217-284)
|
||||
renderChatList(),
|
||||
|
||||
// Messages (L285-454)
|
||||
renderMessages(), showEmptyState(),
|
||||
_messageHTML(), _summaryHTML(), _isSummaryMessage(),
|
||||
toggleSummarizedHistory(),
|
||||
|
||||
// Streaming (L455-596)
|
||||
streamResponse(),
|
||||
|
||||
// Model Selector (L597-727)
|
||||
getModelValue(), setModelValue(),
|
||||
updateModelSelector(), initModelDropdown(),
|
||||
|
||||
// Capability Badges (L728-774)
|
||||
getSelectedModelCaps(), updateCapabilityBadges(),
|
||||
|
||||
// User / Connection (L775-811)
|
||||
updateUser(), showAdminButton(),
|
||||
|
||||
// Generating State (L812-862)
|
||||
setGenerating(), showRegenerate(),
|
||||
|
||||
// Toast (L863-873)
|
||||
toast(),
|
||||
|
||||
// Settings Modal (L874-921) — thin dispatcher
|
||||
openSettings(), switchSettingsTab(),
|
||||
|
||||
// Export (L2191-2214)
|
||||
exportMarkdown(), exportJSON(),
|
||||
|
||||
// Message Actions (L2215-2240)
|
||||
(delegated action handlers)
|
||||
|
||||
// Scroll (L2226-2240)
|
||||
scrollToBottom(),
|
||||
};
|
||||
```
|
||||
|
||||
Also includes the top-level `_avatarHTML()` helper (L5-24) and
|
||||
summary message helpers (L167-188).
|
||||
|
||||
#### ui-settings.js (~550 lines)
|
||||
Extends UI with all settings modal tab content.
|
||||
|
||||
```
|
||||
Object.assign(UI, {
|
||||
// Preset Form Component (L25-166) — _presetFormHTML(), etc.
|
||||
|
||||
// Appearance (L922-1267)
|
||||
loadAppearanceSettings(), initAppearance(),
|
||||
|
||||
// Providers (L1444-1480)
|
||||
loadProviderList(), hideProviderForm(),
|
||||
|
||||
// Profile (L994-1002)
|
||||
loadProfileIntoSettings(),
|
||||
|
||||
// Teams (L1003-1267)
|
||||
loadMyTeams(), loadTeamsTab(), openTeamManage(),
|
||||
showTeamPicker(), loadTeamManageMembers(),
|
||||
loadTeamManageProviders(), loadTeamManagePresets(),
|
||||
loadTeamAuditLog(), loadTeamPresetModelDropdown(),
|
||||
switchTeamTab(),
|
||||
|
||||
// Usage (L1160-1267)
|
||||
loadMyUsage(), loadTeamUsage(),
|
||||
|
||||
// User Model Roles (L1268-1328)
|
||||
loadUserRoles(),
|
||||
|
||||
// User Models & Presets (L2118-2190)
|
||||
loadUserModels(), loadUserPresets(),
|
||||
|
||||
// checkUserProvidersAllowed(), checkUserPresetsAllowed()
|
||||
});
|
||||
```
|
||||
|
||||
#### ui-admin.js (~650 lines)
|
||||
Extends UI with all admin modal content.
|
||||
|
||||
```
|
||||
Object.assign(UI, {
|
||||
// Admin Modal (L1481-1604)
|
||||
openTeamAdmin(), switchAdminTab(),
|
||||
|
||||
// Users (L1549-1588)
|
||||
loadAdminUsers(),
|
||||
|
||||
// Stats (L1589-1604)
|
||||
loadAdminStats(),
|
||||
|
||||
// Roles (L1605-1658)
|
||||
loadAdminRoles(),
|
||||
|
||||
// Usage + Audit (L1659-1813)
|
||||
loadAdminUsage(), loadAuditLog(),
|
||||
|
||||
// Providers (L1814-1835)
|
||||
loadAdminProviders(),
|
||||
|
||||
// Models (L1836-1863)
|
||||
loadAdminModels(),
|
||||
|
||||
// Presets (L1864-1939)
|
||||
loadAdminPresets(),
|
||||
|
||||
// Teams (L1940-2117)
|
||||
loadAdminTeams(), openTeamDetail(),
|
||||
loadTeamMembers(), loadMemberUserDropdown(),
|
||||
|
||||
// Settings (L2037-2117)
|
||||
loadAdminSettings(), updateBannerPreview(),
|
||||
});
|
||||
```
|
||||
|
||||
### From app.js → 6 files
|
||||
|
||||
#### tokens.js (~120 lines)
|
||||
Already has its own `Tokens` namespace — clean extraction.
|
||||
|
||||
```
|
||||
const Tokens = { ... }; // L36-74
|
||||
function updateInputTokens() // L78-106
|
||||
function updateContextWarning() // L108-147
|
||||
function dismissContextWarning() // L148-152
|
||||
```
|
||||
|
||||
#### notes.js (~300 lines)
|
||||
Self-contained with own state variables.
|
||||
|
||||
```
|
||||
var _editingNoteId = null;
|
||||
var _notesSelectMode = false;
|
||||
var _selectedNoteIds = new Set();
|
||||
var _notesSort = 'updated_desc';
|
||||
|
||||
openNotes(), loadNotesList(), _noteListItem()
|
||||
_highlightHeadline()
|
||||
_enterSelectMode(), _exitSelectMode()
|
||||
_toggleNoteSelect(), _toggleSelectAll()
|
||||
_updateSelectedCount(), _bulkDeleteSelected()
|
||||
loadNoteFolders(), showNotesList()
|
||||
copyNoteContent(), openNoteEditor()
|
||||
_populateEditFields(), _clearEditFields()
|
||||
_showNoteReadMode(), _showNoteEditMode()
|
||||
_showNotePreview()
|
||||
saveNote(), deleteNote()
|
||||
_initNotesListeners() ← NEW: extracted from initListeners
|
||||
```
|
||||
|
||||
#### chat.js (~500 lines)
|
||||
All conversation operations.
|
||||
|
||||
```
|
||||
// Chat management
|
||||
loadChats(), selectChat(), newChat(), deleteChat()
|
||||
|
||||
// Per-chat model persistence
|
||||
_saveChatModel(), _restoreChatModel()
|
||||
|
||||
// Send + stream
|
||||
sendMessage(), stopGeneration()
|
||||
|
||||
// Active path
|
||||
reloadActivePath()
|
||||
|
||||
// Regenerate
|
||||
regenerateMessage(), regenerate()
|
||||
|
||||
// Edit
|
||||
editMessage(), submitEdit(), cancelEdit()
|
||||
|
||||
// Branch navigation
|
||||
switchSibling()
|
||||
|
||||
// Summarize & Continue
|
||||
summarizeAndContinue()
|
||||
|
||||
// Preview
|
||||
clearPreview()
|
||||
|
||||
_initChatListeners() ← NEW: extracted from initListeners
|
||||
```
|
||||
|
||||
#### settings-handlers.js (~400 lines)
|
||||
Settings-related handlers + command palette.
|
||||
|
||||
```
|
||||
// Settings
|
||||
handleSaveSettings(), loadSettings(), saveSettings()
|
||||
|
||||
// Avatar
|
||||
updateAvatarPreview()
|
||||
|
||||
// Providers
|
||||
handleCreateProvider(), deleteProvider()
|
||||
refreshProviderModels(), editProvider()
|
||||
|
||||
// Admin settings save
|
||||
handleSaveAdminSettings()
|
||||
|
||||
// User model roles
|
||||
userRoleProviderChanged(), saveUserRole(), clearUserRole()
|
||||
|
||||
// Command Palette
|
||||
_cmdCommands[], toggleCmdPalette(), openCmdPalette()
|
||||
closeCmdPalette(), _handleCmdKey(), _highlightCmdItem()
|
||||
_getVisibleCommands(), _renderCmdResults(), executeCmdAction()
|
||||
|
||||
_initSettingsListeners() ← NEW: extracted from initListeners
|
||||
```
|
||||
|
||||
#### admin-handlers.js (~450 lines)
|
||||
Admin actions + team management.
|
||||
|
||||
```
|
||||
// Admin user management
|
||||
toggleUserActive(), showApproveForm(), hideApproveForm()
|
||||
handleApproveUser(), resetUserPassword(), promoteUser()
|
||||
createAdminUser()
|
||||
|
||||
// Admin roles
|
||||
adminSaveRole(), adminRoleProviderChanged()
|
||||
|
||||
// User model preferences
|
||||
toggleModelVisibility(), bulkSetVisibility()
|
||||
bulkSetUserModelVisibility(), deleteUserPreset()
|
||||
|
||||
// Admin presets
|
||||
_adminPresetForm, ensureAdminPresetForm()
|
||||
editAdminPreset(), createAdminPreset()
|
||||
toggleAdminPreset(), deleteAdminPreset()
|
||||
|
||||
// Team admin (system admin)
|
||||
toggleTeamActive(), deleteTeam()
|
||||
updateTeamMember(), removeTeamMember()
|
||||
|
||||
// Team admin (settings-side)
|
||||
settingsUpdateTeamMember(), settingsRemoveTeamMember()
|
||||
settingsDeleteTeamPreset(), settingsToggleTeamProvider()
|
||||
settingsDeleteTeamProvider(), settingsEditTeamProvider()
|
||||
|
||||
_initAdminListeners() ← NEW: extracted from initListeners
|
||||
```
|
||||
|
||||
#### app.js (~400 lines) — the orchestrator
|
||||
What remains after extraction.
|
||||
|
||||
```
|
||||
// State
|
||||
const App = { ... };
|
||||
|
||||
// Models
|
||||
resolveCapabilities(), fetchModels()
|
||||
|
||||
// Init + Boot
|
||||
init(), startApp()
|
||||
|
||||
// Auth flow
|
||||
showSplash(), hideSplash()
|
||||
handleLogin(), handleRegister(), handleLogout()
|
||||
switchAuthTab(), setAuthError(), setAuthLoading()
|
||||
|
||||
// Modal helpers
|
||||
openModal(), closeModal()
|
||||
checkTabsOverflow(), updateTabArrows()
|
||||
|
||||
// Branding + Banners
|
||||
initBranding(), initBanners()
|
||||
|
||||
// Listeners — thin dispatcher
|
||||
function initListeners() {
|
||||
if (_listenersInit) return;
|
||||
_listenersInit = true;
|
||||
_initChatListeners(); // from chat.js
|
||||
_initSettingsListeners(); // from settings-handlers.js
|
||||
_initAdminListeners(); // from admin-handlers.js
|
||||
_initNotesListeners(); // from notes.js
|
||||
_initGlobalKeyboard(); // local: Escape, Ctrl+K, resize
|
||||
_initSidePanelResize(); // from ui-format.js
|
||||
}
|
||||
|
||||
// Boot
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
```
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### 1. Preparation (before any splits)
|
||||
- [ ] Branch from merged v0.10.2
|
||||
- [ ] Add `node --check` validation to a quick local script:
|
||||
`for f in src/js/*.js; do node --check "$f" || exit 1; done`
|
||||
- [ ] Run all frontend tests as baseline: `node --test src/js/__tests__/*.test.js`
|
||||
|
||||
### 2. Extract ui-format.js
|
||||
- [ ] Create `src/js/ui-format.js`
|
||||
- [ ] Move: `esc()`, formatting functions, side panel resize, helpers
|
||||
- [ ] Remove from `ui.js`
|
||||
- [ ] Add `<script>` tag in `index.html` **before** `ui.js`
|
||||
- [ ] Add to `sw.js` SHELL_FILES
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 3. Extract ui-settings.js
|
||||
- [ ] Create `src/js/ui-settings.js`
|
||||
- [ ] Move settings/team UI methods out of UI object in `ui.js`
|
||||
- [ ] Add `Object.assign(UI, { ... })` wrapper
|
||||
- [ ] Add `<script>` tag **after** `ui.js`
|
||||
- [ ] Add to `sw.js`
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 4. Extract ui-admin.js
|
||||
- [ ] Create `src/js/ui-admin.js`
|
||||
- [ ] Move admin UI methods out of UI object
|
||||
- [ ] Add `Object.assign(UI, { ... })` wrapper
|
||||
- [ ] Add `<script>` tag **after** `ui.js`
|
||||
- [ ] Add to `sw.js`
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 5. Extract tokens.js
|
||||
- [ ] Create `src/js/tokens.js`
|
||||
- [ ] Move `Tokens` object + `updateInputTokens` + `updateContextWarning` + `dismissContextWarning`
|
||||
- [ ] Remove from `app.js`
|
||||
- [ ] Add `<script>` tag **before** `app.js`
|
||||
- [ ] Add to `sw.js`
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 6. Extract notes.js
|
||||
- [ ] Create `src/js/notes.js`
|
||||
- [ ] Move all notes state + functions
|
||||
- [ ] Extract `_initNotesListeners()` from `initListeners()` — cut the
|
||||
notes-related event bindings into a new function, call it from
|
||||
`initListeners()`
|
||||
- [ ] Add `<script>` tag **before** `app.js`
|
||||
- [ ] Add to `sw.js`
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 7. Extract chat.js
|
||||
- [ ] Create `src/js/chat.js`
|
||||
- [ ] Move chat management, send, regen, edit, branch, summarize, model persistence
|
||||
- [ ] Extract `_initChatListeners()` from `initListeners()` — cut chat input,
|
||||
Enter-to-send, message action delegation, etc.
|
||||
- [ ] Add `<script>` tag **before** `app.js`
|
||||
- [ ] Add to `sw.js`
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 8. Extract settings-handlers.js
|
||||
- [ ] Create `src/js/settings-handlers.js`
|
||||
- [ ] Move settings handlers, provider CRUD, command palette, avatar, user roles
|
||||
- [ ] Extract `_initSettingsListeners()` from `initListeners()`
|
||||
- [ ] Add `<script>` tag **before** `app.js`
|
||||
- [ ] Add to `sw.js`
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 9. Extract admin-handlers.js
|
||||
- [ ] Create `src/js/admin-handlers.js`
|
||||
- [ ] Move admin actions, presets, team management
|
||||
- [ ] Extract `_initAdminListeners()` from `initListeners()`
|
||||
- [ ] Add `<script>` tag **before** `app.js`
|
||||
- [ ] Add to `sw.js`
|
||||
- [ ] Run syntax check + tests
|
||||
|
||||
### 10. Final validation
|
||||
- [ ] `node --check` all JS files
|
||||
- [ ] `node --test src/js/__tests__/*.test.js` — all 159 tests pass
|
||||
- [ ] Verify `index.html` script order matches dependency graph
|
||||
- [ ] Verify `sw.js` SHELL_FILES lists all new files
|
||||
- [ ] Manual smoke test: login, chat, settings, admin, notes, debug
|
||||
- [ ] Grep for any remaining forward references / undefined calls
|
||||
- [ ] Update ROADMAP: mark v0.10.3 complete
|
||||
- [ ] Update CHANGELOG
|
||||
|
||||
## Target State
|
||||
|
||||
| File | Lines | Domain |
|
||||
|------|------:|--------|
|
||||
| api.js | 575 | HTTP client, auth tokens |
|
||||
| debug.js | 580 | Debug logger + modal |
|
||||
| events.js | 327 | EventBus WebSocket |
|
||||
| ui-format.js | ~280 | Markdown, code blocks, esc(), helpers |
|
||||
| ui-core.js | ~650 | UI object: rendering, model selector, streaming |
|
||||
| ui-settings.js | ~550 | Settings tabs, teams, providers, user prefs |
|
||||
| ui-admin.js | ~650 | Admin tabs, users, roles, usage, teams |
|
||||
| tokens.js | ~120 | Context tracking, token estimation |
|
||||
| notes.js | ~300 | Notes panel, editor, multi-select |
|
||||
| chat.js | ~500 | Chat ops, send, regen, edit, branch, summarize |
|
||||
| settings-handlers.js | ~400 | Settings save, provider CRUD, command palette |
|
||||
| admin-handlers.js | ~450 | Admin actions, presets, team management |
|
||||
| app.js | ~400 | State, init, boot, auth, listener dispatch |
|
||||
| **Total** | **~5,782** | 13 files (was 5) |
|
||||
|
||||
Line count drops slightly due to removed duplicate section headers and
|
||||
consolidated comments. Average file: ~445 lines. Largest: ~650.
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
**Forward references:** Function declarations are hoisted within a
|
||||
`<script>`, but not across scripts. All functions called by
|
||||
`initListeners()` must be defined in scripts loaded before `app.js`.
|
||||
The load order above ensures this.
|
||||
|
||||
**`UI` object extension:** `Object.assign(UI, { ... })` in
|
||||
ui-settings.js and ui-admin.js means `UI` must be defined first
|
||||
(in ui-core.js). The load order handles this.
|
||||
|
||||
**Regression testing:** Each extraction step ends with syntax check +
|
||||
test run. If anything breaks, it's isolated to the last extraction.
|
||||
|
||||
**Rollback:** Each step is a single commit. Revert any step independently.
|
||||
|
||||
## What This Does NOT Change
|
||||
|
||||
- No ES modules, no import/export, no bundler
|
||||
- No function renaming (HTML onclick handlers untouched)
|
||||
- No new dependencies
|
||||
- No architectural changes to backend
|
||||
- No feature additions
|
||||
- The `UI` object is still a single global — just defined across files
|
||||
- All tests continue to work unchanged
|
||||
Reference in New Issue
Block a user