solstone-linux 0.3.0__tar.gz → 0.3.2__tar.gz

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 (66) hide show
  1. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/CHANGELOG.md +14 -0
  2. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/INSTALL.md +8 -8
  3. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/PKG-INFO +2 -2
  4. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/README.md +1 -1
  5. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/pyproject.toml +1 -1
  6. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/__init__.py +1 -1
  7. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/chat_bridge.py +1 -1
  8. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/cli.py +16 -19
  9. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/config.py +9 -0
  10. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/dbus_service.py +3 -8
  11. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/doctor.py +12 -0
  12. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/sync.py +194 -53
  13. solstone_linux-0.3.2/src/solstone_linux/sync_health.py +364 -0
  14. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/tray.py +69 -77
  15. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/upload.py +22 -20
  16. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_chat_bridge.py +26 -0
  17. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_cli.py +30 -1
  18. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_dbus_service.py +12 -13
  19. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_doctor.py +26 -1
  20. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_sync.py +210 -23
  21. solstone_linux-0.3.2/tests/test_sync_health.py +121 -0
  22. solstone_linux-0.3.2/tests/test_sync_health_surfaces.py +191 -0
  23. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_tray.py +67 -40
  24. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_upload.py +19 -1
  25. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/.gitignore +0 -0
  26. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/AGENTS.md +0 -0
  27. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/CLAUDE.md +0 -0
  28. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/LICENSE +0 -0
  29. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/Makefile +0 -0
  30. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/contrib/icons/hicolor/scalable/status/solstone-error.svg +0 -0
  31. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/contrib/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
  32. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/contrib/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
  33. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/contrib/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
  34. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/scripts/extract_changelog.sh +0 -0
  35. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/scripts/release.sh +0 -0
  36. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/activity.py +0 -0
  37. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/audio_detect.py +0 -0
  38. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/audio_mute.py +0 -0
  39. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/audio_recorder.py +0 -0
  40. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/dbusmenu.py +0 -0
  41. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-error.svg +0 -0
  42. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
  43. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
  44. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
  45. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/install_guard.py +0 -0
  46. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/monitor_positions.py +0 -0
  47. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/observer.py +0 -0
  48. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/recovery.py +0 -0
  49. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/screencast.py +0 -0
  50. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/session_env.py +0 -0
  51. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/sni.py +0 -0
  52. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/solstone-linux.service.in +0 -0
  53. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/src/solstone_linux/streams.py +0 -0
  54. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/__init__.py +0 -0
  55. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_activity.py +0 -0
  56. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_config.py +0 -0
  57. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_dbusmenu.py +0 -0
  58. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_extract_changelog.py +0 -0
  59. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_install_guard.py +0 -0
  60. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_monitor_positions.py +0 -0
  61. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_observer.py +0 -0
  62. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_observer_emits_stream_silent_event.py +0 -0
  63. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_screencast.py +0 -0
  64. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_screencast_stop_filters_silent_streams.py +0 -0
  65. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_session_env.py +0 -0
  66. {solstone_linux-0.3.0 → solstone_linux-0.3.2}/tests/test_streams.py +0 -0
