@anastops/cli 2.0.1 → 2.1.1

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 (42) hide show
  1. package/dist/commands/doctor.d.ts.map +1 -1
  2. package/dist/commands/doctor.js +220 -4
  3. package/dist/commands/doctor.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +92 -4
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/uninstall.d.ts.map +1 -1
  8. package/dist/commands/uninstall.js +26 -5
  9. package/dist/commands/uninstall.js.map +1 -1
  10. package/dist/index.js +0 -0
  11. package/dist/index.js.map +1 -1
  12. package/package.json +6 -3
  13. package/ranger-tui/ranger_tui/__pycache__/__init__.cpython-314.pyc +0 -0
  14. package/ranger-tui/ranger_tui/__pycache__/app.cpython-314.pyc +0 -0
  15. package/ranger-tui/ranger_tui/actions/__pycache__/__init__.cpython-314.pyc +0 -0
  16. package/ranger-tui/ranger_tui/actions/__pycache__/agent_actions.cpython-314.pyc +0 -0
  17. package/ranger-tui/ranger_tui/actions/__pycache__/session_actions.cpython-314.pyc +0 -0
  18. package/ranger-tui/ranger_tui/actions/__pycache__/task_actions.cpython-314.pyc +0 -0
  19. package/ranger-tui/ranger_tui/app.py +113 -22
  20. package/ranger-tui/ranger_tui/data/__init__.py +2 -1
  21. package/ranger-tui/ranger_tui/data/__pycache__/__init__.cpython-314.pyc +0 -0
  22. package/ranger-tui/ranger_tui/data/__pycache__/client.cpython-314.pyc +0 -0
  23. package/ranger-tui/ranger_tui/data/client.py +38 -10
  24. package/ranger-tui/ranger_tui/screens/__init__.py +2 -0
  25. package/ranger-tui/ranger_tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
  26. package/ranger-tui/ranger_tui/screens/__pycache__/command_palette.cpython-314.pyc +0 -0
  27. package/ranger-tui/ranger_tui/screens/__pycache__/dashboard.cpython-314.pyc +0 -0
  28. package/ranger-tui/ranger_tui/screens/__pycache__/help.cpython-314.pyc +0 -0
  29. package/ranger-tui/ranger_tui/screens/__pycache__/loading.cpython-314.pyc +0 -0
  30. package/ranger-tui/ranger_tui/screens/__pycache__/session.cpython-314.pyc +0 -0
  31. package/ranger-tui/ranger_tui/screens/__pycache__/task.cpython-314.pyc +0 -0
  32. package/ranger-tui/ranger_tui/screens/dashboard.py +18 -4
  33. package/ranger-tui/ranger_tui/screens/loading.py +214 -0
  34. package/ranger-tui/ranger_tui/screens/session.py +19 -5
  35. package/ranger-tui/ranger_tui/screens/task.py +18 -4
  36. package/ranger-tui/ranger_tui/widgets/__pycache__/agents_table.cpython-314.pyc +0 -0
  37. package/ranger-tui/ranger_tui/widgets/__pycache__/header.cpython-314.pyc +0 -0
  38. package/ranger-tui/ranger_tui/widgets/__pycache__/metrics_panel.cpython-314.pyc +0 -0
  39. package/ranger-tui/ranger_tui/widgets/__pycache__/sessions_table.cpython-314.pyc +0 -0
  40. package/ranger-tui/ranger_tui/widgets/__pycache__/tasks_table.cpython-314.pyc +0 -0
  41. package/ranger-tui/ranger_tui/widgets/__pycache__/topbar.cpython-314.pyc +0 -0
  42. package/ranger-tui/ranger_tui/widgets/topbar.py +14 -36
