solstone-linux 0.4.1__tar.gz → 0.4.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 (86) hide show
  1. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/AGENTS.md +2 -0
  2. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/CHANGELOG.md +11 -0
  3. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/INSTALL.md +12 -8
  4. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/PKG-INFO +8 -2
  5. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/README.md +7 -1
  6. solstone_linux-0.4.2/contrib/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
  7. solstone_linux-0.4.2/contrib/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
  8. solstone_linux-0.4.2/contrib/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
  9. solstone_linux-0.4.2/contrib/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
  10. solstone_linux-0.4.2/contrib/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
  11. solstone_linux-0.4.2/contrib/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
  12. solstone_linux-0.4.2/contrib/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
  13. solstone_linux-0.4.2/contrib/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
  14. solstone_linux-0.4.2/contrib/icons/hicolor/scalable/apps/solstone-observer.svg +18 -0
  15. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/pyproject.toml +1 -1
  16. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/__init__.py +1 -1
  17. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/cli.py +14 -0
  18. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
  19. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
  20. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
  21. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
  22. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
  23. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
  24. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
  25. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
  26. solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/scalable/apps/solstone-observer.svg +18 -0
  27. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/observer.py +23 -11
  28. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/sync.py +23 -1
  29. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/upload.py +6 -1
  30. solstone_linux-0.4.2/tests/test_observer_health_beacon.py +159 -0
  31. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/.gitignore +0 -0
  32. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/CLAUDE.md +0 -0
  33. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/LICENSE +0 -0
  34. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/Makefile +0 -0
  35. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/contrib/icons/hicolor/scalable/status/solstone-error.svg +0 -0
  36. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/contrib/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
  37. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/contrib/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
  38. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/contrib/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
  39. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/scripts/extract_changelog.sh +0 -0
  40. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/scripts/release.sh +0 -0
  41. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/activity.py +0 -0
  42. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/audio_detect.py +0 -0
  43. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/audio_mute.py +0 -0
  44. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/audio_recorder.py +0 -0
  45. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/chat_bridge.py +0 -0
  46. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/config.py +0 -0
  47. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/dbus_service.py +0 -0
  48. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/dbusmenu.py +0 -0
  49. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/doctor.py +0 -0
  50. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-error.svg +0 -0
  51. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
  52. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
  53. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
  54. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/install_guard.py +0 -0
  55. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/monitor_positions.py +0 -0
  56. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/recovery.py +0 -0
  57. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/screencast.py +0 -0
  58. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/session_env.py +0 -0
  59. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/sni.py +0 -0
  60. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/solstone-linux.service.in +0 -0
  61. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/streams.py +0 -0
  62. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/sync_health.py +0 -0
  63. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/src/solstone_linux/tray.py +0 -0
  64. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/__init__.py +0 -0
  65. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/conftest.py +0 -0
  66. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_activity.py +0 -0
  67. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_chat_bridge.py +0 -0
  68. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_cli.py +0 -0
  69. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_config.py +0 -0
  70. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_dbus_service.py +0 -0
  71. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_dbusmenu.py +0 -0
  72. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_doctor.py +0 -0
  73. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_extract_changelog.py +0 -0
  74. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_install_guard.py +0 -0
  75. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_monitor_positions.py +0 -0
  76. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_observer.py +0 -0
  77. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_observer_emits_stream_silent_event.py +0 -0
  78. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_screencast.py +0 -0
  79. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_screencast_stop_filters_silent_streams.py +0 -0
  80. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_session_env.py +0 -0
  81. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_streams.py +0 -0
  82. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_sync.py +0 -0
  83. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_sync_health.py +0 -0
  84. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_sync_health_surfaces.py +0 -0
  85. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_tray.py +0 -0
  86. {solstone_linux-0.4.1 → solstone_linux-0.4.2}/tests/test_upload.py +0 -0
@@ -47,6 +47,8 @@ State machine has two modes: `screencast` (screen active, recording video) and `
47
47
 
48
48
  The capture loop never makes network calls. It writes locally; sync handles all uploads.
49
49
 
50
+ The `observe/status` heartbeat carries top-level diagnostics-only health-beacon fields for registered observers; these contain no captured content, paths, URLs, tokens, titles, or labels. Missing or legacy beacons are liveness-only and not failures; journal-side ingest rejections (`health.ingest_rejection`) are separate and are not produced by the observer.
51
+
50
52
  ## Commands
51
53
 
52
54
  ```bash
