← Back to Blog
</> For Developers πŸ€– AI-written, human-guided

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 handlers
  • services/creator/prop_generator.py β€” generation orchestration
  • services/creator/prop_stream.py β€” streaming logic
  • services/creator/prop_crud.py β€” database operations
  • services/creator/prop_ai.py β€” AI provider calls
  • creator_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.py covers list, get, create, update, delete, run dispatch, and history events
  • project_service.py covers 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.py with 8 functions, HQ guard (you cannot delete the HQ room), and cascade delete for dependent resources
  • agent_service.py with 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.tsx
  • RoomsTab.tsx
  • ProjectsTab.tsx
  • BehaviorTab.tsx
  • DataTab.tsx
  • AdvancedTab.tsx
  • shared.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.ts
  • rooms.ts
  • agents.ts
  • projects.ts
  • tasks.ts
  • settings.ts
  • sessions.ts
  • utils.ts
  • router.ts
  • index.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 types
  • PropMakerToolbar.tsx β€” the toolbar UI
  • ThinkingPanel.tsx β€” the AI thinking display
  • PropPreview.tsx β€” the 3D prop preview
  • GenerationHistory.tsx β€” the history sidebar
  • PropControls.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_VERSION constants
  • app/db/migrations.py β€” all CREATE TABLE, ALTER TABLE, and CREATE INDEX for 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 and kill_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 components
  • utils/buildingLayout.ts β€” layout calculations and room bounds
  • utils/botActivity.ts β€” bot status, label humanization, activity text
  • utils/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.tsx
  • steps/StepProgress.tsx β€” the progress bar
  • onboardingTypes.ts + onboardingHelpers.tsx β€” shared types and utilities

The wizard shell now holds only step routing and state transitions. 1132 β†’ 266 lines (βˆ’76%).


Numbers

MetricResult
Refactors15
Tests281/281 passing
TypeScript errors0
Vite build6.28s, 3561 modules
Regressions0

Line count reductions (round two):

FileBeforeAfterReduction
database.py102776βˆ’93%
OnboardingWizard.tsx1132266βˆ’76%
World3DView.tsx1623467βˆ’71%
meeting_orchestrator.py982340βˆ’65%
openclaw.py1149398βˆ’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.