@@ -4,6 +4,20 @@ All notable changes to solstone-linux are documented here.
4
4
  The format is based on Keep a Changelog (https://keepachangelog.com/),
5
5
  and this project adheres to Semantic Versioning.
6
6
 
7
+ ## [0.3.2] - 2026-06-16
8
+
9
+ ### Changed
10
+ - the tray status now tells the truth about sync. it shows "connected" only when this observer has genuinely reached your journal with nothing left to send, and clearly says when it's offline, needs updating, or needs to re-authorize, instead of looking fine while quietly falling behind. the same honest status carries across the tray, `status`, and `doctor`.
11
+
12
+ ### Fixed
13
+ - setup no longer asks for a journal url under any path. if you ran into a lingering "journal url" prompt during setup, that's gone — setup connects to your local journal automatically, and `solstone-linux setup --server-url <url>` still points at a journal you reach directly.
14
+
15
+ ## [0.3.1] - 2026-06-15
16
+
17
+ ### Changed
18
+ - chat notifications now use the journal's current callosum connection path,
19
+ with the observer key still sent in the authorization header.
20
+
7
21
  ## [0.3.0] - 2026-06-14
8
22
 
9
23
  setup is now zero-config: the observer connects to your journal automatically,
@@ -4,7 +4,7 @@ these instructions are for a coding agent and human working together. solstone-l
4
4
 
5
5
  solstone must already be installed and running. if it isn't, start there: https://solstone.app/install
6
6
 
7
- > **most users install solstone-linux from PyPI in three commands** on the machine that will host the observer: `pipx install solstone-linux`, `solstone-linux install-service`, then `solstone-linux setup` (which prompts for your journal URL and auto-registers). if the observer machine can't reach your solstone host, mint a key from there first with `sol observer create <name>` and paste it during setup. the instructions below are for developers building from source or troubleshooting the install.
7
+ > **most users install solstone-linux from PyPI in three commands** on the machine that will host the observer: `pipx install solstone-linux`, `solstone-linux install-service`, then `solstone-linux setup` (which registers against your journal over the local `http://localhost:5015` link — no URL to type). if the observer machine reaches your solstone host directly instead, run `solstone-linux setup --server-url <journal-url>`. the instructions below are for developers building from source or troubleshooting the install.
8
8
 
9
9
  ## before you begin
10
10
 
@@ -14,7 +14,7 @@ check if solstone-linux is already installed and running:
14
14
 
15
15
  ```
16
16
  systemctl --user status solstone-linux
17
- sol remote list
17
+ journal observer list
18
18
  ```
19
19
 
20
20
  if it's already active and connected, you're done.
@@ -24,12 +24,12 @@ if it's already active and connected, you're done.
24
24
  - **system dependencies.** the observer needs PyGObject, GStreamer, and PipeWire bindings from system packages. installing these requires sudo.
25
25
  - **stream name.** this identifies this observer's stream. the machine's hostname is the typical choice.
26
26
 
27
- ### remote sol
27
+ ### journal reached directly (not over the local link)
28
28
 
29
- The observer connects to your solstone journal over HTTPS colocation is optional. For remote-sol setups:
29
+ By default the observer registers over the local `http://localhost:5015` link, so the journal and observer are colocated. If you reach your journal directly over HTTPS instead:
30
30
 
31
31
  - clone anywhere; the `$(sol root)/observers` path in step 2 only applies when sol is installed locally.
32
- - `solstone-linux setup` will prompt for the journal URL (since local `sol remote list` isn't available) and auto-register the observer with your journal via HTTP, persisting the returned key. No manual key handoff is needed if the journal's observer-registration endpoint is reachable.
32
+ - run `solstone-linux setup --server-url <journal-url>` to point at that journal and auto-register the observer over HTTP, persisting the returned key. No manual key handoff is needed if the journal's observer-registration endpoint is reachable.
33
33
  - otherwise, the install sequence below is the same.
34
34
 
35
35
  ## install sequence
@@ -66,7 +66,7 @@ this is the developer/from-source path; most installs should use the `pipx insta
66
66
 
67
67
  `uv` / `pipx`: Fedora packages both (`sudo dnf install uv pipx`); Debian/Ubuntu package `pipx` but not `uv`. the PyPI install flow only needs `pipx` — `uv` is optional and used by the from-source dev workflow in the Makefile.
68
68
 
69
- 2. cloning into `$(sol root)/observers` is only a developer convenience for keeping observer checkouts colocated with a local solstone clone. for remote-sol setups, clone anywhere — the observer runs independently of your journal at runtime:
69
+ 2. cloning into `$(sol root)/observers` is only a developer convenience for keeping observer checkouts colocated with a local solstone clone. for a journal you reach directly, clone anywhere — the observer runs independently of your journal at runtime:
70
70
  ```
71
71
  cd "$(sol root)/observers"
72
72
  git clone https://github.com/solpbc/solstone-linux.git
@@ -75,11 +75,11 @@ this is the developer/from-source path; most installs should use the `pipx insta
75
75
  ```
76
76
  `make install-service` is a smart install-or-upgrade: detects fresh-install vs upgrade via a marker file, runs CI in upgrade mode, guards against cross-repo contamination.
77
77
 
78
- 3. run the interactive setup:
78
+ 3. run setup:
79
79
  ```
80
80
  solstone-linux setup
81
81
  ```
82
- this prompts for the journal URL and registers the observer with your journal.
82
+ this registers the observer against your journal over the local `http://localhost:5015` link — no URL to type. pass `--server-url <journal-url>` for a journal you reach directly.
83
83
 
84
84
  4. verify the service is running:
85
85
  ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solstone-linux
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Standalone Linux desktop observer for solstone
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -53,7 +53,7 @@ solstone-linux install-service
53
53
  solstone-linux setup
54
54
  ```
55
55
 
56
- `setup` prompts for your journal URL and registers the observer for you. If this machine can't reach your solstone host directly, mint a key from there with `sol observer create <name>` and paste it during setup.
56
+ `setup` registers the observer against your journal over the local `http://localhost:5015` link, so there's no URL to type. If this machine reaches your solstone host directly instead, run `solstone-linux setup --server-url <journal-url>`. (Legacy fallback: mint a key on the journal host with `journal observer create <name>` and paste it during setup.)
57
57
 
58
58
  ### Developers building from source
59
59
 
@@ -38,7 +38,7 @@ solstone-linux install-service
38
38
  solstone-linux setup
39
39
  ```
40
40
 
41
- `setup` prompts for your journal URL and registers the observer for you. If this machine can't reach your solstone host directly, mint a key from there with `sol observer create <name>` and paste it during setup.
41
+ `setup` registers the observer against your journal over the local `http://localhost:5015` link, so there's no URL to type. If this machine reaches your solstone host directly instead, run `solstone-linux setup --server-url <journal-url>`. (Legacy fallback: mint a key on the journal host with `journal observer create <name>` and paste it during setup.)
42
42
 
43
43
  ### Developers building from source
44
44
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "solstone-linux"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Standalone Linux desktop observer for solstone"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -3,4 +3,4 @@
3
3
 
4
4
  """Standalone Linux desktop observer for solstone."""
5
5
 
6
- __version__ = "0.3.0"
6
+ __version__ = "0.3.2"
@@ -392,7 +392,7 @@ async def run_chat_bridge(config: Config, stop_event: asyncio.Event) -> None:
392
392
 
393
393
  server_url = config.server_url.rstrip("/")
394
394
  key = config.key
395
- sse_url = f"{server_url}/app/observer/{key}/callosum"
395
+ sse_url = f"{server_url}/app/observer/callosum"
396
396
  pending: OrderedDict[str, PendingRequest] = OrderedDict()
397
397
  opt_in_state = {"value": False}
398
398
  opt_in_task = asyncio.create_task(
@@ -15,18 +15,19 @@ from __future__ import annotations
15
15
  import argparse
16
16
  import asyncio
17
17
  import importlib.resources
18
- import json
19
18
  import logging
20
19
  import os
21
20
  import shutil
22
21
  import socket
23
22
  import subprocess
24
23
  import sys
24
+ import time
25
25
  from pathlib import Path
26
26
 
27
27
  from . import doctor, streams
28
28
  from .config import DEFAULT_SERVER_URL, load_config, save_config
29
29
  from .streams import stream_name
30
+ from .sync_health import derive_health, load_facts
30
31
 
31
32
 
32
33
  def _setup_logging(verbose: bool = False) -> None:
@@ -93,11 +94,12 @@ def cmd_setup(args: argparse.Namespace) -> int:
93
94
 
94
95
  config = load_config()
95
96
 
96
- server_url = getattr(args, "server_url", None) or config.server_url
97
- if not server_url and not non_interactive:
98
- url = input(f"Solstone journal URL [{DEFAULT_SERVER_URL}]: ").strip()
99
- server_url = url or DEFAULT_SERVER_URL
100
- server_url = server_url or DEFAULT_SERVER_URL
97
+ # Resolve the journal URL: an explicit --server-url wins, then any saved
98
+ # URL, otherwise the local link default. Under pure-PL the journal is
99
+ # reached over the localhost link, so no URL needs to be typed.
100
+ server_url = (
101
+ getattr(args, "server_url", None) or config.server_url or DEFAULT_SERVER_URL
102
+ )
101
103
  config.server_url = server_url
102
104
 
103
105
  stream_override = getattr(args, "stream_name", None)
@@ -157,10 +159,11 @@ def _cmd_setup_interactive() -> int:
157
159
 
158
160
  config = load_config()
159
161
 
160
- # Prompt for server URL
161
- default_url = config.server_url or DEFAULT_SERVER_URL
162
- url = input(f"Solstone journal URL [{default_url}]: ").strip()
163
- config.server_url = url or default_url
162
+ # No prompt: default to the local link. Under pure-PL the journal is reached
163
+ # over the localhost link, so no URL needs to be typed; a saved URL (or
164
+ # `solstone-linux setup --server-url <url>`) points at a journal reached
165
+ # directly.
166
+ config.server_url = config.server_url or DEFAULT_SERVER_URL
164
167
 
165
168
  # Derive stream name
166
169
  if not config.stream:
@@ -375,15 +378,9 @@ def cmd_status(args: argparse.Namespace) -> int:
375
378
  else:
376
379
  print(f"Retain: {retention} day(s)")
377
380
 
378
- # Synced days
379
- synced_path = config.state_dir / "synced_days.json"
380
- if synced_path.exists():
381
- try:
382
- with open(synced_path) as f:
383
- synced = json.load(f)
384
- print(f"Synced: {len(synced)} day(s) fully synced")
385
- except (json.JSONDecodeError, OSError):
386
- pass
381
+ facts = load_facts(config.state_dir)
382
+ health = derive_health(facts, time.time(), config.sync_stale_threshold)
383
+ print(health.cli)
387
384
 
388
385
  # Systemd status
389
386
  try:
@@ -24,6 +24,7 @@ DEFAULT_SERVER_URL = "http://localhost:5015"
24
24
  DEFAULT_SEGMENT_INTERVAL = 300
25
25
  DEFAULT_SYNC_RETRY_DELAYS = [5, 30, 120, 300]
26
26
  DEFAULT_SYNC_MAX_RETRIES = 10
27
+ DEFAULT_SYNC_STALE_THRESHOLD = 600
27
28
 
28
29
 
29
30
  @dataclass
@@ -38,6 +39,7 @@ class Config:
38
39
  default_factory=lambda: list(DEFAULT_SYNC_RETRY_DELAYS)
39
40
  )
40
41
  sync_max_retries: int = DEFAULT_SYNC_MAX_RETRIES
42
+ sync_stale_threshold: int = DEFAULT_SYNC_STALE_THRESHOLD
41
43
  cache_retention_days: int = 7
42
44
  chat_bridge_enabled: bool = True
43
45
  capture_framerate: int = 1
@@ -97,6 +99,12 @@ def load_config(base_dir: Path | None = None) -> Config:
97
99
  config.sync_retry_delays = data["sync_retry_delays"]
98
100
  if "sync_max_retries" in data:
99
101
  config.sync_max_retries = data["sync_max_retries"]
102
+ try:
103
+ config.sync_stale_threshold = int(
104
+ data.get("sync_stale_threshold", DEFAULT_SYNC_STALE_THRESHOLD)
105
+ )
106
+ except (TypeError, ValueError):
107
+ config.sync_stale_threshold = DEFAULT_SYNC_STALE_THRESHOLD
100
108
  try:
101
109
  config.cache_retention_days = int(data.get("cache_retention_days", 7))
102
110
  except (TypeError, ValueError):
@@ -120,6 +128,7 @@ def save_config(config: Config) -> None:
120
128
  "segment_interval": config.segment_interval,
121
129
  "sync_retry_delays": config.sync_retry_delays,
122
130
  "sync_max_retries": config.sync_max_retries,
131
+ "sync_stale_threshold": config.sync_stale_threshold,
123
132
  "cache_retention_days": config.cache_retention_days,
124
133
  "chat_bridge_enabled": config.chat_bridge_enabled,
125
134
  "capture_framerate": config.capture_framerate,
@@ -38,13 +38,13 @@ class ObserverService(ServiceInterface):
38
38
  @dbus_property(access=PropertyAccess.READ)
39
39
  def SyncStatus(self) -> "s":
40
40
  if self._observer._sync:
41
- return self._observer._sync.sync_status
42
- return "synced"
41
+ return self._observer._sync.health.state.value
42
+ return "unknown"
43
43
 
44
44
  @dbus_property(access=PropertyAccess.READ)
45
45
  def SyncProgress(self) -> "s":
46
46
  if self._observer._sync:
47
- return self._observer._sync.sync_progress
47
+ return self._observer._sync.progress
48
48
  return ""
49
49
 
50
50
  @dbus_property(access=PropertyAccess.READ)
@@ -122,17 +122,12 @@ class ObserverService(ServiceInterface):
122
122
  except OSError:
123
123
  pass
124
124
 
125
- synced_days = 0
126
- if self._observer._sync:
127
- synced_days = len(self._observer._sync._synced_days)
128
-
129
125
  total_size_mb = int(total_size / (1024 * 1024))
130
126
  uptime_seconds = int(time.monotonic() - self._observer._start_mono)
131
127
 
132
128
  return {
133
129
  "captures_today": Variant("i", captures_today),
134
130
  "total_size_mb": Variant("i", total_size_mb),
135
- "synced_days": Variant("i", synced_days),
136
131
  "uptime_seconds": Variant("i", uptime_seconds),
137
132
  }
138
133
 
@@ -13,8 +13,12 @@ import os
13
13
  import shutil
14
14
  import subprocess
15
15
  import sys
16
+ import time
16
17
  from typing import Callable, NamedTuple
17
18
 
19
+ from .config import load_config
20
+ from .sync_health import derive_health, load_facts
21
+
18
22
  CheckResult = NamedTuple(
19
23
  "CheckResult",
20
24
  [("name", str), ("severity", str), ("detail", str)],
@@ -286,6 +290,13 @@ def check_appindicator_ext() -> CheckResult:
286
290
  )
287
291
 
288
292
 
293
+ def check_sync_health() -> CheckResult:
294
+ config = load_config()
295
+ facts = load_facts(config.state_dir)
296
+ health = derive_health(facts, time.time(), config.sync_stale_threshold)
297
+ return CheckResult("sync health", health.doctor_severity, health.doctor_detail)
298
+
299
+
289
300
  def run_doctor() -> int:
290
301
  checks: list[tuple[str, Callable[[], CheckResult]]] = [
291
302
  ("python version", check_python_version),
@@ -297,6 +308,7 @@ def run_doctor() -> int:
297
308
  ("xdg-desktop-portal", lambda: asyncio.run(check_portal())),
298
309
  ("x11 capture", check_x11_capture),
299
310
  ("systemd --user", check_user_systemd),
311
+ ("sync health", check_sync_health),
300
312
  ("pipx", check_pipx),
301
313
  ("appindicator ext (soft)", check_appindicator_ext),
302
314
  ]