@@ -4,6 +4,17 @@ 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.4.2] - 2026-06-29
8
+
9
+ ### Added
10
+ - this observer now sends your journal a small, diagnostics-only health note alongside its regular check-in, covering its name, version, how long it's been running, and whether syncing is keeping up. it carries none of what it experiences with you: no screen, audio, file paths, or titles. just enough for you to see at a glance that an observer is alive and in good health.
11
+
12
+ ### Changed
13
+ - this observer now carries the sol mark across your desktop, in the app launcher and menus. the tray status icons are unchanged.
14
+
15
+ ### Fixed
16
+ - installing solstone-linux now works cleanly on current debian and ubuntu. the earlier steps could fail while rebuilding the desktop graphics libraries from scratch; the updated install reuses the ones already on your system, so it goes through.
17
+
7
18
  ## [0.4.1] - 2026-06-17
8
19
 
9
20
  ### Added
@@ -4,7 +4,9 @@ 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 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.
7
+ > **most users install solstone-linux from PyPI in three commands** on the machine that will host the observer: `pipx install --system-site-packages 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
+ >
9
+ > the `--system-site-packages` flag is **required**: it lets pipx's virtualenv reuse your distro's system PyGObject, pycairo, and GStreamer bindings (the `python3-gi` / `python3-cairo` packages installed below) instead of rebuilding PyGObject from source. a plain `pipx install solstone-linux` rebuilds PyGObject in an isolated venv, which needs the full GObject-Introspection build toolchain (`libgirepository-2.0-dev`) — and that dev package isn't even available on every supported distro (Debian 12 stable doesn't ship it). `--system-site-packages` is the path that works on every distro and skips the compile entirely.
8
10
 
9
11
  ## before you begin
10
12
 
@@ -34,7 +36,7 @@ By default the observer registers over the local `http://localhost:5015` link, s
34
36
 
35
37
  ## install sequence
36
38
 
37
- this is the developer/from-source path; most installs should use the `pipx install solstone-linux` + `solstone-linux install-service` + `solstone-linux setup` flow described in the callout above.
39
+ this is the developer/from-source path; most installs should use the `pipx install --system-site-packages solstone-linux` + `solstone-linux install-service` + `solstone-linux setup` flow described in the callout above.
38
40
 
39
41
  1. install system dependencies for your distro, including `pipx`. if you need sudo, walk your human through it.
40
42
 
@@ -62,7 +64,7 @@ this is the developer/from-source path; most installs should use the `pipx insta
62
64
  ```
63
65
  note: package names diverge from Fedora — `typelib-1_0-Gtk-4_0` (not `gtk4`), `gstreamer-plugin-pipewire` (singular), and `alsa-devel` (not `alsa-lib-devel`).
64
66
 
65
- the `cairo` headers + `gcc` + Python dev headers in the Fedora/Debian lines above are there because `pycairo` builds from source during install; without them the install fails with a compile error before you'd reach the Troubleshooting section. they're the same fixes listed under Troubleshooting, hoisted up so a fresh install works in one shot.
67
+ with the recommended `pipx install --system-site-packages solstone-linux`, the system `python3-gi` and `python3-cairo` packages above satisfy PyGObject and pycairo, so **neither builds from source**. the `cairo` headers + `gcc` + Python dev headers in the Fedora/Debian lines are kept as a fallback for any other pure-Python dependency that lacks a prebuilt wheel on your platform and they're what an *isolated*-venv install (a plain `pipx install` without `--system-site-packages`) needs to compile pycairo from source. (an isolated-venv install also needs `libgirepository-2.0-dev`; see Troubleshooting.)
66
68
 
