server-motd 0.1.0__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 (134) hide show
  1. server_motd-0.1.0/.ansible-lint +5 -0
  2. server_motd-0.1.0/.gitattributes +3 -0
  3. server_motd-0.1.0/.github/workflows/ci.yml +149 -0
  4. server_motd-0.1.0/.github/workflows/release.yml +152 -0
  5. server_motd-0.1.0/.gitignore +43 -0
  6. server_motd-0.1.0/CHANGELOG.md +24 -0
  7. server_motd-0.1.0/CLAUDE.md +13 -0
  8. server_motd-0.1.0/GLOSSARY.md +45 -0
  9. server_motd-0.1.0/LICENSE +674 -0
  10. server_motd-0.1.0/PKG-INFO +756 -0
  11. server_motd-0.1.0/README.md +65 -0
  12. server_motd-0.1.0/ansible/ci-absent.yml +7 -0
  13. server_motd-0.1.0/ansible/ci-present.yml +12 -0
  14. server_motd-0.1.0/ansible/roles/server_motd/defaults/main.yml +26 -0
  15. server_motd-0.1.0/ansible/roles/server_motd/files/requirements.lock +5 -0
  16. server_motd-0.1.0/ansible/roles/server_motd/filter_plugins/server_motd.py +73 -0
  17. server_motd-0.1.0/ansible/roles/server_motd/meta/argument_specs.yml +48 -0
  18. server_motd-0.1.0/ansible/roles/server_motd/meta/main.yml +13 -0
  19. server_motd-0.1.0/ansible/roles/server_motd/tasks/absent.yml +45 -0
  20. server_motd-0.1.0/ansible/roles/server_motd/tasks/main.yml +8 -0
  21. server_motd-0.1.0/ansible/roles/server_motd/tasks/present.yml +108 -0
  22. server_motd-0.1.0/ansible/roles/server_motd/templates/50-server-motd.j2 +27 -0
  23. server_motd-0.1.0/docs/adr/0001-pure-python-single-package.md +3 -0
  24. server_motd-0.1.0/docs/adr/0002-break-upstream-compatibility.md +3 -0
  25. server_motd-0.1.0/docs/agents/domain.md +51 -0
  26. server_motd-0.1.0/docs/agents/issue-tracker.md +59 -0
  27. server_motd-0.1.0/docs/agents/triage-labels.md +7 -0
  28. server_motd-0.1.0/docs/configuration.md +77 -0
  29. server_motd-0.1.0/docs/example-config.toml +96 -0
  30. server_motd-0.1.0/docs/install-debian.md +48 -0
  31. server_motd-0.1.0/docs/install-windows.md +30 -0
  32. server_motd-0.1.0/docs/migrating-from-v1.md +51 -0
  33. server_motd-0.1.0/docs/releasing.md +47 -0
  34. server_motd-0.1.0/docs/research/debian-motd-mechanics.md +141 -0
  35. server_motd-0.1.0/docs/research/download-clients-api.md +192 -0
  36. server_motd-0.1.0/docs/research/servarr-api.md +79 -0
  37. server_motd-0.1.0/docs/research/tautulli-api.md +105 -0
  38. server_motd-0.1.0/docs/research/windows-profile-mechanics.md +121 -0
  39. server_motd-0.1.0/docs/secrets.md +9 -0
  40. server_motd-0.1.0/docs/troubleshooting.md +41 -0
  41. server_motd-0.1.0/pyproject.toml +37 -0
  42. server_motd-0.1.0/scripts/record-resolution.sh +39 -0
  43. server_motd-0.1.0/src/server_motd/__init__.py +1 -0
  44. server_motd-0.1.0/src/server_motd/__main__.py +5 -0
  45. server_motd-0.1.0/src/server_motd/cli.py +126 -0
  46. server_motd-0.1.0/src/server_motd/config.py +155 -0
  47. server_motd-0.1.0/src/server_motd/model.py +38 -0
  48. server_motd-0.1.0/src/server_motd/modules/__init__.py +54 -0
  49. server_motd-0.1.0/src/server_motd/modules/_common.py +42 -0
  50. server_motd-0.1.0/src/server_motd/modules/_http.py +78 -0
  51. server_motd-0.1.0/src/server_motd/modules/cpu.py +31 -0
  52. server_motd-0.1.0/src/server_motd/modules/disks.py +32 -0
  53. server_motd-0.1.0/src/server_motd/modules/ips.py +31 -0
  54. server_motd-0.1.0/src/server_motd/modules/load.py +30 -0
  55. server_motd-0.1.0/src/server_motd/modules/logins.py +29 -0
  56. server_motd-0.1.0/src/server_motd/modules/memory.py +38 -0
  57. server_motd-0.1.0/src/server_motd/modules/processes.py +21 -0
  58. server_motd-0.1.0/src/server_motd/modules/qbittorrent.py +82 -0
  59. server_motd-0.1.0/src/server_motd/modules/rtorrent.py +112 -0
  60. server_motd-0.1.0/src/server_motd/modules/sabnzbd.py +59 -0
  61. server_motd-0.1.0/src/server_motd/modules/servarr.py +103 -0
  62. server_motd-0.1.0/src/server_motd/modules/tautulli.py +85 -0
  63. server_motd-0.1.0/src/server_motd/modules/updates.py +56 -0
  64. server_motd-0.1.0/src/server_motd/modules/uptime.py +38 -0
  65. server_motd-0.1.0/src/server_motd/profile.py +167 -0
  66. server_motd-0.1.0/src/server_motd/py.typed +0 -0
  67. server_motd-0.1.0/src/server_motd/render.py +167 -0
  68. server_motd-0.1.0/src/server_motd/runner.py +56 -0
  69. server_motd-0.1.0/src/server_motd/terminal.py +35 -0
  70. server_motd-0.1.0/tests/__init__.py +0 -0
  71. server_motd-0.1.0/tests/conftest.py +74 -0
  72. server_motd-0.1.0/tests/fakescgi.py +128 -0
  73. server_motd-0.1.0/tests/fakeserver.py +184 -0
  74. server_motd-0.1.0/tests/fixtures/lidarr/calendar.json +1 -0
  75. server_motd-0.1.0/tests/fixtures/lidarr/health_ok.json +1 -0
  76. server_motd-0.1.0/tests/fixtures/lidarr/queue_status.json +1 -0
  77. server_motd-0.1.0/tests/fixtures/lidarr/wanted_missing.json +1 -0
  78. server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_4x.json +13 -0
  79. server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_50.json +13 -0
  80. server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_52.json +13 -0
  81. server_motd-0.1.0/tests/fixtures/qbittorrent/maindata_empty.json +2 -0
  82. server_motd-0.1.0/tests/fixtures/radarr/calendar.json +1 -0
  83. server_motd-0.1.0/tests/fixtures/radarr/health_error.json +1 -0
  84. server_motd-0.1.0/tests/fixtures/radarr/queue_status.json +1 -0
  85. server_motd-0.1.0/tests/fixtures/radarr/wanted_missing.json +1 -0
  86. server_motd-0.1.0/tests/fixtures/rtorrent/multicall.json +7 -0
  87. server_motd-0.1.0/tests/fixtures/sabnzbd/error.json +1 -0
  88. server_motd-0.1.0/tests/fixtures/sabnzbd/queue_active.json +1 -0
  89. server_motd-0.1.0/tests/fixtures/sabnzbd/queue_idle.json +1 -0
  90. server_motd-0.1.0/tests/fixtures/sabnzbd/server_stats.json +1 -0
  91. server_motd-0.1.0/tests/fixtures/sonarr/calendar.json +1 -0
  92. server_motd-0.1.0/tests/fixtures/sonarr/health_ok.json +1 -0
  93. server_motd-0.1.0/tests/fixtures/sonarr/health_warn.json +1 -0
  94. server_motd-0.1.0/tests/fixtures/sonarr/queue_status.json +1 -0
  95. server_motd-0.1.0/tests/fixtures/sonarr/wanted_missing.json +1 -0
  96. server_motd-0.1.0/tests/fixtures/tautulli/activity_empty.json +1 -0
  97. server_motd-0.1.0/tests/fixtures/tautulli/activity_none.json +1 -0
  98. server_motd-0.1.0/tests/fixtures/tautulli/activity_streams.json +1 -0
  99. server_motd-0.1.0/tests/fixtures/tautulli/server_status_down.json +1 -0
  100. server_motd-0.1.0/tests/fixtures/tautulli/server_status_up.json +1 -0
  101. server_motd-0.1.0/tests/golden/.gitkeep +0 -0
  102. server_motd-0.1.0/tests/golden/cpu.txt +6 -0
  103. server_motd-0.1.0/tests/golden/disks.txt +7 -0
  104. server_motd-0.1.0/tests/golden/ips.txt +7 -0
  105. server_motd-0.1.0/tests/golden/layout-110.txt +17 -0
  106. server_motd-0.1.0/tests/golden/layout-50.txt +24 -0
  107. server_motd-0.1.0/tests/golden/layout-80.txt +17 -0
  108. server_motd-0.1.0/tests/golden/layout-ascii.txt +17 -0
  109. server_motd-0.1.0/tests/golden/layout-figlet.txt +18 -0
  110. server_motd-0.1.0/tests/golden/load.txt +6 -0
  111. server_motd-0.1.0/tests/golden/logins.txt +7 -0
  112. server_motd-0.1.0/tests/golden/memory.txt +7 -0
  113. server_motd-0.1.0/tests/golden/processes.txt +6 -0
  114. server_motd-0.1.0/tests/golden/sonarr.txt +9 -0
  115. server_motd-0.1.0/tests/golden/updates_none.txt +7 -0
  116. server_motd-0.1.0/tests/golden/updates_pending.txt +8 -0
  117. server_motd-0.1.0/tests/golden/updates_reboot.txt +8 -0
  118. server_motd-0.1.0/tests/golden/uptime.txt +6 -0
  119. server_motd-0.1.0/tests/test_ansible_role.py +148 -0
  120. server_motd-0.1.0/tests/test_check.py +133 -0
  121. server_motd-0.1.0/tests/test_cli.py +15 -0
  122. server_motd-0.1.0/tests/test_docs.py +107 -0
  123. server_motd-0.1.0/tests/test_layout.py +127 -0
  124. server_motd-0.1.0/tests/test_profile.py +123 -0
  125. server_motd-0.1.0/tests/test_qbittorrent.py +119 -0
  126. server_motd-0.1.0/tests/test_render.py +161 -0
  127. server_motd-0.1.0/tests/test_rtorrent.py +66 -0
  128. server_motd-0.1.0/tests/test_sabnzbd.py +81 -0
  129. server_motd-0.1.0/tests/test_servarr.py +155 -0
  130. server_motd-0.1.0/tests/test_system_modules.py +184 -0
  131. server_motd-0.1.0/tests/test_tautulli.py +110 -0
  132. server_motd-0.1.0/tests/test_timeouts.py +61 -0
  133. server_motd-0.1.0/tests/test_updates.py +68 -0
  134. server_motd-0.1.0/tests/test_windows.py +151 -0
