CrewHub v0.17.5: A Night of Refactoring
Sometimes you ship features. Sometimes you spend a night fixing the mess underneath them. This was one of those nights.
Between Feb 19 and Feb 20, 2026, we ran through fifteen significant refactors on the develop branch. All 281 tests kept passing throughout. TypeScript reported 0 errors. The Vite build stayed clean at 6.28s across 3561 modules. Hundreds of lines removed, nothing broken.
Here is what changed and why it matters.
Backend
1. get_db() async context manager
Before: every route file called aiosqlite.connect() directly, inline, often without proper cleanup. Over 80 connection leak sites spread across 24 route files and service modules.
After: a single @asynccontextmanager called get_db(). Row factory is set once, automatically. Every caller gets a properly scoped connection and it closes on exit, no matter what.
Commit 960ba9d. 175 insertions, 651 deletions. Net: -476 lines.
This one change probably improved the health of the database layer more than anything we have done. Connection leaks are nasty because they are silent β the app keeps running, queries keep working, until suddenly they donβt.
2. creator.py service layer
creator.py was 1524 lines. One file, everything in it: route handlers, business logic, streaming logic, CRUD, AI calls, Pydantic models. A classic monolith-in-a-file.
It is now split across two commits (4777b6d + 7b16b1b) into:
routes/creator.py(385 lines) β pure HTTP handlersservices/creator/prop_generator.pyβ generation orchestrationservices/creator/prop_stream.pyβ streaming logicservices/creator/prop_crud.pyβ database operationsservices/creator/prop_ai.pyβ AI provider callscreator_models.pyβ all Pydantic models
Routes now just validate input and call services. Services do the actual work. Models live in one place.
3. Task and Project service layer
Commit 233873c. Two new service modules:
task_service.pycovers list, get, create, update, delete, run dispatch, and history eventsproject_service.pycovers the same plus an archive guard (you cannot archive a project with active tasks) and auto-slug generation
Routes are now pure HTTP handlers. They parse the request, call the service, return the response. No business logic in route files.
4. Room and Agent service layer
Commit 80f17b0. Same pattern:
room_service.pywith 8 functions, HQ guard (you cannot delete the HQ room), and cascade delete for dependent resourcesagent_service.pywith gateway sync (agent changes propagate to connected gateways automatically)
One intentional exception: the generate-bio endpoint stays in the route file. It makes an external AI call with streaming response, which is inherently I/O-bound and tightly coupled to the HTTP response. Forcing it into a service would add indirection without benefit.
5. db.row_factory cleanup
After the get_db() refactor, 22 raw aiosqlite.connect() calls remained in the services layer (commit 69e652c). These were cleaned up in a follow-up pass.
Bonus fixes found during this pass: several places were using positional row indexing (row[0]) instead of named keys. Fixed. A test was monkeypatching the wrong module for the database connection. Fixed.
Frontend
6. SettingsPanel.tsx
2608 lines. One component. Everything.
It is now a 195-line shell that renders tabs. The actual content lives in 7 extracted files under components/settings/:
LookAndFeelTab.tsxRoomsTab.tsxProjectsTab.tsxBehaviorTab.tsxDataTab.tsxAdvancedTab.tsxshared.tsx(shared types and helpers)
RoomsTab is the most interesting one. It now owns all room and rule state locally, including the dialogs for creating and editing rooms. It reports modal open/close state back to the parent via an onModalStateChange prop so the shell can handle backdrop behavior correctly.
Commits 4147619 + 51b5190.
7. taskConstants.ts
PRIORITY_CONFIG and STATUS_CONFIG were duplicated three times across the codebase. They now live in lib/taskConstants.ts and are imported wherever needed.
Bonus fix: MobileKanbanPanel was using hardcoded hex colors for priority and status indicators instead of reading from the config. Fixed.
8. formatters.ts
Seven inline date and time formatting functions, scattered across different components, doing slightly different things. Consolidated into lib/formatters.ts with a consistent API. One source of truth for how dates and times render across the app.
9. mockApi.ts
2018 lines. One file. It was the entire mock API layer.
It is now a 6-line re-export shim pointing at 10 domain files in lib/mock/:
types.tsrooms.tsagents.tsprojects.tstasks.tssettings.tssessions.tsutils.tsrouter.tsindex.ts
Commit 23ab791. The shim exists so existing imports do not break. Nothing changed from the consumerβs perspective.
10. FullscreenPropMaker.tsx
1952 lines down to a 1193-line orchestrator plus 6 sub-components under world3d/zones/creator/:
propMakerTypes.tsβ shared typesPropMakerToolbar.tsxβ the toolbar UIThinkingPanel.tsxβ the AI thinking displayPropPreview.tsxβ the 3D prop previewGenerationHistory.tsxβ the history sidebarPropControls.tsxβ the control panel
The orchestrator owns state and coordinates between sub-components. Sub-components are dumb display units with clear prop interfaces.
Bug fixes found during review
Two bugs surfaced during code review while doing these refactors:
ChatMessageBubble.tsx: The React.memo equality check was not including msg.thinking in its comparison. This meant thinking blocks would not re-render when showThinking was toggled to true. Fixed.
ChatHeader3DScene.tsx: A camera position comment described old calculated values (Y=0.748, Z=0.9) that no longer matched the actual tuned constants in the code (Y=0.35, Z=0.65). The comment was misleading anyone reading the file. Fixed.
Round two: the five large files
After the initial ten refactors, five large files were still on the list. Same day, same approach.
11. database.py
1027 lines holding schema definitions, migrations, seed data, health checks, and the get_db() context manager all in one place. It is now a 76-line entry point that re-exports everything, backed by four focused modules:
app/db/schema.pyβDB_PATH,DEMO_MODE,SCHEMA_VERSIONconstantsapp/db/migrations.pyβ allCREATE TABLE,ALTER TABLE, andCREATE INDEXfor schema v4βv16 (600 lines)app/db/seed.pyβseed_default_data(), demo agent and task seeding (341 lines)app/db/health.pyβcheck_database_health()(40 lines)
All 25+ files that import from app.db.database required zero changes. Public symbols are re-exported from database.py via noqa: F401 imports. 1027 β 76 lines (β93%).
12. openclaw.py
1149 lines of WebSocket connection management. Split into three mixin modules:
_handshake.pyβ full v2 device-identity auth and WebSocket connect (178 lines)_session_io.pyβ JSONL session file reading andkill_session(211 lines)_extended_api.pyβsend_chat, streaming, cron CRUD, system queries (290 lines)
OpenClawConnection inherits both mixins and delegates _do_connect to perform_handshake. 1149 β 398 lines.
13. meeting_orchestrator.py
982 lines. The orchestrator was doing orchestration, DB access, document loading, and public API all at once. DB helpers and public API moved to a new meeting_service.py (524 lines). The orchestrator now focuses on the meeting state machine. 982 β 340 lines.
14. World3DView.tsx
1623 lines, the main 3D world component. Extracted into:
SceneContent.tsxβ the full R3F scene graph, receives all data as props (no hooks inside Canvas)CanvasErrorBoundary.tsx,MeetingOverlays.tsx,DragStatusIndicator.tsxβ focused UI componentsutils/buildingLayout.tsβ layout calculations and room boundsutils/botActivity.tsβ bot status, label humanization, activity textutils/botPositions.tsβ position helpers and debug bot utilities
The architecture rule was respected throughout: no fetch or SSE hooks inside the R3F Canvas. Data flows in as props. 1623 β 467 lines (β71%).
15. OnboardingWizard.tsx
1132 lines, the full onboarding flow. Each step extracted into its own component:
steps/StepWelcome.tsx,StepScan.tsx,StepConfigure.tsx,StepReady.tsxsteps/StepProgress.tsxβ the progress baronboardingTypes.ts+onboardingHelpers.tsxβ shared types and utilities
The wizard shell now holds only step routing and state transitions. 1132 β 266 lines (β76%).
Numbers
| Metric | Result |
|---|---|
| Refactors | 15 |
| Tests | 281/281 passing |
| TypeScript errors | 0 |
| Vite build | 6.28s, 3561 modules |
| Regressions | 0 |
Line count reductions (round two):
| File | Before | After | Reduction |
|---|---|---|---|
database.py | 1027 | 76 | β93% |
OnboardingWizard.tsx | 1132 | 266 | β76% |
World3DView.tsx | 1623 | 467 | β71% |
meeting_orchestrator.py | 982 | 340 | β65% |
openclaw.py | 1149 | 398 | β65% |
This is the kind of work that does not show up in a feature list but makes everything else easier. Smaller files, clearer boundaries, fewer surprises. The codebase is in better shape than it was yesterday.
CrewHub is open source under AGPL-3.0. Join the Discord or view on GitHub.