67
69
  `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
70
 
@@ -140,11 +142,13 @@ Common install-time errors and their fixes:
140
142
  - arch: `sudo pacman -S pkgconf cairo`
141
143
  - opensuse: `sudo zypper install pkgconf-pkg-config cairo-devel`
142
144
 
143
- - **`girepository-2.0` missing or `pygobject` build failure**
144
- - fedora: `sudo dnf install gobject-introspection-devel`
145
- - debian/ubuntu: `sudo apt install libgirepository1.0-dev`
146
- - arch: `sudo pacman -S gobject-introspection`
147
- - opensuse: `sudo zypper install gobject-introspection-devel`
145
+ - **`girepository-2.0` missing or `pygobject` build failure** — only hit when installing into an *isolated* venv (a plain `pipx install` **without** `--system-site-packages`), which rebuilds PyGObject from PyPI from source. the recommended `pipx install --system-site-packages solstone-linux` uses your system `python3-gi` and skips this build entirely.
146
+ - **first, retry with `--system-site-packages`.** `pipx install --system-site-packages solstone-linux` needs no build toolchain. this is the fix on Debian 12 (stable) and other distros that don't package the `-2.0` dev headers at all.
147
+ - if you must build from source: PyPI's PyGObject (3.50+, Sept 2024 onward) needs the girepository-**2.0** dev headers, not the old 1.0 package:
148
+ - fedora: `sudo dnf install gobject-introspection-devel`
149
+ - debian/ubuntu: `sudo apt install libgirepository-2.0-dev` (Debian 13 / Ubuntu 24.04+; older releases that ship PyGObject < 3.50 use `libgirepository1.0-dev`)
150
+ - arch: `sudo pacman -S gobject-introspection`
151
+ - opensuse: `sudo zypper install gobject-introspection-devel`
148
152
 
149
153
  - **`Python.h: No such file or directory`**
150
154
  - fedora: `sudo dnf install python3-devel`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solstone-linux
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Standalone Linux desktop observer for solstone
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -48,11 +48,13 @@ solstone (the journal) must already be installed and running on the host this ob
48
48
  On the machine that will host the observer:
49
49
 
50
50
  ```bash
51
- pipx install solstone-linux
51
+ pipx install --system-site-packages solstone-linux
52
52
  solstone-linux install-service
53
53
  solstone-linux setup
54
54
  ```
55
55
 
56
+ The `--system-site-packages` flag is required: it lets pipx reuse your distro's system PyGObject/pycairo/GStreamer bindings (the `python3-gi` / `python3-cairo` packages from System Dependencies above) instead of rebuilding PyGObject from source — which needs the GObject-Introspection build toolchain and isn't packaged on every distro. See `INSTALL.md` if a plain `pipx install` failed with a `girepository-2.0` build error.
57
+
56
58
  `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
59
 
58
60
  ### Developers building from source
@@ -85,6 +87,10 @@ solstone-linux run
85
87
  solstone-linux status
86
88
  ```
87
89
 
90
+ Registered observers also include a diagnostics-only status beacon in the
91
+ journal: identity, version, uptime, and sync liveness counts only, with no
92
+ captured or experienced content.
93
+
88
94
  ## License
89
95
 
90
96
  AGPL-3.0-only — Copyright (c) 2026 sol pbc
@@ -33,11 +33,13 @@ solstone (the journal) must already be installed and running on the host this ob
33
33
  On the machine that will host the observer:
34
34
 
35
35
  ```bash
36
- pipx install solstone-linux
36
+ pipx install --system-site-packages solstone-linux
37
37
  solstone-linux install-service
38
38
  solstone-linux setup
39
39
  ```
40
40
 
41
+ The `--system-site-packages` flag is required: it lets pipx reuse your distro's system PyGObject/pycairo/GStreamer bindings (the `python3-gi` / `python3-cairo` packages from System Dependencies above) instead of rebuilding PyGObject from source — which needs the GObject-Introspection build toolchain and isn't packaged on every distro. See `INSTALL.md` if a plain `pipx install` failed with a `girepository-2.0` build error.
42
+
41
43
  `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
44
 
43
45
  ### Developers building from source
@@ -70,6 +72,10 @@ solstone-linux run
70
72
  solstone-linux status