@@ -0,0 +1,214 @@
1
+ """
2
+ Loading screen displayed during database connection.
3
+
4
+ Uses a wave animation (dots highlighting left to right) inside the loading box.
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional
9
+
10
+ from textual.screen import Screen
11
+ from textual.app import ComposeResult
12
+ from textual.containers import Vertical, Horizontal, Container
13
+ from textual.widgets import Static
14
+ from textual.reactive import reactive
15
+ from textual.timer import Timer
16
+
17
+ from ranger_tui.widgets.ranger_image import RangerImage
18
+
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class LoadingScreen(Screen):
24
+ """Loading screen with wave animation inside the box.
25
+
26
+ Uses a wave animation (dots highlighting left to right) inside the loading box.
27
+ """
28
+
29
+ CSS = """
30
+ LoadingScreen {
31
+ background: #000000;
32
+ align: center middle;
33
+ }
34
+
35
+ #loading-box {
36
+ width: auto;
37
+ min-width: 150;
38
+ height: auto;
39
+ border: round #3d5c8c;
40
+ padding: 2 4;
41
+ align: center middle;
42
+ }
43
+
44
+ #loading-inner {
45
+ width: 100%;
46
+ height: auto;
47
+ align: center middle;
48
+ }
49
+
50
+ #loading-title-row {
51
+ width: auto;
52
+ height: auto;
53
+ align: center middle;
54
+ }
55
+
56
+ #loading-title-anastops {
57
+ width: auto;
58
+ height: auto;
59
+ content-align: right middle;
60
+ padding-right: 3;
61
+ }
62
+
63
+ #loading-image-container {
64
+ width: auto;
65
+ height: auto;
66
+ align: center middle;
67
+ }
68
+
69
+ #loading-title-ranger {
70
+ width: auto;
71
+ height: auto;
72
+ content-align: left middle;
73
+ padding-left: 3;
74
+ }
75
+
76
+ #loading-progress-container {
77
+ width: 100%;
78
+ height: auto;
79
+ align: center middle;
80
+ margin-top: 6;
81
+ margin-bottom: 4;
82
+ }
83
+
84
+ #loading-animation {
85
+ width: 100%;
86
+ height: 1;
87
+ text-align: center;
88
+ content-align: center middle;
89
+ }
90
+
91
+ #loading-status-text {
92
+ width: 100%;
93
+ height: auto;
94
+ text-align: center;
95
+ content-align: center middle;
96
+ color: #7aa2f7;
97
+ margin-top: 1;
98
+ }
99
+ """
100
+
101
+ # Reactive variables
102
+ status_text: reactive[str] = reactive("Waiting for connection...")
103
+ loading_animation: reactive[str] = reactive("")
104
+
105
+ # Animation state
106
+ animation_timer: Optional[Timer] = None
107
+ animation_frame: int = 0
108
+
109
+ def _update_loading_animation(self) -> None:
110
+ """Update loading animation with wave pattern (dots highlighting left to right)."""
111
+ num_dots = 25
112
+
113
+ # Current position of the wave
114
+ current_pos = self.animation_frame % num_dots
115
+
116
+ # Build dots WITHOUT padding spaces - let CSS center them
117
+ dots = []
118
+ for i in range(num_dots):
119
+ # Calculate distance from current wave position
120
+ distance = abs(i - current_pos)
121
+
122
+ if distance == 0:
123
+ dots.append('[bold #7aa2f7]\u25cf[/]') # Full circle (bright blue)
124
+ elif distance == 1:
125
+ dots.append('[#5a7fa8]\u25cb[/]') # Empty circle (medium blue)
126
+ else:
127
+ dots.append('[dim #3d5c8c]\u00b7[/]') # Middle dot (dim blue)
128
+
129
+ # Join with single space between dots - CSS text-align: center will center this
130
+ self.loading_animation = ' '.join(dots)
131
+ self.animation_frame += 1
132
+
133
+ def watch_loading_animation(self, value: str) -> None:
134
+ """Update loading animation widget when it changes."""
135
+ try:
136
+ animation = self.query_one("#loading-animation", Static)
137
+ animation.update(value)
138
+ except Exception:
139
+ pass
140
+
141
+ def compose(self) -> ComposeResult:
142
+ with Container(id="loading-box"):
143
+ with Vertical(id="loading-inner"):
144
+ # Horizontal title layout: ANASTOPS [image] RANGER
145
+ with Horizontal(id="loading-title-row"):
146
+ # ANASTOPS title (blue) - large box-drawing ASCII art
147
+ anastops_title = (
148
+ "[bold #7aa2f7]"
149
+ "\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\n"
150
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\n"
151
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\n"
152
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255a\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255a\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2588\u2588\u2551\n"
153
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\n"
154
+ "\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d[/]"
155
+ )
156
+ yield Static(anastops_title, id="loading-title-anastops", markup=True)
157
+
158
+ # Ranger image in center
159
+ with Container(id="loading-image-container"):
160
+ yield RangerImage()
161
+
162
+ # RANGER title (purple) - large box-drawing ASCII art
163
+ ranger_title = (
164
+ "[bold #bb9af7]"
165
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \n"
166
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255d\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\n"
167
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\n"
168
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255a\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255d \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\n"
169
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255a\u2588\u2588\u2588\u2588\u2551\u255a\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255d\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\n"
170
+ "\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d[/]"
171
+ )
172
+ yield Static(ranger_title, id="loading-title-ranger", markup=True)
173
+
174
+ # Animation and status text
175
+ with Vertical(id="loading-progress-container"):
176
+ yield Static("", id="loading-animation", markup=True)
177
+ yield Static(self.status_text, id="loading-status-text", markup=True)
178
+
179
+ def on_mount(self) -> None:
180
+ """Start the loading animation."""
181
+ logger.debug("LoadingScreen mounted - starting wave loading animation")
182
+ self.animation_frame = 0
183
+ # Use 0.1 seconds (10 FPS) for smooth animation
184
+ self.animation_timer = self.set_interval(0.1, self._update_loading_animation)
185
+
186
+ def watch_status_text(self, value: str) -> None:
187
+ """Update status text when it changes."""
188
+ try:
189
+ status = self.query_one("#loading-status-text", Static)
190
+ status.update(value)
191
+ status.refresh(layout=True)
192
+ self.refresh(layout=True)
193
+ except Exception:
194
+ pass
195
+
196
+ def set_status(self, text: str) -> None:
197
+ """Set the status text."""
198
+ self.status_text = text
199
+
200
+ def set_error(self, error_text: str) -> None:
201
+ """Display error message."""
202
+ self.status_text = f"[bold #f7768e]{error_text}[/]"
203
+
204
+ def hide_spinner(self) -> None:
205
+ """Hide the loading animation."""
206
+ try:
207
+ # Stop the animation timer
208
+ if self.animation_timer is not None:
209
+ self.animation_timer.stop()
210
+ self.animation_timer = None
211
+ # Clear the animation widget
212
+ self.query_one("#loading-animation", Static).update("")
213
+ except Exception:
214
+ pass
@@ -7,7 +7,7 @@ from textual.app import ComposeResult
7
7
  from textual.screen import Screen
8
8
  from textual.binding import Binding
9
9
  from textual.containers import Horizontal
10
- from textual.widgets import DataTable, Footer, TabbedContent, TabPane, RichLog
10
+ from textual.widgets import DataTable, Footer, TabbedContent, TabPane, RichLog, Static
11
11
 
12
12
  from ranger_tui.data import SessionReport
13
13
  from ranger_tui.widgets import TopBar, Sidebar
@@ -16,22 +16,36 @@ from ranger_tui.theme import format_status, format_model, STATUS_SYMBOLS
16
16
 
17
17
  class SessionScreen(Screen):
18
18
  """Session detail screen with tasks and agents."""
19
-
19
+
20
+ DEFAULT_CSS = """
21
+ .view-title {
22
+ height: 4;
23
+ content-align: center middle;
24
+ text-style: bold;
25
+ background: $surface;
26
+ border: tall $primary;
27
+ color: $primary;
28
+ text-align: center;
29
+ padding: 0 2;
30
+ }
31
+ """
32
+
20
33
  BINDINGS = [
21
34
  Binding("enter", "open_task", "Open", show=True),
22
35
  Binding("t", "new_task", "New Task", show=True),
23
36
  ]
24
-
37
+
25
38
  session_id: str = ""
26
39
  report: SessionReport | None = None
27
-
40
+
28
41
  def __init__(self, session_id: str):
29
42
  super().__init__()
30
43
  self.session_id = session_id
31
-
44
+
32
45
  def compose(self) -> ComposeResult:
33
46
  """Compose the session detail layout."""
34
47
  yield TopBar(title="Loading...")
48
+ yield Static("[bold]S E S S I O N D E T A I L S[/]", classes="view-title", markup=True)
35
49
  with Horizontal(id="main"):
36
50
  with TabbedContent(id="tabs"):
37
51
  with TabPane("Tasks", id="tasks-tab"):
@@ -5,7 +5,7 @@ Task detail screen - View task details, input, output, and errors.
5
5
  from textual.app import ComposeResult
6
6
  from textual.screen import Screen
7
7
  from textual.containers import Horizontal
8
- from textual.widgets import Footer, TabbedContent, TabPane, RichLog, Markdown
8
+ from textual.widgets import Footer, TabbedContent, TabPane, RichLog, Markdown, Static
9
9
 
10
10
  from ranger_tui.data import Task
11
11
  from ranger_tui.widgets import TopBar, Sidebar
@@ -14,17 +14,31 @@ from ranger_tui.theme import STATUS_SYMBOLS
14
14
 
15
15
  class TaskScreen(Screen):
16
16
  """Task detail screen."""
17
-
17
+
18
+ DEFAULT_CSS = """
19
+ .view-title {
20
+ height: 4;
21
+ content-align: center middle;
22
+ text-style: bold;
23
+ background: $surface;
24
+ border: tall $primary;
25
+ color: $primary;
26
+ text-align: center;
27
+ padding: 0 2;
28
+ }
29
+ """
30
+
18
31
  task_id: str = ""
19
32
  task: Task | None = None
20
-
33
+
21
34
  def __init__(self, task_id: str):
22
35
  super().__init__()
23
36
  self.task_id = task_id
24
-
37
+
25
38
  def compose(self) -> ComposeResult:
26
39
  """Compose the task detail layout."""
27
40
  yield TopBar(title="Loading...")
41
+ yield Static("[bold]T A S K D E T A I L S[/]", classes="view-title", markup=True)
28
42
  with Horizontal(id="main"):
29
43
  with TabbedContent(id="tabs"):
30
44
  with TabPane("Output", id="output-tab"):
@@ -37,23 +37,19 @@ class TopBar(Widget):
37
37
  margin: 0 1;
38
38
  }
39
39
 
40
- #topbar-view-title {
41
- width: auto;
42
- height: 100%;
43
- content-align: center middle;
44
- margin-left: 2;
45
- }
46
-
47
40
  .topbar-spacer {
48
41
  width: 1fr;
49
42
  }
50
43
 
51
44
  #topbar-status-frame {
52
- width: auto;
45
+ width: 28;
46
+ min-width: 26;
47
+ max-width: 32;
53
48
  height: auto;
54
- align: center middle;
49
+ background: $panel;
55
50
  border: round $secondary-darken-2;
56
- padding: 0 1;
51
+ padding: 1 2;
52
+ content-align: center middle;
57
53
  }
58
54
 
59
55
  TopBar > .topbar-status, #topbar-status-frame .topbar-status {
@@ -74,60 +70,42 @@ class TopBar(Widget):
74
70
 
75
71
  mongo_ok: reactive[bool] = reactive(False)
76
72
  redis_ok: reactive[bool] = reactive(False)
77
- view_title: reactive[str] = reactive("")
78
-
79
- def __init__(self, title: str | None = None, view_title: str = "") -> None:
73
+
74
+ def __init__(self, title: str | None = None) -> None:
80
75
  """
