heathrow 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +58 -0
  3. data/README.md +205 -0
  4. data/bin/heathrow +42 -0
  5. data/bin/heathrowd +283 -0
  6. data/docs/ARCHITECTURE.md +1172 -0
  7. data/docs/DATABASE_SCHEMA.md +685 -0
  8. data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
  9. data/docs/DISCORD_SETUP.md +142 -0
  10. data/docs/GMAIL_OAUTH_SETUP.md +120 -0
  11. data/docs/PLUGIN_SYSTEM.md +1370 -0
  12. data/docs/PROJECT_PLAN.md +1022 -0
  13. data/docs/README.md +417 -0
  14. data/docs/REDDIT_SETUP.md +174 -0
  15. data/docs/REPLY_FORWARD.md +182 -0
  16. data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
  17. data/heathrow.gemspec +34 -0
  18. data/heathrowd.service +21 -0
  19. data/img/heathrow.svg +95 -0
  20. data/img/rss_threaded.png +0 -0
  21. data/img/sources.png +0 -0
  22. data/lib/heathrow/address_book.rb +42 -0
  23. data/lib/heathrow/config.rb +332 -0
  24. data/lib/heathrow/database.rb +731 -0
  25. data/lib/heathrow/database_new.rb +392 -0
  26. data/lib/heathrow/event_bus.rb +175 -0
  27. data/lib/heathrow/logger.rb +122 -0
  28. data/lib/heathrow/message.rb +176 -0
  29. data/lib/heathrow/message_composer.rb +399 -0
  30. data/lib/heathrow/message_organizer.rb +774 -0
  31. data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
  32. data/lib/heathrow/notmuch.rb +45 -0
  33. data/lib/heathrow/oauth2_smtp.rb +254 -0
  34. data/lib/heathrow/plugin/base.rb +212 -0
  35. data/lib/heathrow/plugin_manager.rb +141 -0
  36. data/lib/heathrow/poller.rb +93 -0
  37. data/lib/heathrow/smtp_sender.rb +204 -0
  38. data/lib/heathrow/source.rb +39 -0
  39. data/lib/heathrow/sources/base.rb +74 -0
  40. data/lib/heathrow/sources/discord.rb +357 -0
  41. data/lib/heathrow/sources/gmail.rb +294 -0
  42. data/lib/heathrow/sources/imap.rb +198 -0
  43. data/lib/heathrow/sources/instagram.rb +307 -0
  44. data/lib/heathrow/sources/instagram_fetch.py +101 -0
  45. data/lib/heathrow/sources/instagram_send.py +55 -0
  46. data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
  47. data/lib/heathrow/sources/maildir.rb +606 -0
  48. data/lib/heathrow/sources/messenger.rb +212 -0
  49. data/lib/heathrow/sources/messenger_fetch.js +297 -0
  50. data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
  51. data/lib/heathrow/sources/messenger_send.js +32 -0
  52. data/lib/heathrow/sources/messenger_send.py +100 -0
  53. data/lib/heathrow/sources/reddit.rb +461 -0
  54. data/lib/heathrow/sources/rss.rb +299 -0
  55. data/lib/heathrow/sources/slack.rb +375 -0
  56. data/lib/heathrow/sources/source_manager.rb +328 -0
  57. data/lib/heathrow/sources/telegram.rb +498 -0
  58. data/lib/heathrow/sources/webpage.rb +207 -0
  59. data/lib/heathrow/sources/weechat.rb +479 -0
  60. data/lib/heathrow/sources/whatsapp.rb +474 -0
  61. data/lib/heathrow/ui/application.rb +8098 -0
  62. data/lib/heathrow/ui/navigation.rb +8 -0
  63. data/lib/heathrow/ui/panes.rb +8 -0
  64. data/lib/heathrow/ui/source_wizard.rb +567 -0
  65. data/lib/heathrow/ui/threaded_view.rb +780 -0
  66. data/lib/heathrow/ui/views.rb +8 -0
  67. data/lib/heathrow/version.rb +3 -0
  68. data/lib/heathrow/wizards/discord_wizard.rb +193 -0
  69. data/lib/heathrow/wizards/slack_wizard.rb +140 -0
  70. data/lib/heathrow.rb +55 -0
  71. metadata +147 -0