71
73
  ```
72
74
 
75
+ Registered observers also include a diagnostics-only status beacon in the
76
+ journal: identity, version, uptime, and sync liveness counts only, with no
77
+ captured or experienced content.
78
+
73
79
  ## License
74
80
 
75
81
  AGPL-3.0-only — Copyright (c) 2026 sol pbc
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="solstone">
2
+ <title>solstone</title>
3
+ <!-- canonical sol app icon (transparent) — UNIFIED WORDMARK direction (locked 2026-06-25, founder-approved).
4
+ mark: the sol wordmark (sun ring + "sol" Comfortaa Bold) from sol-wordmark.svg, at full extents.
5
+ ground: NONE (transparent), open center — the background shows through the ring.
6
+ color: #B06A1A accessible orange (ring + "sol" glyph) — bumped from native #E8923A so it holds on
7
+ light surfaces too; clears 3:1 on light, and on dark the #F5C740 gold rays carry brightness
8
+ while #B06A1A still reads (~4:1). #F5C740 gold rays.
9
+ geometry: identical mark + scale (1.185x, 35.9529) to sol-app-icon-cream.svg; only the ground differs.
10
+ use: Windows + Linux (and any controlled/dark surface — docs, dark UI, marketing on dark).
11
+ decision: records/decisions/260625-cmo-sol-app-icon-unified-wordmark.md
12
+ RENDERING RULE: render from this SVG at every target size; never downsample. -->
13
+ <g transform="translate(512,512) scale(35.9529) translate(-16,-16)">
14
+ <path fill="#F5C740" d="M16.0 2.5 L18.6 7.3 A9.1 9.1 0 0 0 13.4 7.3 Z M23.9 5.1 L23.2 10.5 A9.1 9.1 0 0 0 19.0 7.4 Z M28.8 11.8 L25.1 15.8 A9.1 9.1 0 0 0 23.5 10.9 Z M28.8 20.2 L23.5 21.1 A9.1 9.1 0 0 0 25.1 16.2 Z M23.9 26.9 L19.0 24.6 A9.1 9.1 0 0 0 23.2 21.5 Z M16.0 29.5 L13.4 24.7 A9.1 9.1 0 0 0 18.6 24.7 Z M8.1 26.9 L8.8 21.5 A9.1 9.1 0 0 0 13.0 24.6 Z M3.2 20.2 L6.9 16.2 A9.1 9.1 0 0 0 8.5 21.1 Z M3.2 11.8 L8.5 10.9 A9.1 9.1 0 0 0 6.9 15.8 Z M8.1 5.1 L13.0 7.4 A9.1 9.1 0 0 0 8.8 10.5 Z"/>
15
+ <circle cx="16" cy="16" r="8.0" fill="none" stroke="#B06A1A" stroke-width="1.2"/>
16
+ <path fill="#B06A1A" fill-rule="evenodd" d="M12.079 18.795C13.489 18.795 14.229 18.065 14.229 17.155C14.229 16.365 13.729 15.835 12.229 15.535C11.149 15.315 10.939 15.095 10.939 14.725C10.939 14.345 11.399 14.135 11.989 14.135C12.499 14.135 12.859 14.235 13.199 14.555C13.399 14.745 13.729 14.815 13.949 14.665C14.159 14.505 14.169 14.255 13.989 14.035C13.589 13.545 12.889 13.245 12.009 13.245C10.989 13.245 9.959 13.735 9.959 14.755C9.959 15.525 10.529 16.075 11.879 16.335C12.919 16.525 13.249 16.815 13.239 17.215C13.229 17.615 12.809 17.895 12.039 17.895C11.429 17.895 10.889 17.625 10.659 17.375C10.469 17.175 10.189 17.125 9.929 17.335C9.699 17.515 9.659 17.825 9.859 18.035C10.299 18.475 11.149 18.795 12.079 18.795Z M16.999 18.795C18.609 18.795 19.749 17.645 19.749 16.025C19.739 14.395 18.599 13.245 16.999 13.245C15.379 13.245 14.239 14.395 14.239 16.025C14.239 17.645 15.379 18.795 16.999 18.795ZM16.999 17.895C15.959 17.895 15.219 17.125 15.219 16.025C15.219 14.925 15.959 14.145 16.999 14.145C18.039 14.145 18.769 14.925 18.769 16.025C18.769 17.125 18.039 17.895 16.999 17.895Z M21.569 18.755H21.589C21.989 18.755 22.269 18.545 22.269 18.255C22.269 17.965 22.079 17.755 21.819 17.755H21.569C21.279 17.755 21.069 17.405 21.069 16.905V11.445C21.069 11.155 20.859 10.945 20.569 10.945C20.279 10.945 20.069 11.155 20.069 11.445V16.905C20.069 17.985 20.689 18.755 21.569 18.755Z"/>
17
+ </g>
18
+ </svg>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "solstone-linux"
3
- version = "0.4.1"
3
+ version = "0.4.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.4.1"
6
+ __version__ = "0.4.2"
@@ -322,6 +322,7 @@ def cmd_install_service(args: argparse.Namespace) -> int:
322
322
  "Exec=/bin/sh -c 'systemctl --user import-environment"
323
323
  " DISPLAY XAUTHORITY XDG_SESSION_TYPE 2>/dev/null;"
324
324
  " systemctl --user start solstone-linux.service'\n"
325
+ "Icon=solstone-observer\n"
325
326
  "StartupNotify=false\n"
326
327
  "X-GNOME-Autostart-enabled=true\n"
327
328
  "Hidden=false\n"
@@ -365,6 +366,19 @@ def cmd_install_service(args: argparse.Namespace) -> int:
365
366
  shutil.copy2(svg, status_dir / svg.name)
366
367
  print(f"Installed {status_dir / svg.name}")
367
368
 
369
+ # Application icon — the unified sol app icon (wordmark, transparent
370
+ # ground), installed into the hicolor *apps* context under the name
371
+ # "solstone-observer" (matches the SNI app_id and the .desktop Icon=).
372
+ # Mirrors every freedesktop size dir the repo ships (PNG ladder +
373
+ # scalable SVG). Distinct from the status/ tray icons above.
374
+ for ctx_dir in sorted(icon_source.glob("*/apps")):
375
+ dest_ctx = icon_dest / ctx_dir.parent.name / "apps"
376
+ dest_ctx.mkdir(parents=True, exist_ok=True)
377
+ for asset in sorted(ctx_dir.iterdir()):
378
+ if asset.suffix in (".png", ".svg"):
379
+ shutil.copy2(asset, dest_ctx / asset.name)
380
+ print(f"Installed {dest_ctx / asset.name}")
381
+
368
382
  # Self-heal: earlier installs copied a solstone index.theme into this
369
383
  # shared hicolor dir. Because the user icon dir out-ranks
370
384
  # /usr/share/icons, that file shadowed the system hicolor index (which
@@ -0,0 +1,18 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="solstone">
2
+ <title>solstone</title>
3
+ <!-- canonical sol app icon (transparent) — UNIFIED WORDMARK direction (locked 2026-06-25, founder-approved).
4
+ mark: the sol wordmark (sun ring + "sol" Comfortaa Bold) from sol-wordmark.svg, at full extents.
5
+ ground: NONE (transparent), open center — the background shows through the ring.
6
+ color: #B06A1A accessible orange (ring + "sol" glyph) — bumped from native #E8923A so it holds on
7
+ light surfaces too; clears 3:1 on light, and on dark the #F5C740 gold rays carry brightness
8
+ while #B06A1A still reads (~4:1). #F5C740 gold rays.
9
+ geometry: identical mark + scale (1.185x, 35.9529) to sol-app-icon-cream.svg; only the ground differs.
10
+ use: Windows + Linux (and any controlled/dark surface — docs, dark UI, marketing on dark).
11
+ decision: records/decisions/260625-cmo-sol-app-icon-unified-wordmark.md
12
+ RENDERING RULE: render from this SVG at every target size; never downsample. -->
13
+ <g transform="translate(512,512) scale(35.9529) translate(-16,-16)">
14
+ <path fill="#F5C740" d="M16.0 2.5 L18.6 7.3 A9.1 9.1 0 0 0 13.4 7.3 Z M23.9 5.1 L23.2 10.5 A9.1 9.1 0 0 0 19.0 7.4 Z M28.8 11.8 L25.1 15.8 A9.1 9.1 0 0 0 23.5 10.9 Z M28.8 20.2 L23.5 21.1 A9.1 9.1 0 0 0 25.1 16.2 Z M23.9 26.9 L19.0 24.6 A9.1 9.1 0 0 0 23.2 21.5 Z M16.0 29.5 L13.4 24.7 A9.1 9.1 0 0 0 18.6 24.7 Z M8.1 26.9 L8.8 21.5 A9.1 9.1 0 0 0 13.0 24.6 Z M3.2 20.2 L6.9 16.2 A9.1 9.1 0 0 0 8.5 21.1 Z M3.2 11.8 L8.5 10.9 A9.1 9.1 0 0 0 6.9 15.8 Z M8.1 5.1 L13.0 7.4 A9.1 9.1 0 0 0 8.8 10.5 Z"/>
15
+ <circle cx="16" cy="16" r="8.0" fill="none" stroke="#B06A1A" stroke-width="1.2"/>
16
+ <path fill="#B06A1A" fill-rule="evenodd" d="M12.079 18.795C13.489 18.795 14.229 18.065 14.229 17.155C14.229 16.365 13.729 15.835 12.229 15.535C11.149 15.315 10.939 15.095 10.939 14.725C10.939 14.345 11.399 14.135 11.989 14.135C12.499 14.135 12.859 14.235 13.199 14.555C13.399 14.745 13.729 14.815 13.949 14.665C14.159 14.505 14.169 14.255 13.989 14.035C13.589 13.545 12.889 13.245 12.009 13.245C10.989 13.245 9.959 13.735 9.959 14.755C9.959 15.525 10.529 16.075 11.879 16.335C12.919 16.525 13.249 16.815 13.239 17.215C13.229 17.615 12.809 17.895 12.039 17.895C11.429 17.895 10.889 17.625 10.659 17.375C10.469 17.175 10.189 17.125 9.929 17.335C9.699 17.515 9.659 17.825 9.859 18.035C10.299 18.475 11.149 18.795 12.079 18.795Z M16.999 18.795C18.609 18.795 19.749 17.645 19.749 16.025C19.739 14.395 18.599 13.245 16.999 13.245C15.379 13.245 14.239 14.395 14.239 16.025C14.239 17.645 15.379 18.795 16.999 18.795ZM16.999 17.895C15.959 17.895 15.219 17.125 15.219 16.025C15.219 14.925 15.959 14.145 16.999 14.145C18.039 14.145 18.769 14.925 18.769 16.025C18.769 17.125 18.039 17.895 16.999 17.895Z M21.569 18.755H21.589C21.989 18.755 22.269 18.545 22.269 18.255C22.269 17.965 22.079 17.755 21.819 17.755H21.569C21.279 17.755 21.069 17.405 21.069 16.905V11.445C21.069 11.155 20.859 10.945 20.569 10.945C20.279 10.945 20.069 11.155 20.069 11.445V16.905C20.069 17.985 20.689 18.755 21.569 18.755Z"/>
17
+ </g>
18
+ </svg>
@@ -32,6 +32,7 @@ import numpy as np
32
32
  from dbus_next.aio import MessageBus
33
33
  from dbus_next.constants import BusType
34
34
 
35
+ from . import __version__
35
36
  from .activity import (
36
37
  is_power_save_active,
37
38
  is_screen_locked,
@@ -44,7 +45,7 @@ from .config import Config
44
45
  from .recovery import write_segment_metadata
45
46
  from .screencast import Screencaster, SilentStream, StreamInfo, X11Screencaster
46
47
  from .sync import SyncService
47
- from .upload import UploadClient
48
+ from .upload import STREAM_TYPE, UploadClient
48
49
 
49
50
  logger = logging.getLogger(__name__)
50
51
 
@@ -475,16 +476,27 @@ class Observer:
475
476
  "power_save": self.cached_power_save,
476
477
  }
477
478
 
478
- self._client.relay_event(
479
- "observe",
480
- "status",
481
- mode=self.current_mode,
482
- screencast=screencast_info,
483
- audio=audio_info,
484
- activity=activity_info,
485
- host=HOST,
486
- platform=PLATFORM,
487
- )
479
+ status_fields = {
480
+ "mode": self.current_mode,
481
+ "screencast": screencast_info,
482
+ "audio": audio_info,
483
+ "activity": activity_info,
484
+ "host": HOST,
485
+ "platform": PLATFORM,
486
+ }
487
+ if self._client.is_registered and self.stream:
488
+ status_fields.update(
489
+ {
490
+ "name": self.stream,
491
+ "stream_type": STREAM_TYPE,
492
+ "version": __version__,
493
+ "uptime": elapsed,
494
+ }
495
+ )
496
+ if self._sync is not None:
497
+ status_fields.update(self._sync.health_beacon_fields())
498
+
499
+ self._client.relay_event("observe", "status", **status_fields)
488
500
 
489
501
  def _refresh_tray(self):
490
502
  """Refresh the SNI tray UI. Safe when tray is unavailable; disables on failure."""
@@ -25,7 +25,7 @@ import shutil
25
25
  import time
26
26
  from datetime import datetime, timedelta
27
27
  from pathlib import Path
28
- from typing import Callable
28
+ from typing import Any, Callable
29
29
 
30
30
  from .config import Config
31
31
  from .sync_health import (
@@ -98,6 +98,28 @@ class SyncService:
98
98
  def progress(self) -> str:
99
99
  return self._facts.progress
100
100
 
101
+ def health_beacon_fields(self) -> dict[str, Any]:
102
+ """Diagnostics-only sync fields: counts, epoch seconds, and error class."""
103
+ last_successful_sync = self._facts.last_successful_sync
104
+ last_error_class = self._facts.last_error_class
105
+ last_error_code = self._facts.last_error_code
106
+
107
+ if last_error_class is None:
108
+ last_error_reason = None
109
+ elif last_error_code is not None:
110
+ last_error_reason = f"{last_error_class.value}:{last_error_code}"
111
+ else:
112
+ last_error_reason = last_error_class.value
113
+
114
+ return {
115
+ "last_successful_sync": int(last_successful_sync)
116
+ if last_successful_sync is not None
117
+ else None,
118
+ "pending_queue_depth": self._facts.pending_confirmed,
119
+ "recent_error_count": min(99, max(0, self._consecutive_failures)),
120
+ "last_error_reason": last_error_reason,
121
+ }
122
+
101
123
  def _synced_days_path(self) -> Path:
102
124
  return self._config.state_dir / "synced_days.json"
103
125
 
@@ -30,6 +30,7 @@ logger = logging.getLogger(__name__)
30
30
 
31
31
  UPLOAD_TIMEOUT = 300
32
32
  EVENT_TIMEOUT = 30
33
+ STREAM_TYPE = "desktop"
33
34
 
34
35
 
35
36
  def _auth_headers(key: str) -> dict[str, str]:
@@ -65,6 +66,10 @@ class UploadClient:
65
66
  def is_revoked(self) -> bool:
66
67
  return self._revoked
67
68
 
69
+ @property
70
+ def is_registered(self) -> bool:
71
+ return bool(self._key)
72
+
68
73
  def _persist_registration(self, config: Config, key: str, stream: str) -> None:
69
74
  """Persist the server-issued handle and locked stream back to config."""
70
75
  from .config import save_config
@@ -86,7 +91,7 @@ class UploadClient:
86
91
  descriptor: dict[str, Any] = {
87
92
  "platform": platform.system().lower(),
88
93
  "hostname": socket.gethostname(),
89
- "stream_type": "desktop",
94
+ "stream_type": STREAM_TYPE,
90
95
  "version": __version__,
91
96
  }
92
97
  if self._stream:
@@ -0,0 +1,159 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-only
2
+ # Copyright (c) 2026 sol pbc
3
+
4
+ import time
5
+ from unittest.mock import MagicMock
6
+
7
+ from solstone_linux import __version__
8
+ from solstone_linux.config import Config
9
+ from solstone_linux.observer import MODE_SCREENCAST, Observer
10
+ from solstone_linux.screencast import StreamInfo
11
+ from solstone_linux.sync import SyncService
12
+ from solstone_linux.sync_health import ErrorType
13
+ from solstone_linux.upload import STREAM_TYPE
14
+
15
+ FIXED_EPOCH = 1_798_888_123.5
16
+ HEALTH_KEYS = {
17
+ "name",
18
+ "stream_type",
19
+ "version",
20
+ "uptime",
21
+ "last_successful_sync",
22
+ "pending_queue_depth",
23
+ "recent_error_count",
24
+ "last_error_reason",
25
+ }
26
+ BASE_STATUS_KEYS = {
27
+ "mode",
28
+ "screencast",
29
+ "audio",
30
+ "activity",
31
+ "host",
32
+ "platform",
33
+ }
34
+
35
+
36
+ def _observer(tmp_path, registered: bool = True) -> Observer:
37
+ config = Config(base_dir=tmp_path)
38
+ observer = Observer(config)
39
+ observer._client = MagicMock()
40
+ observer._client.is_registered = registered
41
+ observer.stream = "desk-host"
42
+ observer.start_at_mono = time.monotonic() - 12
43
+ observer._sync = SyncService(config, MagicMock(), now=lambda: FIXED_EPOCH)
44
+ return observer
45
+
46
+
47
+ def _status_kwargs(observer: Observer) -> dict:
48
+ observer.emit_status()
49
+ args, kwargs = observer._client.relay_event.call_args
50
+ assert args == ("observe", "status")
51
+ return kwargs
52
+
53
+
54
+ def test_registered_first_emit_includes_all_health_fields_top_level(tmp_path):
55
+ observer = _observer(tmp_path)
56
+
57
+ kwargs = _status_kwargs(observer)
58
+
59
+ assert HEALTH_KEYS.issubset(kwargs)
60
+ assert "health" not in kwargs
61
+ assert kwargs["name"] == "desk-host"
62
+ assert kwargs["stream_type"] == STREAM_TYPE
63
+ assert kwargs["version"] == __version__
64
+ assert isinstance(kwargs["uptime"], int)
65
+ assert kwargs["uptime"] >= 0
66
+ assert kwargs["last_successful_sync"] is None
67
+ assert kwargs["pending_queue_depth"] is None
68
+ assert kwargs["recent_error_count"] == 0
69
+ assert kwargs["last_error_reason"] is None
70
+
71
+
72
+ def test_periodic_reemit_carries_same_health_fields(tmp_path):
73
+ observer = _observer(tmp_path)
74
+
75
+ first = _status_kwargs(observer)
76
+ second = _status_kwargs(observer)
77
+
78
+ assert HEALTH_KEYS.issubset(first)
79
+ assert HEALTH_KEYS.issubset(second)
80
+
81
+
82
+ def test_health_fields_exclude_captured_content_and_extra_health_keys(tmp_path):
83
+ observer = _observer(tmp_path)
84
+ observer.current_mode = MODE_SCREENCAST
85
+ observer.current_streams = [
86
+ StreamInfo(
87
+ node_id=42,
88
+ position="left",
89
+ connector="HDMI-SECRET",
90
+ x=0,
91
+ y=0,
92
+ width=1920,
93
+ height=1080,
94
+ file_path="/captured/private/window-title-meeting.webm",
95
+ )
96
+ ]
97
+ observer.threshold_hits = 4
98
+ observer.cached_is_active = True
99
+ observer.cached_screen_locked = False
100
+ observer.cached_is_muted = True
101
+ observer.cached_power_save = False
102
+
103
+ kwargs = _status_kwargs(observer)
104
+
105
+ assert set(kwargs) - BASE_STATUS_KEYS == HEALTH_KEYS
106
+ forbidden = (
107
+ "/captured/private",
108
+ "window-title",
109
+ "meeting",
110
+ "HDMI-SECRET",
111
+ "threshold_hits",
112
+ "sink_muted",
113
+ )
114
+ health_values = [kwargs[key] for key in HEALTH_KEYS]
115
+ for value in health_values:
116
+ assert not any(token in str(value) for token in forbidden)
117
+
118
+
119
+ def test_successful_no_work_sync_reflected_in_health_beacon(tmp_path):
120
+ observer = _observer(tmp_path)
121
+ observer._sync._commit_pass_result(True)
122
+
123
+ kwargs = _status_kwargs(observer)
124
+
125
+ assert kwargs["last_successful_sync"] == int(FIXED_EPOCH)
126
+ assert kwargs["pending_queue_depth"] == 0
127
+ assert kwargs["recent_error_count"] == 0
128
+ assert kwargs["last_error_reason"] is None
129
+
130
+
131
+ def test_failed_delivery_return_false_is_nonfatal_for_status_emit(tmp_path):
132
+ observer = _observer(tmp_path)
133
+ observer._client.relay_event.return_value = False
134
+
135
+ observer.emit_status()
136
+ observer.emit_status()
137
+
138
+ assert observer._client.relay_event.call_count == 2
139
+
140
+
141
+ def test_unregistered_observer_emits_base_status_without_health_fields(tmp_path):
142
+ observer = _observer(tmp_path, registered=False)
143
+
144
+ kwargs = _status_kwargs(observer)
145
+
146
+ assert BASE_STATUS_KEYS.issubset(kwargs)
147
+ assert HEALTH_KEYS.isdisjoint(kwargs)
148
+
149
+
150
+ def test_failure_count_clamps_and_last_error_reason_is_safe(tmp_path):
151
+ config = Config(base_dir=tmp_path)
152
+ sync = SyncService(config, MagicMock(), now=lambda: FIXED_EPOCH)
153
+
154
+ for _ in range(150):
155
+ sync._record_failure(ErrorType.TRANSIENT, 503)
156
+
157
+ fields = sync.health_beacon_fields()
158
+ assert fields["recent_error_count"] == 99
159
+ assert fields["last_error_reason"] == "transient:503"
File without changes
File without changes
File without changes