81
76
  Initialize TopBar.
82
-
77
+
83
78
  Args:
84
79
  title: Optional custom title. If None, uses the standard logo.
85
- view_title: Current view name (e.g., "Sessions", "Tasks")
86
80
  """
87
81
  super().__init__()
88
82
  self._custom_title = title
89
- self.view_title = view_title
90
83
 
91
84
  def compose(self) -> ComposeResult:
92
85
  # Left spacer (for centering)
93
86
  yield Static("", classes="topbar-spacer")
94
-
87
+
95
88
  # ANASTOPS title
96
89
  line1_left = "[bold #00d4ff]▄▀█[/] [bold #00c8ff]█▄[/][bold #00c8ff]█[/] [bold #00bcff]▄▀█[/] [bold #00b0ff]█▀[/] [bold #00a4ff]▀█▀[/] [bold #0098ff]█▀█[/] [bold #008cff]█▀█[/] [bold #0080ff]█▀[/]"
97
90
  line2_left = "[bold #00d4ff]█▀█[/] [bold #00c8ff]█[/] [bold #00c8ff]█[/] [bold #00bcff]█▀█[/] [bold #00b0ff]▄█[/] [bold #00a4ff]█[/] [bold #0098ff]█▄█[/] [bold #008cff]█▀▀[/] [bold #0080ff]▄█[/]"
98
91
  yield Static(line1_left + "\n" + line2_left, id="topbar-title-left", classes="topbar-title-text", markup=True)
99
-
92
+
100
93
  # Ranger image in center with circle frame
101
94
  with Horizontal(id="topbar-brand"):
102
95
  yield RangerImage()
103
-
96
+
104
97
  # RANGER title
105
98
  line1_right = "[bold #ff00ff]█▀█[/] [bold #e600ff]▄▀█[/] [bold #cc00ff]█▄[/][bold #cc00ff]█[/] [bold #b300ff]█▀▀[/] [bold #9900ff]█▀▀[/] [bold #8000ff]█▀█[/]"
106
99
  line2_right = "[bold #ff00ff]█▀▄[/] [bold #e600ff]█▀█[/] [bold #cc00ff]█[/] [bold #cc00ff]█[/] [bold #b300ff]█▄█[/] [bold #9900ff]██▄[/] [bold #8000ff]█▀▄[/]"
107
100
  yield Static(line1_right + "\n" + line2_right, id="topbar-title-right", classes="topbar-title-text", markup=True)
108
-
101
+
109
102
  # Right spacer (for centering)
110
103
  yield Static("", classes="topbar-spacer")
111
-
104
+
112
105
  # Status frame on far right
113
106
  with Horizontal(id="topbar-status-frame"):
114
107
  yield Static("", classes="topbar-status", id="topbar-status", markup=True)
115
108
 
116
- def watch_view_title(self, value: str) -> None:
117
- """Update view title display."""
118
- try:
119
- view_widget = self.query_one("#topbar-view-title", Static)
120
- if value:
121
- view_widget.update(f"[bold]{value}[/]")
122
- else:
123
- view_widget.update("")
124
- except Exception:
125
- pass
126
-
127
- def set_view_title(self, title: str) -> None:
128
- """Set the current view title."""
129
- self.view_title = title
130
-
131
109
  def watch_mongo_ok(self, value: bool) -> None:
132
110
  """Update status when mongo connection changes."""
133
111
  self._update_status()