@@ -0,0 +1,1172 @@
1
+ # Heathrow Architecture
2
+
3
+ **Design Philosophy:** Modularity, isolation, testability, and resilience.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ 1. [System Overview](#system-overview)
10
+ 2. [Layer Architecture](#layer-architecture)
11
+ 3. [Component Isolation](#component-isolation)
12
+ 4. [Data Flow](#data-flow)
13
+ 5. [Concurrency Model](#concurrency-model)
14
+ 6. [Error Handling Strategy](#error-handling-strategy)
15
+ 7. [Testing Strategy](#testing-strategy)
16
+ 8. [Performance Considerations](#performance-considerations)
17
+
18
+ ---
19
+
20
+ ## System Overview
21
+
22
+ Heathrow is structured as a layered system where each layer has clear responsibilities and well-defined interfaces. No component can bypass its layer or access components outside its scope directly.
23
+
24
+ ### Core Principles
25
+
26
+ 1. **Single Responsibility** - Each component does one thing well
27
+ 2. **Interface Segregation** - Components depend on abstractions, not implementations
28
+ 3. **Dependency Inversion** - High-level modules don't depend on low-level modules
29
+ 4. **Open/Closed** - Open for extension (plugins), closed for modification (core)
30
+ 5. **Fail-Safe** - Component failure doesn't cascade to other components
31
+
32
+ ### High-Level Architecture
33
+
34
+ ```
35
+ ┌───────────────────────────────────────────────────────────┐
36
+ │ User │
37
+ └─────────────────────────┬─────────────────────────────────┘
38
+
39
+
40
+ ┌───────────────────────────────────────────────────────────┐
41
+ │ UI Layer │
42
+ │ ┌─────────────────────────────────────────────────────┐ │
43
+ │ │ rcurses (TUI Framework) │ │
44
+ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
45
+ │ │ │ Panes │ │ Input │ │ Render │ │ │
46
+ │ │ └──────────┘ └──────────┘ └──────────┘ │ │
47
+ │ └─────────────────────────────────────────────────────┘ │
48
+ └─────────────────────────┬─────────────────────────────────┘
49
+
50
+
51
+ ┌───────────────────────────────────────────────────────────┐
52
+ │ Application Layer │
53
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
54
+ │ │ Message │ │ View │ │ Filter │ │
55
+ │ │ Router │ │ Manager │ │ Engine │ │
56
+ │ └──────────────┘ └──────────────┘ └──────────────────┘ │
57
+ │ ┌──────────────┐ ┌──────────────┐ │
58
+ │ │ Stream │ │ Search │ │
59
+ │ │ Manager │ │ Engine │ │
60
+ │ └──────────────┘ └──────────────┘ │
61
+ └─────────────────────────┬─────────────────────────────────┘
62
+
63
+
64
+ ┌───────────────────────────────────────────────────────────┐
65
+ │ Core Layer │
66
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
67
+ │ │ Plugin │ │ Config │ │ Database │ │
68
+ │ │ Manager │ │ Manager │ │ Layer │ │
69
+ │ └──────────────┘ └──────────────┘ └──────────────────┘ │
70
+ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
71
+ │ │ Event │ │ Logger │ │ Cache │ │
72
+ │ │ Bus │ │ │ │ │ │
73
+ │ └──────────────┘ └──────────────┘ └──────────────────┘ │
74
+ └─────────────────────────┬─────────────────────────────────┘
75
+
76
+
77
+ ┌───────────────────────────────────────────────────────────┐
78
+ │ Plugin Layer │
79
+ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
80
+ │ │ Gmail │ │ Slack │ │Discord │ │ RSS │ │ IRC │ │
81
+ │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │
82
+ │ ┌────────┐ ┌────────┐ ┌────────┐ │
83
+ │ │WhatsApp│ │Telegram│ │ Reddit │ [...] │
84
+ │ └────────┘ └────────┘ └────────┘ │
85
+ └───────────────────────────────────────────────────────────┘
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Layer Architecture
91
+
92
+ ### 1. UI Layer
93
+
94
+ **Responsibility:** User interaction and display
95
+
96
+ **Components:**
97
+ - Pane Manager (`lib/heathrow/ui/pane_manager.rb`)
98
+ - Input Handler (`lib/heathrow/ui/input_handler.rb`)
99
+ - Renderer (`lib/heathrow/ui/renderer.rb`)
100
+ - Key Binding Manager (`lib/heathrow/ui/key_bindings.rb`)
101
+
102
+ **Interface:**
103
+
104
+ ```ruby
105
+ module Heathrow
106
+ module UI
107
+ class Application
108
+ # Initialize UI with application layer dependencies
109
+ def initialize(message_router, view_manager, event_bus)
110
+
111
+ # Main event loop
112
+ def run
113
+
114
+ # Handle user input
115
+ def handle_input(key)
116
+
117
+ # Refresh display
118
+ def refresh
119
+
120
+ # Shutdown
121
+ def shutdown
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ **Dependencies:**
128
+ - Down: Application Layer (message_router, view_manager)
129
+ - Up: None (top layer)
130
+
131
+ **Isolation:**
132
+ - UI crashes don't affect data layer
133
+ - Can be replaced with different UI (web, GUI) without changing logic
134
+ - No business logic in UI components
135
+
136
+ ---
137
+
138
+ ### 2. Application Layer
139
+
140
+ **Responsibility:** Business logic and workflows
141
+
142
+ **Components:**
143
+
144
+ #### Message Router
145
+
146
+ Routes messages to appropriate views based on filter rules.
147
+
148
+ **File:** `lib/heathrow/message_router.rb`
149
+
150
+ ```ruby
151
+ module Heathrow
152
+ class MessageRouter
153
+ def initialize(filter_engine, view_manager, db)
154
+
155
+ # Route a single message
156
+ def route_message(message)
157
+
158
+ # Route multiple messages (batch)
159
+ def route_messages(messages)
160
+
161
+ # Get messages for a specific view
162
+ def messages_for_view(view_id)
163
+ end
164
+ end
165
+ ```
166
+
167
+ #### View Manager
168
+
169
+ Manages views (buffers) and their configurations.
170
+
171
+ **File:** `lib/heathrow/view_manager.rb`
172
+
173
+ ```ruby
174
+ module Heathrow
175
+ class ViewManager
176
+ def initialize(db, filter_engine)
177
+
178
+ # Get all views
179
+ def all_views
180
+
181
+ # Get view by ID
182
+ def get_view(id)
183
+
184
+ # Get view by key binding
185
+ def get_view_by_key(key)
186
+
187
+ # Create new view
188
+ def create_view(name, filters, key_binding: nil)
189
+
190
+ # Update view
191
+ def update_view(id, attributes)
192
+
193
+ # Delete view
194
+ def delete_view(id)
195
+
196
+ # Get remainder view (catch-all)
197
+ def remainder_view
198
+ end
199
+ end
200
+ ```
201
+
202
+ #### Filter Engine
203
+
204
+ Evaluates filter rules against messages.
205
+
206
+ **File:** `lib/heathrow/filter_engine.rb`
207
+
208
+ ```ruby
209
+ module Heathrow
210
+ class FilterEngine
211
+ def initialize
212
+
213
+ # Check if message matches filter
214
+ def matches?(message, filter)
215
+
216
+ # Evaluate complex filter expression
217
+ def evaluate(message, expression)
218
+
219
+ # Parse filter string to AST
220
+ def parse_filter(filter_string)
221
+ end
222
+ end
223
+ ```
224
+
225
+ #### Stream Manager
226
+
227
+ Manages real-time message streams from plugins.
228
+
229
+ **File:** `lib/heathrow/stream_manager.rb`
230
+
231
+ ```ruby
232
+ module Heathrow
233
+ class StreamManager
234
+ def initialize(plugin_manager, event_bus, message_router)
235
+
236
+ # Start streaming from plugin
237
+ def start_stream(source_id)
238
+
239
+ # Stop streaming
240
+ def stop_stream(source_id)
241
+
242
+ # Get active streams
243
+ def active_streams
244
+ end
245
+ end
246
+ ```
247
+
248
+ #### Search Engine
249
+
250
+ Full-text search across all messages.
251
+
252
+ **File:** `lib/heathrow/search_engine.rb`
253
+
254
+ ```ruby
255
+ module Heathrow
256
+ class SearchEngine
257
+ def initialize(db)
258
+
259
+ # Search messages
260
+ def search(query, filters: {})
261
+
262
+ # Save search
263
+ def save_search(name, query)
264
+
265
+ # Get saved searches
266
+ def saved_searches
267
+ end
268
+ end
269
+ ```
270
+
271
+ **Dependencies:**
272
+ - Down: Core Layer (database, event_bus, plugin_manager)
273
+ - Up: UI Layer
274
+
275
+ **Isolation:**
276
+ - Business logic independent of UI
277
+ - Can run headless (for testing, automation)
278
+ - No direct plugin access (only through plugin_manager)
279
+
280
+ ---
281
+
282
+ ### 3. Core Layer
283
+
284
+ **Responsibility:** Infrastructure services
285
+
286
+ **Components:**
287
+
288
+ #### Plugin Manager
289
+
290
+ **File:** `lib/heathrow/plugin_manager.rb`
291
+
292
+ ```ruby
293
+ module Heathrow
294
+ class PluginManager
295
+ def initialize(event_bus, config, db)
296
+
297
+ # Load all enabled plugins
298
+ def load_all
299
+
300
+ # Load specific plugin
301
+ def load_plugin(plugin_type, source_id)
302
+
303
+ # Unload plugin
304
+ def unload_plugin(source_id)
305
+
306
+ # Reload plugin
307
+ def reload_plugin(source_id)
308
+
309
+ # Get plugin instance
310
+ def get_plugin(source_id)
311
+
312
+ # List all loaded plugins
313
+ def list_plugins
314
+ end
315
+ end
316
+ ```
317
+
318
+ #### Config Manager
319
+
320
+ **File:** `lib/heathrow/config.rb`
321
+
322
+ ```ruby
323
+ module Heathrow
324
+ class Config
325
+ def initialize(config_path = "~/.heathrow/config.yml")
326
+
327
+ # Get config value (supports dot notation: "ui.theme")
328
+ def get(key_path, default = nil)
329
+
330
+ # Set config value
331
+ def set(key_path, value)
332
+
333
+ # Save to disk
334
+ def save
335
+
336
+ # Reload from disk
337
+ def reload
338
+
339
+ # Validate configuration
340
+ def validate
341
+ end
342
+ end
343
+ ```
344
+
345
+ #### Database Layer
346
+
347
+ **File:** `lib/heathrow/database.rb`
348
+
349
+ ```ruby
350
+ module Heathrow
351
+ class Database
352
+ def initialize(db_path = "~/.heathrow/heathrow.db")
353
+
354
+ # Execute statement (INSERT, UPDATE, DELETE)
355
+ def exec(sql, params = [])
356
+
357
+ # Query data (SELECT)
358
+ def query(sql, params = [])
359
+
360
+ # Transaction wrapper
361
+ def transaction(&block)
362
+
363
+ # Migrate to latest schema version
364
+ def migrate_to_latest
365
+
366
+ # Backup database
367
+ def backup
368
+ end
369
+ end
370
+ ```
371
+
372
+ #### Event Bus
373
+
374
+ **File:** `lib/heathrow/event_bus.rb`
375
+
376
+ ```ruby
377
+ module Heathrow
378
+ class EventBus
379
+ def initialize
380
+
381
+ # Subscribe to event type
382
+ # Returns handler_id for unsubscribing
383
+ def subscribe(event_type, &handler)
384
+
385
+ # Unsubscribe handler
386
+ def unsubscribe(handler_id)
387
+
388
+ # Publish event
389
+ def publish(event_type, data)
390
+
391
+ # Clear all handlers
392
+ def clear
393
+
394
+ # Get subscription count
395
+ def subscription_count(event_type = nil)
396
+ end
397
+ end
398
+ ```
399
+
400
+ **Event Types:**
401
+
402
+ ```ruby
403
+ # Message events
404
+ :message_received # New message fetched
405
+ :message_sent # Message sent successfully
406
+ :message_read # Message marked as read
407
+ :message_starred # Message starred
408
+ :message_archived # Message archived
409
+
410
+ # Plugin events
411
+ :plugin_loaded # Plugin loaded successfully
412
+ :plugin_unloaded # Plugin unloaded
413
+ :plugin_error # Plugin encountered error
414
+ :plugin_reconnecting # Plugin attempting reconnect
415
+
416
+ # View events
417
+ :view_changed # User switched views
418
+ :view_created # New view created
419
+ :view_deleted # View deleted
420
+
421
+ # UI events
422
+ :ui_refresh # UI needs refresh
423
+ :ui_resize # Terminal resized
424
+
425
+ # Sync events
426
+ :sync_started # Sync began
427
+ :sync_completed # Sync finished
428
+ :sync_failed # Sync error
429
+
430
+ # Search events
431
+ :search_executed # Search performed
432
+
433
+ # Log events
434
+ :log # Log message
435
+ ```
436
+
437
+ #### Logger
438
+
439
+ **File:** `lib/heathrow/logger.rb`
440
+
441
+ ```ruby
442
+ module Heathrow
443
+ class Logger
444
+ def initialize(log_path = "~/.heathrow/heathrow.log")
445
+
446
+ # Log methods
447
+ def debug(component, message)
448
+ def info(component, message)
449
+ def warn(component, message)
450
+ def error(component, message, exception = nil)
451
+
452
+ # Rotate log files
453
+ def rotate
454
+
455
+ # Set log level
456
+ def level=(level) # :debug, :info, :warn, :error
457
+ end
458
+ end
459
+ ```
460
+
461
+ #### Cache
462
+
463
+ **File:** `lib/heathrow/cache.rb`
464
+
465
+ ```ruby
466
+ module Heathrow
467
+ class Cache
468
+ def initialize(ttl: 300)
469
+
470
+ # Get cached value
471
+ def get(key)
472
+
473
+ # Set cached value
474
+ def set(key, value, ttl: nil)
475
+
476
+ # Delete cached value
477
+ def delete(key)
478
+
479
+ # Clear all cache
480
+ def clear
481
+
482
+ # Clear expired entries
483
+ def prune
484
+ end
485
+ end
486
+ ```
487
+
488
+ **Dependencies:**
489
+ - Down: None (bottom layer - only system libraries)
490
+ - Up: Application Layer, Plugin Layer
491
+
492
+ **Isolation:**
493
+ - Infrastructure services used by all layers
494
+ - No business logic
495
+ - Highly tested and stable
496
+
497
+ ---
498
+
499
+ ### 4. Plugin Layer
500
+
501
+ **Responsibility:** External service integrations
502
+
503
+ **Components:**
504
+ - Individual plugins (Gmail, Slack, Discord, etc.)
505
+ - Plugin Base Class
506
+
507
+ **File Structure:**
508
+
509
+ ```
510
+ lib/heathrow/plugins/
511
+ ├── gmail.rb
512
+ ├── slack.rb
513
+ ├── discord.rb
514
+ ├── telegram.rb
515
+ ├── rss.rb
516
+ ├── reddit.rb
517
+ ├── irc.rb
518
+ └── ...
519
+ ```
520
+
521
+ **Dependencies:**
522
+ - Down: None (external APIs only)
523
+ - Up: Core Layer (plugin_manager)
524
+
525
+ **Isolation:**
526
+ - Plugins are completely isolated from each other
527
+ - Plugin crash doesn't affect other plugins or core
528
+ - Plugins loaded/unloaded independently
529
+ - Plugins communicate only via Event Bus
530
+ - No direct plugin-to-plugin calls
531
+
532
+ ---
533
+
534
+ ## Component Isolation
535
+
536
+ ### Isolation Guarantees
537
+
538
+ | Component | Can Access | Cannot Access | Failure Impact |
539
+ |----------------|--------------------|----------------------|----------------|
540
+ | UI Layer | Application Layer | Core, Plugins | UI only |
541
+ | Message Router | Core Layer | Plugins directly | Routing only |
542
+ | View Manager | DB, Filter Engine | Plugins, UI | Views only |
543
+ | Filter Engine | None | All others | Filtering only |
544
+ | Plugin Manager | DB, Event Bus | Plugins' internals | Plugin mgmt |
545
+ | Plugin (Gmail) | Core API only | Other plugins, UI | Gmail only |
546
+ | Database | SQLite only | All others | ✗ CRITICAL |
547
+ | Event Bus | Logger only | All others | ✗ CRITICAL |
548
+
549
+ ### Critical Components
550
+
551
+ These components **must never crash**:
552
+
553
+ 1. **Database** - All data access
554
+ 2. **Event Bus** - All inter-component communication
555
+ 3. **Logger** - Debugging and audit trail
556
+ 4. **Config Manager** - System configuration
557
+
558
+ **Protection Strategy:**
559
+ - Extensive unit testing (100% coverage)
560
+ - Integration testing
561
+ - Defensive programming (validate all inputs)
562
+ - Fallback mechanisms
563
+ - No external dependencies (use stdlib only)
564
+
565
+ ### Safe Components
566
+
567
+ These components **can crash safely**:
568
+
569
+ 1. **Plugins** - Isolated, can be restarted
570
+ 2. **Filter Engine** - Defaults to "show all" if fails
571
+ 3. **Search Engine** - Non-essential feature
572
+ 4. **Cache** - Can be cleared and rebuilt
573
+
574
+ **Recovery Strategy:**
575
+ - Error boundaries catch exceptions
576
+ - Log error details
577
+ - Notify user
578
+ - Continue with degraded functionality
579
+ - Allow retry
580
+
581
+ ---
582
+
583
+ ## Data Flow
584
+
585
+ ### Message Ingestion Flow
586
+
587
+ ```
588
+ 1. Plugin fetches messages from external service
589
+
590
+
591
+ 2. Plugin.normalize_message() converts to Heathrow::Message
592
+
593
+
594
+ 3. Plugin emits :message_received event
595
+
596
+
597
+ 4. StreamManager receives event
598
+
599
+
600
+ 5. StreamManager passes to MessageRouter
601
+
602
+
603
+ 6. MessageRouter saves to Database
604
+
605
+
606
+ 7. MessageRouter evaluates filter rules
607
+
608
+
609
+ 8. MessageRouter updates view message counts
610
+
611
+
612
+ 9. MessageRouter emits :ui_refresh event
613
+
614
+
615
+ 10. UI refreshes display
616
+ ```
617
+
618
+ ### Message Sending Flow
619
+
620
+ ```
621
+ 1. User composes message in UI
622
+
623
+
624
+ 2. UI creates Heathrow::Message object
625
+
626
+
627
+ 3. UI calls MessageRouter.send_message()
628
+
629
+
630
+ 4. MessageRouter determines target plugin
631
+
632
+
633
+ 5. MessageRouter calls Plugin.send_message()
634
+
635
+
636
+ 6. Plugin sends via external API
637
+
638
+
639
+ 7. Plugin saves to Database (sent messages)
640
+
641
+
642
+ 8. Plugin emits :message_sent event
643
+
644
+
645
+ 9. UI shows confirmation
646
+ ```
647
+
648
+ ### View Switching Flow
649
+
650
+ ```
651
+ 1. User presses view key (e.g., "1")
652
+
653
+
654
+ 2. UI calls ViewManager.get_view_by_key("1")
655
+
656
+
657
+ 3. ViewManager queries Database for view
658
+
659
+
660
+ 4. ViewManager returns View object
661
+
662
+
663
+ 5. UI calls MessageRouter.messages_for_view(view.id)
664
+
665
+
666
+ 6. MessageRouter queries Database with view filters
667
+
668
+
669
+ 7. MessageRouter returns Message[]
670
+
671
+
672
+ 8. UI renders messages in pane
673
+
674
+
675
+ 9. UI emits :view_changed event
676
+ ```
677
+
678
+ ---
679
+
680
+ ## Concurrency Model
681
+
682
+ ### Threading Strategy
683
+
684
+ **Main Thread:**
685
+ - UI event loop
686
+ - User input handling
687
+ - Rendering
688
+
689
+ **Background Threads:**
690
+ - Plugin real-time streams (one thread per plugin)
691
+ - Periodic sync tasks
692
+ - Database operations (via connection pool)
693
+
694
+ **Thread Safety:**
695
+
696
+ ```ruby
697
+ # Database: Thread-safe via mutex
698
+ class Database
699
+ def initialize(db_path)
700
+ @db = SQLite3::Database.new(db_path)
701
+ @mutex = Mutex.new
702
+ end
703
+
704
+ def query(sql, params = [])
705
+ @mutex.synchronize do
706
+ @db.execute(sql, params)
707
+ end
708
+ end
709
+ end
710
+
711
+ # Event Bus: Thread-safe via Queue
712
+ class EventBus
713
+ def initialize
714
+ @handlers = {}
715
+ @queue = Queue.new
716
+ @mutex = Mutex.new
717
+
718
+ # Start event processing thread
719
+ start_processor
720
+ end
721
+
722
+ def publish(event_type, data)
723
+ @queue.push([event_type, data])
724
+ end
725
+
726
+ private
727
+
728
+ def start_processor
729
+ Thread.new do
730
+ loop do
731
+ event_type, data = @queue.pop
732
+ dispatch(event_type, data)
733
+ end
734
+ end
735
+ end
736
+
737
+ def dispatch(event_type, data)
738
+ handlers = @mutex.synchronize { @handlers[event_type]&.dup || [] }
739
+ handlers.each { |h| h.call(data) rescue nil }
740
+ end
741
+ end
742
+
743
+ # Plugin Streams: Isolated threads
744
+ class StreamManager
745
+ def start_stream(source_id)
746
+ plugin = @plugin_manager.get_plugin(source_id)
747
+
748
+ thread = Thread.new do
749
+ loop do
750
+ begin
751
+ messages = plugin.fetch_messages
752
+ messages.each { |m| @message_router.route_message(m) }
753
+ sleep plugin.poll_interval
754
+ rescue => e
755
+ log_error(plugin, e)
756
+ sleep 60 # Backoff on error
757
+ end
758
+ end
759
+ end
760
+
761
+ @streams[source_id] = thread
762
+ end
763
+ end
764
+ ```
765
+
766
+ ### Synchronization Points
767
+
768
+ **Critical Sections:**
769
+ 1. Database writes (protected by mutex)
770
+ 2. Event handler registration (protected by mutex)
771
+ 3. Plugin list modifications (protected by mutex)
772
+
773
+ **Lock-Free Sections:**
774
+ 1. Reading messages from DB (read-only, no locks needed)
775
+ 2. Filter evaluation (pure function, no state)
776
+ 3. Message rendering (read-only)
777
+
778
+ ---
779
+
780
+ ## Error Handling Strategy
781
+
782
+ ### Error Hierarchy
783
+
784
+ ```
785
+ StandardError
786
+
787
+ ├─ Heathrow::Error (base for all Heathrow errors)
788
+ │ │
789
+ │ ├─ Heathrow::ConfigError
790
+ │ │ ├─ InvalidConfigError
791
+ │ │ └─ MissingConfigError
792
+ │ │
793
+ │ ├─ Heathrow::DatabaseError
794
+ │ │ ├─ MigrationError
795
+ │ │ └─ QueryError
796
+ │ │
797
+ │ ├─ Heathrow::Plugin::Error
798
+ │ │ ├─ ConnectionError
799
+ │ │ ├─ AuthenticationError
800
+ │ │ ├─ RateLimitError
801
+ │ │ ├─ NotFoundError
802
+ │ │ └─ ValidationError
803
+ │ │
804
+ │ └─ Heathrow::UIError
805
+ │ ├─ RenderError
806
+ │ └─ InputError
807
+ ```
808
+
809
+ ### Error Boundaries
810
+
811
+ **UI Layer:**
812
+
813
+ ```ruby
814
+ def run
815
+ loop do
816
+ begin
817
+ key = get_input
818
+ handle_input(key)
819
+ refresh
820
+ rescue UIError => e
821
+ show_error_message(e.message)
822
+ log(:error, "UI error: #{e.message}")
823
+ # Continue running
824
+ rescue => e
825
+ log(:error, "Unexpected error: #{e.message}")
826
+ show_crash_screen(e)
827
+ # Attempt to continue or graceful shutdown
828
+ end
829
+ end
830
+ end
831
+ ```
832
+
833
+ **Plugin Layer:**
834
+
835
+ ```ruby
836
+ def fetch_messages_safe(plugin)
837
+ plugin.fetch_messages
838
+ rescue Plugin::RateLimitError => e
839
+ log(:warn, "Rate limited: #{plugin.name}")
840
+ schedule_retry(plugin, delay: 60)
841
+ []
842
+ rescue Plugin::ConnectionError => e
843
+ log(:warn, "Connection error: #{plugin.name}")
844
+ schedule_retry(plugin, delay: 30)
845
+ []
846
+ rescue Plugin::AuthenticationError => e
847
+ log(:error, "Auth failed: #{plugin.name}")
848
+ notify_user("Please re-authenticate #{plugin.name}")
849
+ disable_plugin(plugin)
850
+ []
851
+ rescue => e
852
+ log(:error, "Plugin crashed: #{plugin.name} - #{e.message}")
853
+ disable_plugin(plugin)
854
+ []
855
+ end
856
+ ```
857
+
858
+ **Database Layer:**
859
+
860
+ ```ruby
861
+ def query(sql, params = [])
862
+ @mutex.synchronize do
863
+ @db.execute(sql, params)
864
+ end
865
+ rescue SQLite3::SQLException => e
866
+ log(:error, "SQL error: #{e.message}")
867
+ raise DatabaseError, "Query failed: #{e.message}"
868
+ rescue => e
869
+ log(:error, "Unexpected DB error: #{e.message}")
870
+ # Attempt to reconnect
871
+ reconnect
872
+ retry
873
+ end
874
+ ```
875
+
876
+ ### Recovery Strategies
877
+
878
+ | Error Type | Strategy | User Impact |
879
+ |----------------------|-----------------------------------|-------------|
880
+ | Plugin crash | Disable plugin, notify user | Loss of one source |
881
+ | Database locked | Retry with backoff | Brief delay |
882
+ | Config invalid | Use defaults, notify user | Degraded UX |
883
+ | UI render error | Clear screen, re-render | Brief flicker |
884
+ | Network timeout | Retry, then skip | Delayed sync |
885
+ | Rate limit | Backoff, retry later | Delayed sync |
886
+ | Auth expired | Prompt re-auth | User action required |
887
+ | Out of memory | Clear cache, log warning | Slower performance |
888
+ | Disk full | Notify user, stop syncing | No new messages |
889
+
890
+ ---
891
+
892
+ ## Testing Strategy
893
+
894
+ ### Test Pyramid
895
+
896
+ ```
897
+ ┌─────────────┐
898
+ │ Manual │ ← 5% (exploratory testing)
899
+ │ Testing │
900
+ ┌─┴─────────────┴─┐
901
+ │ Integration │ ← 20% (component interaction)
902
+ │ Tests │
903
+ ┌─┴─────────────────┴─┐
904
+ │ Unit Tests │ ← 75% (component isolation)
905
+ └─────────────────────┘
906
+ ```
907
+
908
+ ### Unit Testing
909
+
910
+ **Goal:** Test individual components in isolation
911
+
912
+ **Coverage Target:** 90%+ for critical components, 70%+ overall
913
+
914
+ **Example:**
915
+
916
+ ```ruby
917
+ # test/test_filter_engine.rb
918
+ class TestFilterEngine < Minitest::Test
919
+ def setup
920
+ @engine = Heathrow::FilterEngine.new
921
+ @message = Heathrow::Message.new(
922
+ sender: "test@example.com",
923
+ subject: "Test message",
924
+ content: "Hello world",
925
+ read: false
926
+ )
927
+ end
928
+
929
+ def test_simple_equality_filter
930
+ filter = {field: "sender", op: "=", value: "test@example.com"}
931
+ assert @engine.matches?(@message, filter)
932
+ end
933
+
934
+ def test_complex_and_filter
935
+ filter = {
936
+ rules: [
937
+ {field: "read", op: "=", value: false},
938
+ {field: "sender", op: "=", value: "test@example.com"}
939
+ ],
940
+ logic: "AND"
941
+ }
942
+ assert @engine.matches?(@message, filter)
943
+ end
944
+ end
945
+ ```
946
+
947
+ ### Integration Testing
948
+
949
+ **Goal:** Test component interaction
950
+
951
+ **Coverage:** Key workflows and data flows
952
+
953
+ **Example:**
954
+
955
+ ```ruby
956
+ # test/integration/test_message_flow.rb
957
+ class TestMessageFlow < Minitest::Test
958
+ def setup
959
+ @db = Database.new(":memory:")
960
+ @event_bus = EventBus.new
961
+ @config = Config.new
962
+ @plugin_manager = PluginManager.new(@event_bus, @config, @db)
963
+ @message_router = MessageRouter.new(@filter_engine, @view_manager, @db)
964
+ end
965
+
966
+ def test_message_ingestion_to_view
967
+ # 1. Create test plugin
968
+ source_id = create_test_source("test_plugin")
969
+ plugin = @plugin_manager.load_plugin("test_plugin", source_id)
970
+
971
+ # 2. Create test view
972
+ view = @view_manager.create_view("Test", {field: "sender", op: "=", value: "test@example.com"})
973
+
974
+ # 3. Fetch messages
975
+ messages = plugin.fetch_messages
976
+
977
+ # 4. Route to views
978
+ messages.each { |m| @message_router.route_message(m) }
979
+
980
+ # 5. Verify messages in view
981
+ view_messages = @message_router.messages_for_view(view.id)
982
+ assert view_messages.count > 0
983
+ end
984
+ end
985
+ ```
986
+
987
+ ### End-to-End Testing
988
+
989
+ **Goal:** Test complete user workflows
990
+
991
+ **Coverage:** Critical paths only
992
+
993
+ **Example:**
994
+
995
+ ```ruby
996
+ # test/e2e/test_user_workflows.rb
997
+ class TestUserWorkflows < Minitest::Test
998
+ def test_read_and_reply_workflow
999
+ # 1. Start application
1000
+ app = Heathrow::UI::Application.new
1001
+
1002
+ # 2. Simulate view switch
1003
+ app.handle_input("1") # Switch to view 1
1004
+
1005
+ # 3. Simulate message selection
1006
+ app.handle_input("j") # Move down
1007
+ app.handle_input("ENTER") # Open message
1008
+
1009
+ # 4. Verify message marked as read
1010
+ message = app.current_message
1011
+ assert message.read
1012
+
1013
+ # 5. Simulate reply
1014
+ app.handle_input("r") # Reply
1015
+ # ... compose and send
1016
+
1017
+ # 6. Verify reply sent
1018
+ assert_event_published(:message_sent)
1019
+ end
1020
+ end
1021
+ ```
1022
+
1023
+ ### Test Helpers
1024
+
1025
+ ```ruby
1026
+ # test/test_helper.rb
1027
+ module TestHelper
1028
+ def create_test_message(overrides = {})
1029
+ defaults = {
1030
+ external_id: SecureRandom.uuid,
1031
+ sender: "test@example.com",
1032
+ subject: "Test",
1033
+ content: "Test message",
1034
+ timestamp: Time.now.to_i,
1035
+ received_at: Time.now.to_i
1036
+ }
1037
+ Heathrow::Message.new(defaults.merge(overrides))
1038
+ end
1039
+
1040
+ def create_test_source(plugin_type)
1041
+ @db.exec(
1042
+ "INSERT INTO sources (name, plugin_type, config, capabilities, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
1043
+ ["Test Source", plugin_type, "{}", '["read"]', 1, Time.now.to_i, Time.now.to_i]
1044
+ )
1045
+ @db.query("SELECT last_insert_rowid()").first.first
1046
+ end
1047
+ end
1048
+ ```
1049
+
1050
+ ---
1051
+
1052
+ ## Performance Considerations
1053
+
1054
+ ### Performance Budget
1055
+
1056
+ | Operation | Target | Maximum |
1057
+ |------------------------|------------|-----------|
1058
+ | UI refresh | < 50ms | < 100ms |
1059
+ | View switch | < 100ms | < 200ms |
1060
+ | Message fetch (100) | < 2s | < 5s |
1061
+ | Database query | < 20ms | < 50ms |
1062
+ | Search query | < 100ms | < 500ms |
1063
+ | Send message | < 1s | < 3s |
1064
+ | Memory (idle) | < 50MB | < 100MB |
1065
+ | Memory (active) | < 100MB | < 200MB |
1066
+
1067
+ ### Optimization Strategies
1068
+
1069
+ **Database:**
1070
+ - Index frequently queried columns
1071
+ - Use prepared statements
1072
+ - Batch inserts in transactions
1073
+ - Periodic VACUUM
1074
+ - Archive old messages
1075
+
1076
+ **UI:**
1077
+ - Lazy rendering (only visible messages)
1078
+ - Virtual scrolling for long lists
1079
+ - Debounce rapid input
1080
+ - Cache rendered content
1081
+
1082
+ **Plugins:**
1083
+ - Connection pooling
1084
+ - Request batching
1085
+ - Response caching
1086
+ - Rate limit respect
1087
+
1088
+ **Memory:**
1089
+ - Weak references for caches
1090
+ - Periodic cache pruning
1091
+ - Stream processing (don't load all at once)
1092
+ - Profile for leaks
1093
+
1094
+ ### Monitoring
1095
+
1096
+ ```ruby
1097
+ # lib/heathrow/performance_monitor.rb
1098
+ module Heathrow
1099
+ class PerformanceMonitor
1100
+ def measure(operation)
1101
+ start = Time.now
1102
+ result = yield
1103
+ duration = ((Time.now - start) * 1000).round(2)
1104
+
1105
+ log(:debug, "#{operation}: #{duration}ms")
1106
+
1107
+ if duration > threshold_for(operation)
1108
+ log(:warn, "Slow operation #{operation}: #{duration}ms")
1109
+ end
1110
+
1111
+ result
1112
+ end
1113
+
1114
+ private
1115
+
1116
+ def threshold_for(operation)
1117
+ {
1118
+ "ui_refresh" => 100,
1119
+ "database_query" => 50,
1120
+ "message_fetch" => 5000,
1121
+ "view_switch" => 200
1122
+ }[operation] || 1000
1123
+ end
1124
+ end
1125
+ end
1126
+ ```
1127
+
1128
+ ---
1129
+
1130
+ ## Deployment Topology
1131
+
1132
+ ### Single-User Desktop
1133
+
1134
+ ```
1135
+ ┌────────────────────────────────┐
1136
+ │ User's Computer │
1137
+ │ ┌──────────────────────────┐ │
1138
+ │ │ Heathrow Process │ │
1139
+ │ │ ┌────────────────────┐ │ │
1140
+ │ │ │ UI (Terminal) │ │ │
1141
+ │ │ ├────────────────────┤ │ │
1142
+ │ │ │ Application │ │ │
1143
+ │ │ ├────────────────────┤ │ │
1144
+ │ │ │ Core + Plugins │ │ │
1145
+ │ │ └────────────────────┘ │ │
1146
+ │ │ ┌────────────────────┐ │ │
1147
+ │ │ │ SQLite DB │ │ │
1148
+ │ │ │ ~/.heathrow/ │ │ │
1149
+ │ │ └────────────────────┘ │ │
1150
+ │ └──────────────────────────┘ │
1151
+ └────────────────────────────────┘
1152
+ ```
1153
+
1154
+ ### Client-Server (Future)
1155
+
1156
+ ```
1157
+ ┌─────────────────┐ ┌──────────────────────┐
1158
+ │ Client │ │ Server │
1159
+ │ ┌───────────┐ │ │ ┌────────────────┐ │
1160
+ │ │ UI Only │ │ <───> │ │ Application │ │
1161
+ │ └───────────┘ │ REST │ │ + Core │ │
1162
+ │ │ │ │ + Plugins │ │
1163
+ │ │ │ └────────────────┘ │
1164
+ │ │ │ ┌────────────────┐ │
1165
+ │ │ │ │ PostgreSQL │ │
1166
+ │ │ │ └────────────────┘ │
1167
+ └─────────────────┘ └──────────────────────┘
1168
+ ```
1169
+
1170
+ ---
1171
+
1172
+ This architecture ensures Heathrow is modular, testable, and resilient. Each component can be developed, tested, and deployed independently without breaking other parts of the system.