@@ -0,0 +1,5 @@
1
+ ---
2
+ profile: production
3
+ exclude_paths:
4
+ - .github/
5
+ - docs/
@@ -0,0 +1,3 @@
1
+ # Templates the role renders onto Linux hosts must keep LF endings, even in a
2
+ # Windows checkout with core.autocrlf (Ansible reads the working tree).
3
+ ansible/roles/server_motd/templates/** text eol=lf
@@ -0,0 +1,149 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: ${{ matrix.os }} py${{ matrix.python }}
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ include:
16
+ - { os: ubuntu-latest, python: "3.11" }
17
+ - { os: ubuntu-latest, python: "3.12" }
18
+ - { os: ubuntu-latest, python: "3.13" }
19
+ - { os: ubuntu-latest, python: "3.14" }
20
+ - { os: windows-latest, python: "3.11" }
21
+ - { os: windows-latest, python: "3.14" }
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python }}
27
+ - run: python -m pip install -e ".[dev]"
28
+ - run: ruff check .
29
+ - run: ruff format --check .
30
+ - run: mypy
31
+ - run: pytest
32
+ - run: python -m build
33
+ - name: Console script runs
34
+ run: server-motd
35
+
36
+ ansible-lint:
37
+ name: ansible-lint
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: "3.12"
44
+ - run: python -m pip install ansible-lint
45
+ - run: ansible-lint
46
+
47
+ debian-hook:
48
+ name: role and hook render
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - uses: actions/setup-python@v5
53
+ with:
54
+ python-version: "3.12"
55
+ - run: python -m pip install build ansible-core
56
+ - run: python -m build --wheel
57
+ - name: Apply the role to the runner
58
+ env:
59
+ ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
60
+ ANSIBLE_STDOUT_CALLBACK: default
61
+ run: |
62
+ wheel=$(ls "$PWD"/dist/*.whl)
63
+ ansible-playbook -i localhost, -c local -b ansible/ci-present.yml \
64
+ -e "ansible_python_interpreter=$(command -v python3)" \
65
+ -e "server_motd_package_src=$wheel"
66
+ - name: Second run is idempotent
67
+ env:
68
+ ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
69
+ run: |
70
+ wheel=$(ls "$PWD"/dist/*.whl)
71
+ ansible-playbook -i localhost, -c local -b ansible/ci-present.yml \
72
+ -e "ansible_python_interpreter=$(command -v python3)" \
73
+ -e "server_motd_package_src=$wheel" | tee run2.log
74
+ grep -E "changed=0 .*failed=0" run2.log
75
+ - name: Config is valid and read-only to the service user
76
+ run: |
77
+ sudo stat -c '%U %G %a' /etc/server-motd/config.toml | grep -x 'root server-motd 640'
78
+ sudo -u server-motd /opt/server-motd/venv/bin/server-motd check
79
+ - name: MOTD renders through the hook
80
+ run: |
81
+ sudo /etc/update-motd.d/50-server-motd | tee motd.txt
82
+ grep -q "ci-host" motd.txt
83
+ sudo env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d | grep -q "ci-host"
84
+ - name: Hook exits 0 with the venv missing
85
+ run: |
86
+ sudo mv /opt/server-motd/venv /opt/server-motd/venv.off
87
+ sudo /etc/update-motd.d/50-server-motd
88
+ sudo mv /opt/server-motd/venv.off /opt/server-motd/venv
89
+ - name: state absent removes everything
90
+ env:
91
+ ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
92
+ run: |
93
+ ansible-playbook -i localhost, -c local -b ansible/ci-absent.yml \
94
+ -e "ansible_python_interpreter=$(command -v python3)"
95
+ test ! -e /etc/update-motd.d/50-server-motd
96
+ test ! -e /opt/server-motd
97
+ test ! -e /etc/server-motd
98
+ ! getent passwd server-motd
99
+
100
+ debian-hook-custom-paths:
101
+ name: role and hook render (non-default paths)
102
+ runs-on: ubuntu-latest
103
+ env:
104
+ ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles
105
+ ANSIBLE_STDOUT_CALLBACK: default
106
+ steps:
107
+ - uses: actions/checkout@v4
108
+ - uses: actions/setup-python@v5
109
+ with:
110
+ python-version: "3.12"
111
+ - run: python -m pip install build ansible-core
112
+ - run: python -m build --wheel
113
+ - name: Write the non-default role variables
114
+ run: |
115
+ cat > custom-paths.yml <<'EOF'
116
+ server_motd_user: motd-ci
117
+ server_motd_base_dir: /srv/motd-ci
118
+ server_motd_config_dir: /etc/motd-ci
119
+ EOF
120
+ - name: Apply the role with a custom user, base dir and config dir
121
+ run: |
122
+ wheel=$(ls "$PWD"/dist/*.whl)
123
+ ansible-playbook -i localhost, -c local -b ansible/ci-present.yml \
124
+ -e "ansible_python_interpreter=$(command -v python3)" \
125
+ -e "server_motd_package_src=$wheel" \
126
+ -e @custom-paths.yml
127
+ - name: Nothing landed at the default paths
128
+ run: |
129
+ test ! -e /opt/server-motd
130
+ test ! -e /etc/server-motd
131
+ ! getent passwd server-motd
132
+ - name: Config is valid and read-only to the custom user
133
+ run: |
134
+ sudo stat -c '%U %G %a' /etc/motd-ci/config.toml | grep -x 'root motd-ci 640'
135
+ sudo -u motd-ci /srv/motd-ci/venv/bin/server-motd check --config /etc/motd-ci/config.toml
136
+ - name: MOTD renders through the hook
137
+ run: |
138
+ sudo /etc/update-motd.d/50-server-motd | tee motd.txt
139
+ grep -q "ci-host" motd.txt
140
+ sudo env -i PATH=/usr/sbin:/usr/bin:/sbin:/bin run-parts --lsbsysinit /etc/update-motd.d | grep -q "ci-host"
141
+ - name: state absent removes the custom paths
142
+ run: |
143
+ ansible-playbook -i localhost, -c local -b ansible/ci-absent.yml \
144
+ -e "ansible_python_interpreter=$(command -v python3)" \
145
+ -e @custom-paths.yml
146
+ test ! -e /etc/update-motd.d/50-server-motd
147
+ test ! -e /srv/motd-ci
148
+ test ! -e /etc/motd-ci
149
+ ! getent passwd motd-ci
@@ -0,0 +1,152 @@
1
+ name: Release
2
+
3
+ # Tag-triggered release: build, publish to PyPI (trusted publishing, no
4
+ # tokens), create a GitHub Release, then open a PR refreshing the role lockfile.
5
+ # Maintainer prerequisites: docs/releasing.md.
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ build:
16
+ name: Build and verify
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - name: Tag matches pyproject version
24
+ run: |
25
+ version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
26
+ if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
27
+ echo "Tag ${GITHUB_REF_NAME} does not match pyproject version ${version}" >&2
28
+ exit 1
29
+ fi
30
+ - name: Changelog has an entry for this version
31
+ run: grep -q "^## \[${GITHUB_REF_NAME#v}\]" CHANGELOG.md
32
+ - run: python -m pip install -e ".[dev]"
33
+ - run: ruff check .
34
+ - run: ruff format --check .
35
+ - run: mypy
36
+ - run: pytest
37
+ - run: python -m build
38
+ - uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+ if-no-files-found: error
43
+
44
+ publish-pypi:
45
+ name: Publish to PyPI
46
+ needs: build
47
+ runs-on: ubuntu-latest
48
+ environment:
49
+ name: pypi
50
+ url: https://pypi.org/project/server-motd/
51
+ permissions:
52
+ id-token: write
53
+ steps:
54
+ - uses: actions/download-artifact@v4
55
+ with:
56
+ name: dist
57
+ path: dist/
58
+ - uses: pypa/gh-action-pypi-publish@release/v1
59
+
60
+ github-release:
61
+ name: GitHub Release
62
+ needs: publish-pypi
63
+ runs-on: ubuntu-latest
64
+ permissions:
65
+ contents: write
66
+ steps:
67
+ - uses: actions/checkout@v4
68
+ - uses: actions/download-artifact@v4
69
+ with:
70
+ name: dist
71
+ path: dist/
72
+ - name: Extract changelog section
73
+ run: |
74
+ version="${GITHUB_REF_NAME#v}"
75
+ awk -v v="$version" '
76
+ /^## \[/ { if (found) exit; if (index($0, "[" v "]")) { found = 1; next } }
77
+ found { print }
78
+ ' CHANGELOG.md > release-notes.md
79
+ - name: Create release
80
+ env:
81
+ GH_TOKEN: ${{ github.token }}
82
+ run: |
83
+ gh release create "$GITHUB_REF_NAME" dist/* \
84
+ --title "$GITHUB_REF_NAME" \
85
+ --notes-file release-notes.md \
86
+ --verify-tag
87
+
88
+ lockfile-pr:
89
+ name: Role lockfile PR
90
+ needs: publish-pypi
91
+ runs-on: ubuntu-latest
92
+ permissions:
93
+ contents: write
94
+ pull-requests: write
95
+ env:
96
+ # Role lockfile; a placeholder until the first release fills it.
97
+ LOCKFILE: ansible/roles/server_motd/files/requirements.lock
98
+ steps:
99
+ - uses: actions/checkout@v4
100
+ with:
101
+ ref: master
102
+ - name: Check for role lockfile
103
+ id: check
104
+ run: |
105
+ if [ -f "$LOCKFILE" ]; then
106
+ echo "present=true" >> "$GITHUB_OUTPUT"
107
+ else
108
+ echo "::notice::$LOCKFILE does not exist yet; skipping lockfile PR"
109
+ echo "present=false" >> "$GITHUB_OUTPUT"
110
+ fi
111
+ - if: steps.check.outputs.present == 'true'
112
+ uses: actions/setup-python@v5
113
+ with:
114
+ python-version: "3.12"
115
+ - name: Regenerate lockfile
116
+ if: steps.check.outputs.present == 'true'
117
+ run: |
118
+ version="${GITHUB_REF_NAME#v}"
119
+ python -m pip install pip-tools
120
+ # The role always installs the figlet banner (MOTD layout decision).
121
+ echo "server-motd[figlet]==${version}" > requirements.in
122
+ # PyPI's index can lag a few minutes behind the upload.
123
+ # --strip-extras keeps the pin as "server-motd==", which the role checks for.
124
+ for attempt in 1 2 3 4 5 6; do
125
+ if pip-compile --quiet --generate-hashes --allow-unsafe --strip-extras \
126
+ --output-file "$LOCKFILE" requirements.in; then
127
+ break
128
+ fi
129
+ [ "$attempt" = 6 ] && exit 1
130
+ sleep 30
131
+ done
132
+ rm requirements.in
133
+ - name: Open pull request
134
+ if: steps.check.outputs.present == 'true'
135
+ env:
136
+ GH_TOKEN: ${{ github.token }}
137
+ run: |
138
+ if git diff --quiet -- "$LOCKFILE"; then
139
+ echo "Lockfile already current"
140
+ exit 0
141
+ fi
142
+ branch="release/lockfile-${GITHUB_REF_NAME}"
143
+ git config user.name "github-actions[bot]"
144
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
145
+ git checkout -b "$branch"
146
+ git add "$LOCKFILE"
147
+ git commit -m "Update role lockfile for ${GITHUB_REF_NAME}"
148
+ git push origin "$branch"
149
+ # Explicit --repo: this repo is a fork, and gh would otherwise target the parent.
150
+ gh pr create --repo "$GITHUB_REPOSITORY" --base master --head "$branch" \
151
+ --title "Update role lockfile for ${GITHUB_REF_NAME}" \
152
+ --body "Automated: refreshes the hash-pinned lockfile used by the Ansible role to install server-motd ${GITHUB_REF_NAME#v} from PyPI."
@@ -0,0 +1,43 @@
1
+ # User-specific stuff:
2
+ .idea/*
3
+
4
+ ## File-based project format:
5
+ *.iws
6
+
7
+ # IntelliJ
8
+ /out/
9
+
10
+ # Byte-compiled / optimized / DLL files
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+
15
+ # logs
16
+ *.log*
17
+
18
+ # databases
19
+ *.db
20
+
21
+ # configs
22
+ *.cfg
23
+ *.json
24
+
25
+ # generators
26
+ *.bat
27
+
28
+ # locks
29
+ locks/*
30
+
31
+ # venv
32
+ /venv/
33
+ .claude/worktrees/
34
+
35
+ # dev
36
+ /.venv/
37
+ /uv.lock
38
+ .mypy_cache/
39
+ .ruff_cache/
40
+ .pytest_cache/
41
+ /dist/
42
+ *.egg-info/
43
+ !tests/fixtures/**/*.json
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ Hand-written. Versions follow `pyproject.toml` and stay 0.x until the v1 scope
4
+ ships. Each release needs a `## [x.y.z]` heading here; the release workflow
5
+ fails without one and uses the section as the GitHub Release notes.
6
+
7
+ ## [0.1.0]
8
+
9
+ First release of `server-motd`, a from-scratch rewrite of the Cloudbox-MOTD
10
+ fork as an independent project. Nothing is compatible with v1 (`config.json`,
11
+ `/opt/motd`, the old fragment names); see `docs/migrating-from-v1.md`.
12
+
13
+ - One pure-Python package for Python 3.11+, with `psutil` as its only required dependency; HTTP and XML-RPC use the standard library.
14
+ - System modules: `ips`, `uptime`, `load`, `cpu`, `memory`, `disks`, `logins`, `processes` and `updates` (pending updates and reboot-required).
15
+ - Service modules, each with any number of named instances: Tautulli, qBittorrent, SABnzbd, rTorrent, Sonarr, Radarr and Lidarr.
16
+ - Modules are fetched concurrently, with a 3 s timeout per module and a 6 s total; a failing module is shown as unavailable instead of breaking the MOTD.
17
+ - TOML config with an ordered `[[modules]]` list, per-module `warn`/`crit` thresholds, `${ENV_VAR}` and `*_file` secrets, and strict validation scoped to each module.
18
+ - Built-in ANSI renderer: two-column boxed layout on wide terminals, a single column on narrow ones, several box styles (including ASCII), and an optional figlet banner (the `figlet` extra).
19
+ - CLI: `server-motd`, `server-motd check`, `install-profile` and `uninstall-profile`, with `--config`, `--width`, `--color` and `--verbose`.
20
+ - Debian/Ubuntu: an Ansible role (`ansible/roles/server_motd`) installs the package and the figlet banner from a hash-pinned lockfile into a root-owned venv, adds a single `update-motd.d` hook that runs as a dedicated user and always exits 0, writes `config.toml` as `root:<user> 0640`, and removes everything with `state: absent`. It never wipes the distro's other MOTD fragments.
21
+ - Windows: a guarded block in the current user's PowerShell 7 and 5.1 profiles, with a per-user `%APPDATA%` config and a read-only `%ProgramData%` fallback.
22
+ - Documentation: README with a golden-tested sample output, a tested example config covering every module kind, install guides for Debian and Windows, and pages on configuration, secrets, troubleshooting, releasing and migrating from v1.
23
+ - Tautulli's bandwidth unit (assumed kbps) is not yet verified against a real server.
24
+ - The legacy v1 bash/Python code is removed; it is kept in the `legacy-v1` git tag.
@@ -0,0 +1,13 @@
1
+ # CLAUDE.md
2
+
3
+ Run all Python through `uv`: `uv run --extra dev pytest` (dev tools are the `dev` extra), `uv run python …`, `uv tool` in place of pipx. `uv` works on the Windows host; a bare `python3` there is a Store stub that hangs or fails silently, and there is no `jq`. Ansible needs the Debian WSL distro, which has no `uv` installed.
4
+
5
+ Plan and decisions: GitHub issue #1 (the wayfinder map) and `docs/adr/`. Before touching an external API, the Debian MOTD, or the Windows profile hook, read the matching note in `docs/research/`.
6
+
7
+ The removed v1 bash/Python code lives in the local git tag `legacy-v1`; keep that tag local (never push it).
8
+
9
+ ## Agent skills
10
+
11
+ - Issue tracker: GitHub Issues via `gh`. See `docs/agents/issue-tracker.md`.
12
+ - Triage labels: `docs/agents/triage-labels.md`.
13
+ - Domain docs (single-context): `GLOSSARY.md` and `docs/adr/`. See `docs/agents/domain.md`.
@@ -0,0 +1,45 @@
1
+ # server-motd
2
+
3
+ A cross-platform login banner that shows a server's identity, system health and the status of the services it runs.
4
+
5
+ ## Language
6
+
7
+ **Module**:
8
+ One self-contained unit of MOTD output, either a system section or a service integration.
9
+ _Avoid_: Plugin, fragment, section
10
+
11
+ **Instance**:
12
+ One named configuration of a service module, so the same service can appear more than once (e.g. two Sonarrs).
13
+ _Avoid_: Profile, server entry
14
+
15
+ **System module**:
16
+ A module reporting on the host itself (uptime, memory, disks, pending updates).
17
+ _Avoid_: Stat, system section
18
+
19
+ **Service module**:
20
+ A module reporting on an external application the host runs (Sonarr, qBittorrent, Tautulli).
21
+ _Avoid_: Integration, provider
22
+
23
+ **Banner**:
24
+ The identity header at the top of the MOTD, showing the server's title.
25
+ _Avoid_: Header, logo
26
+
27
+ **Host integration**:
28
+ The OS-specific hook that makes the MOTD print at login (`update-motd.d` on Debian-family, a PowerShell profile on Windows).
29
+ _Avoid_: Installer, shim
30
+
31
+ **Result**:
32
+ What a module returns: a title, a list of rows and an overall status.
33
+ _Avoid_: Output, section
34
+
35
+ **Row**:
36
+ One labelled value line within a result, with its own status.
37
+ _Avoid_: Line, stat
38
+
39
+ **Status**:
40
+ The semantic health of a row or result (info, ok, warn, crit, unavailable); the renderer maps it to colour.
41
+ _Avoid_: Level, severity, colour
42
+
43
+ **Profile block**:
44
+ The marked span `install-profile` writes into a PowerShell profile to run the MOTD at login; everything outside the markers belongs to the user.
45
+ _Avoid_: Snippet, hook