astrapi-core 26.4.22__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 (149) hide show
  1. astrapi_core-26.4.22/.claude/settings.json +7 -0
  2. astrapi_core-26.4.22/.claude-memory.md +164 -0
  3. astrapi_core-26.4.22/.github/workflows/publish.yml +31 -0
  4. astrapi_core-26.4.22/.gitignore +40 -0
  5. astrapi_core-26.4.22/.gitlab-ci.yml +24 -0
  6. astrapi_core-26.4.22/.vscode/settings.json +15 -0
  7. astrapi_core-26.4.22/CLAUDE.md +3 -0
  8. astrapi_core-26.4.22/PKG-INFO +3 -0
  9. astrapi_core-26.4.22/astrapi_core/modules/activity_log/__init__.py +6 -0
  10. astrapi_core-26.4.22/astrapi_core/modules/activity_log/api.py +104 -0
  11. astrapi_core-26.4.22/astrapi_core/modules/activity_log/engine.py +41 -0
  12. astrapi_core-26.4.22/astrapi_core/modules/activity_log/icon-outline.svg +1 -0
  13. astrapi_core-26.4.22/astrapi_core/modules/activity_log/icon.svg +1 -0
  14. astrapi_core-26.4.22/astrapi_core/modules/activity_log/modul.yaml +4 -0
  15. astrapi_core-26.4.22/astrapi_core/modules/activity_log/templates/content.html +71 -0
  16. astrapi_core-26.4.22/astrapi_core/modules/activity_log/templates/modals/detail.html +111 -0
  17. astrapi_core-26.4.22/astrapi_core/modules/activity_log/templates/modals/log_viewer.html +25 -0
  18. astrapi_core-26.4.22/astrapi_core/modules/activity_log/templates/partials/rows.html +78 -0
  19. astrapi_core-26.4.22/astrapi_core/modules/activity_log/ui.py +44 -0
  20. astrapi_core-26.4.22/astrapi_core/modules/notify/__init__.py +26 -0
  21. astrapi_core-26.4.22/astrapi_core/modules/notify/api.py +122 -0
  22. astrapi_core-26.4.22/astrapi_core/modules/notify/backends/__init__.py +2 -0
  23. astrapi_core-26.4.22/astrapi_core/modules/notify/backends/email.py +117 -0
  24. astrapi_core-26.4.22/astrapi_core/modules/notify/backends/ntfy.py +102 -0
  25. astrapi_core-26.4.22/astrapi_core/modules/notify/engine.py +332 -0
  26. astrapi_core-26.4.22/astrapi_core/modules/notify/icon-outline.svg +1 -0
  27. astrapi_core-26.4.22/astrapi_core/modules/notify/icon.svg +1 -0
  28. astrapi_core-26.4.22/astrapi_core/modules/notify/schema.py +60 -0
  29. astrapi_core-26.4.22/astrapi_core/modules/notify/templates/content.html +236 -0
  30. astrapi_core-26.4.22/astrapi_core/modules/notify/templates/modals/backend_select.html +215 -0
  31. astrapi_core-26.4.22/astrapi_core/modules/notify/templates/modals/channel.html +178 -0
  32. astrapi_core-26.4.22/astrapi_core/modules/notify/templates/modals/job.html +218 -0
  33. astrapi_core-26.4.22/astrapi_core/modules/notify/templates/partials/test_badge.html +22 -0
  34. astrapi_core-26.4.22/astrapi_core/modules/notify/ui.py +307 -0
  35. astrapi_core-26.4.22/astrapi_core/modules/scheduler/__init__.py +20 -0
  36. astrapi_core-26.4.22/astrapi_core/modules/scheduler/api.py +80 -0
  37. astrapi_core-26.4.22/astrapi_core/modules/scheduler/engine.py +374 -0
  38. astrapi_core-26.4.22/astrapi_core/modules/scheduler/icon-outline.svg +1 -0
  39. astrapi_core-26.4.22/astrapi_core/modules/scheduler/icon.svg +1 -0
  40. astrapi_core-26.4.22/astrapi_core/modules/scheduler/job_runner.py +135 -0
  41. astrapi_core-26.4.22/astrapi_core/modules/scheduler/templates/content.html +16 -0
  42. astrapi_core-26.4.22/astrapi_core/modules/scheduler/templates/modals/edit.html +369 -0
  43. astrapi_core-26.4.22/astrapi_core/modules/scheduler/templates/partials/list.html +126 -0
  44. astrapi_core-26.4.22/astrapi_core/modules/scheduler/ui.py +168 -0
  45. astrapi_core-26.4.22/astrapi_core/modules/settings/__init__.py +11 -0
  46. astrapi_core-26.4.22/astrapi_core/modules/settings/engine.py +58 -0
  47. astrapi_core-26.4.22/astrapi_core/modules/settings/icon-outline.svg +1 -0
  48. astrapi_core-26.4.22/astrapi_core/modules/settings/icon.svg +1 -0
  49. astrapi_core-26.4.22/astrapi_core/modules/settings/templates/content.html +4 -0
  50. astrapi_core-26.4.22/astrapi_core/modules/settings/templates/partials/module_card.html +164 -0
  51. astrapi_core-26.4.22/astrapi_core/modules/settings/templates/partials/ssh_key.html +35 -0
  52. astrapi_core-26.4.22/astrapi_core/modules/settings/ui.py +116 -0
  53. astrapi_core-26.4.22/astrapi_core/modules/system/__init__.py +14 -0
  54. astrapi_core-26.4.22/astrapi_core/modules/system/api.py +48 -0
  55. astrapi_core-26.4.22/astrapi_core/modules/system/engine.py +287 -0
  56. astrapi_core-26.4.22/astrapi_core/modules/system/icon-outline.svg +1 -0
  57. astrapi_core-26.4.22/astrapi_core/modules/system/icon.svg +1 -0
  58. astrapi_core-26.4.22/astrapi_core/modules/system/templates/content.html +51 -0
  59. astrapi_core-26.4.22/astrapi_core/modules/system/templates/modals/update.html +18 -0
  60. astrapi_core-26.4.22/astrapi_core/modules/system/templates/partials/metrics.html +204 -0
  61. astrapi_core-26.4.22/astrapi_core/modules/system/templates/partials/update_log.html +49 -0
  62. astrapi_core-26.4.22/astrapi_core/modules/system/ui.py +56 -0
  63. astrapi_core-26.4.22/astrapi_core/modules/system/updater.py +296 -0
  64. astrapi_core-26.4.22/astrapi_core/navigation.yaml +24 -0
  65. astrapi_core-26.4.22/astrapi_core/system/__init__.py +0 -0
  66. astrapi_core-26.4.22/astrapi_core/system/activity_log.py +310 -0
  67. astrapi_core-26.4.22/astrapi_core/system/cmd.py +100 -0
  68. astrapi_core-26.4.22/astrapi_core/system/db.py +304 -0
  69. astrapi_core-26.4.22/astrapi_core/system/format.py +13 -0
  70. astrapi_core-26.4.22/astrapi_core/system/health.py +43 -0
  71. astrapi_core-26.4.22/astrapi_core/system/logger.py +170 -0
  72. astrapi_core-26.4.22/astrapi_core/system/paths.py +164 -0
  73. astrapi_core-26.4.22/astrapi_core/system/reachability.py +37 -0
  74. astrapi_core-26.4.22/astrapi_core/system/secrets.py +124 -0
  75. astrapi_core-26.4.22/astrapi_core/system/systemd.py +42 -0
  76. astrapi_core-26.4.22/astrapi_core/system/version.py +62 -0
  77. astrapi_core-26.4.22/astrapi_core/ui/__init__.py +12 -0
  78. astrapi_core-26.4.22/astrapi_core/ui/_base.py +80 -0
  79. astrapi_core-26.4.22/astrapi_core/ui/app.py +473 -0
  80. astrapi_core-26.4.22/astrapi_core/ui/crud_blueprint.py +321 -0
  81. astrapi_core-26.4.22/astrapi_core/ui/crud_router.py +101 -0
  82. astrapi_core-26.4.22/astrapi_core/ui/fastapi_templates.py +21 -0
  83. astrapi_core-26.4.22/astrapi_core/ui/field_resolver.py +28 -0
  84. astrapi_core-26.4.22/astrapi_core/ui/htmx_crud_router.py +192 -0
  85. astrapi_core-26.4.22/astrapi_core/ui/icons/.gitkeep +0 -0
  86. astrapi_core-26.4.22/astrapi_core/ui/icons/circle-outline.svg +1 -0
  87. astrapi_core-26.4.22/astrapi_core/ui/icons/circle.svg +1 -0
  88. astrapi_core-26.4.22/astrapi_core/ui/icons/moon.svg +1 -0
  89. astrapi_core-26.4.22/astrapi_core/ui/icons/sun.svg +1 -0
  90. astrapi_core-26.4.22/astrapi_core/ui/icons.py +84 -0
  91. astrapi_core-26.4.22/astrapi_core/ui/module_loader.py +213 -0
  92. astrapi_core-26.4.22/astrapi_core/ui/module_registry.py +374 -0
  93. astrapi_core-26.4.22/astrapi_core/ui/page_factory.py +99 -0
  94. astrapi_core-26.4.22/astrapi_core/ui/render.py +68 -0
  95. astrapi_core-26.4.22/astrapi_core/ui/schema_loader.py +63 -0
  96. astrapi_core-26.4.22/astrapi_core/ui/settings_registry.py +206 -0
  97. astrapi_core-26.4.22/astrapi_core/ui/static/css/app.css +1265 -0
  98. astrapi_core-26.4.22/astrapi_core/ui/static/icons/icon-192.png +0 -0
  99. astrapi_core-26.4.22/astrapi_core/ui/static/icons/icon-512.png +0 -0
  100. astrapi_core-26.4.22/astrapi_core/ui/static/js/app.js +197 -0
  101. astrapi_core-26.4.22/astrapi_core/ui/static/js/components/navigation.js +34 -0
  102. astrapi_core-26.4.22/astrapi_core/ui/static/manifest.json +24 -0
  103. astrapi_core-26.4.22/astrapi_core/ui/static/swagger.html +26 -0
  104. astrapi_core-26.4.22/astrapi_core/ui/static/ui_map.html +400 -0
  105. astrapi_core-26.4.22/astrapi_core/ui/storage.py +218 -0
  106. astrapi_core-26.4.22/astrapi_core/ui/store.py +99 -0
  107. astrapi_core-26.4.22/astrapi_core/ui/swagger_utils.py +174 -0
  108. astrapi_core-26.4.22/astrapi_core/ui/templates/_base.html +88 -0
  109. astrapi_core-26.4.22/astrapi_core/ui/templates/content.html +12 -0
  110. astrapi_core-26.4.22/astrapi_core/ui/templates/index.html +90 -0
  111. astrapi_core-26.4.22/astrapi_core/ui/templates/navigation/index.html +93 -0
  112. astrapi_core-26.4.22/astrapi_core/ui/templates/navigation/items.yaml +21 -0
  113. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/components/card.html +21 -0
  114. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/components/sprites/icons.svg +191 -0
  115. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/components/ui_macros.html +290 -0
  116. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/confirm_modal.html +57 -0
  117. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/create_edit/create_edit_modal.html +198 -0
  118. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/create_edit/field_renderer.html +124 -0
  119. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/create_edit/list_field.html +40 -0
  120. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/list_wrapper_inner.html +275 -0
  121. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/lists/settings.html +61 -0
  122. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/log_content.html +12 -0
  123. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/log_modal.html +127 -0
  124. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/notify_card.html +119 -0
  125. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/preview_modal.html +57 -0
  126. astrapi_core-26.4.22/astrapi_core/ui/templates/partials/settings_modal.html +156 -0
  127. astrapi_core-26.4.22/astrapi_core.egg-info/PKG-INFO +3 -0
  128. astrapi_core-26.4.22/astrapi_core.egg-info/SOURCES.txt +147 -0
  129. astrapi_core-26.4.22/astrapi_core.egg-info/dependency_links.txt +1 -0
  130. astrapi_core-26.4.22/astrapi_core.egg-info/top_level.txt +1 -0
  131. astrapi_core-26.4.22/dev_app/config.yaml +8 -0
  132. astrapi_core-26.4.22/dev_app/modules/demo_items/__init__.py +6 -0
  133. astrapi_core-26.4.22/dev_app/modules/demo_items/api.py +18 -0
  134. astrapi_core-26.4.22/dev_app/modules/demo_items/modul.yaml +6 -0
  135. astrapi_core-26.4.22/dev_app/modules/demo_items/schema.yaml +52 -0
  136. astrapi_core-26.4.22/dev_app/modules/demo_items/storage.py +4 -0
  137. astrapi_core-26.4.22/dev_app/modules/demo_items/templates/partials/list.html +10 -0
  138. astrapi_core-26.4.22/dev_app/modules/demo_items/templates/partials/list_header.html +3 -0
  139. astrapi_core-26.4.22/dev_app/modules/demo_items/templates/partials/list_row.html +3 -0
  140. astrapi_core-26.4.22/dev_app/modules/demo_items/ui.py +14 -0
  141. astrapi_core-26.4.22/dev_app/modules/demo_log/__init__.py +5 -0
  142. astrapi_core-26.4.22/dev_app/modules/demo_log/modul.yaml +5 -0
  143. astrapi_core-26.4.22/dev_app/modules/demo_log/templates/partials/list.html +42 -0
  144. astrapi_core-26.4.22/dev_app/modules/demo_log/ui.py +23 -0
  145. astrapi_core-26.4.22/icon-migration.md +181 -0
  146. astrapi_core-26.4.22/main.py +89 -0
  147. astrapi_core-26.4.22/pyproject.toml +14 -0
  148. astrapi_core-26.4.22/requirements.txt +32 -0
  149. astrapi_core-26.4.22/setup.cfg +4 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:raw.githubusercontent.com)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,164 @@
1
+ # astrapi-core – Claude Memory
2
+
3
+ Projektkontext für Claude Code. Wird im Repo versioniert, damit es auf jedem PC verfügbar ist.
4
+
5
+ ---
6
+
7
+ ## Was ist astrapi-core?
8
+
9
+ Full-stack Python-Framework, das **FastAPI** (JSON-APIs und HTML-UI) in einer ASGI-App kombiniert.
10
+ Basis für alle `ctl`-Apps (packagectl, backupctl, …).
11
+ Stellt Modul-System, generisches CRUD, Storage, Scheduler, Notifications und Activity-Log bereit.
12
+
13
+ ---
14
+
15
+ ## Stack
16
+
17
+ | Komponente | Details |
18
+ |---|---|
19
+ | API | FastAPI (`/api/...`) |
20
+ | UI | FastAPI + HTMX + Jinja2 (`/`) – kein Flask mehr! |
21
+ | Persistenz | SQLite (`SqliteStorage`, direkte SQL-Helpers) |
22
+ | Scheduler | APScheduler |
23
+ | Verschlüsselung | Fernet (Secrets) |
24
+ | Python | ≥ 3.11 |
25
+
26
+ **Einstiegspunkt für Apps:** `astrapi.core.ui.create(api: FastAPI, app_root, config, extra_init, modules)`
27
+
28
+ > **Hinweis:** Flask und a2wsgi wurden entfernt. `create()` nimmt jetzt die FastAPI-Instanz als erstes Argument und konfiguriert sie in-place.
29
+
30
+ ---
31
+
32
+ ## Verzeichnisstruktur
33
+
34
+ ```
35
+ astrapi/core/
36
+ ├── ui/
37
+ │ ├── app.py # Flask-Factory (Haupt-Entry-Point)
38
+ │ ├── _base.py # Module-Dataclass
39
+ │ ├── module_registry.py # Auto-Discovery & Registrierung
40
+ │ ├── module_loader.py # Liest modul.yaml + settings.yaml
41
+ │ ├── crud_router.py # Generischer FastAPI-CRUD-Router
42
+ │ ├── crud_blueprint.py # Generischer Flask-CRUD-Blueprint
43
+ │ ├── storage.py # SqliteStorage (ehem. YamlStorage)
44
+ │ ├── settings_registry.py # Settings (SQLite-backed, thread-safe)
45
+ │ ├── schema_loader.py # Parst schema.yaml für Formulare
46
+ │ ├── field_resolver.py # Dynamische Feldoptionen
47
+ │ ├── page_factory.py # Auto-Routes /<key>, /ui/<key>/content
48
+ │ ├── static/ # CSS, JS, Icons
49
+ │ └── templates/ # Jinja2-Templates (index.html, partials/)
50
+
51
+ ├── system/
52
+ │ ├── db.py # SQLite-Verbindungspool, register_table, CRUD-Helpers
53
+ │ ├── activity_log.py # Job-Run-Tracking (Runs + Log-Lines)
54
+ │ ├── secrets.py # Fernet-Secrets (getrennt von Backup)
55
+ │ ├── version.py # CalVer YY.MM.patch.devN
56
+ │ ├── cmd.py # Subprocess-Helpers
57
+ │ ├── health.py # Health-Check-Endpoints
58
+ │ ├── paths.py # Runtime-Pfade
59
+ │ └── systemd.py # sd_notify / Watchdog
60
+
61
+ └── modules/ # Eingebaute Core-Module
62
+ ├── activity_log/ # Job-History-Viewer
63
+ ├── notify/ # Benachrichtigungen (Email, Webhook, …)
64
+ ├── scheduler/ # APScheduler-UI + engine
65
+ ├── settings/ # Globale Einstellungs-UI
66
+ └── sysinfo/ # CPU/RAM/Disk (psutil)
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Key-API (Imports für Apps)
72
+
73
+ ```python
74
+ from astrapi.core.ui import Module, create
75
+ from astrapi.core.ui.crud_router import make_crud_router
76
+ from astrapi.core.ui.crud_blueprint import make_crud_router # UI-CRUD-Router (FastAPI)
77
+ from astrapi.core.ui.storage import SqliteStorage, YamlStorage # YamlStorage = Alias
78
+ from astrapi.core.ui.settings_registry import get, set, get_module, set_module
79
+ from astrapi.core.system.db import register_table, load_config, get_item, create_item, update_item, delete_item
80
+ from astrapi.core.system.secrets import set_secret, get_secret, get_secret_safe
81
+ from astrapi.core.system.activity_log import add_log_entry, list_runs_for_item, get_log_lines
82
+ from astrapi.core.modules.notify import engine as notify_engine # notify_engine.send(...)
83
+ from astrapi.core.modules.scheduler.engine import configure, init, register_action
84
+ from astrapi.core.ui.module_loader import load_modul
85
+ ```
86
+
87
+ > **Hinweis:** `YamlStorage` ist ein Alias für `SqliteStorage`. Die alten YAML-Dateien werden beim ersten Zugriff automatisch migriert.
88
+
89
+ ---
90
+
91
+ ## Modul-Konvention
92
+
93
+ Jedes Modul unter `{app}/modules/<key>/` oder `core/modules/<key>/`:
94
+
95
+ | Datei | Inhalt |
96
+ |---|---|
97
+ | `__init__.py` | Erstellt `Module`-Instanz, registriert Scheduler-Actions |
98
+ | `modul.yaml` | `label`, `icon`, `nav_group`, `card_actions` |
99
+ | `settings.yaml` | Einstellungsfelder (Typ text/password/select, …) |
100
+ | `schema.yaml` | Formularfelder für CRUD-Modal |
101
+ | `api.py` | FastAPI-Router (`make_crud_router` o. manuell) |
102
+ | `ui.py` | FastAPI-Router (`make_crud_router` + Zusatz-Routen) |
103
+ | `engine.py` | Business-Logik |
104
+ | `storage.py` | `store = SqliteStorage(KEY)` |
105
+ | `templates/content.html` | Vollständiger Modul-Inhalt (page-header + Listenbereich) |
106
+ | `templates/partials/card_body.html` | Card-Body-Snippet (meta-grid), eingebunden per `content_template` |
107
+ | `templates/partials/list_header.html` | Tabellen-Header-Spalten (optional) |
108
+ | `templates/partials/list_row.html` | Tabellen-Zeilen-Spalten (optional) |
109
+ | `templates/partials/` | Weitere kleine HTMX-Fragmente (rows, metrics, …) |
110
+ | `templates/modals/` | Eigenständige Modal-Dialoge (edit.html, log.html, …) |
111
+
112
+ **Card-Action-Typen:** `run`, `run_debug`, `log`, `search`, `bar-chart`, `power-on`, `power-off`, `scan-host-key`, `preview`, `archives`, `stats`
113
+
114
+ ---
115
+
116
+ ## Template-Auflösung
117
+
118
+ Priorität: **App-Templates > Core-Templates > Modul-Templates**
119
+ Realisiert via `ChoiceLoader` – Apps können Core-Templates überschreiben.
120
+
121
+ ---
122
+
123
+ ## Datenbank
124
+
125
+ - `db.configure(path)` – muss vor jedem DB-Zugriff aufgerufen werden
126
+ - `register_table(key, ddl, list_fields, col_in, col_out)` – Tabelle deklarieren
127
+ - `create_all_registered_tables()` – alle registrierten Tabellen anlegen
128
+ - Generische CRUD-Helpers: `load_config`, `get_item`, `create_item`, `update_item`, `delete_item`
129
+ - Thread-lokale Verbindungen (`check_same_thread=False`)
130
+
131
+ ---
132
+
133
+ ## Settings
134
+
135
+ ```python
136
+ # Global
137
+ from astrapi.core.ui.settings_registry import get, set
138
+ val = get("MY_KEY", default="fallback")
139
+ set("MY_KEY", "value")
140
+
141
+ # Modul-spezifisch
142
+ from astrapi.core.ui.settings_registry import get_module, set_module
143
+ val = get_module("mymod", "timeout", default="30")
144
+ ```
145
+
146
+ ---
147
+
148
+ ## schema.yaml – Feld-Typen
149
+
150
+ `text`, `number`, `boolean`, `select` (mit `options`-Liste), `list` (Multi-Value), `password` (verschlüsselt in DB)
151
+
152
+ ---
153
+
154
+ ## Versionsschema
155
+
156
+ CalVer: `YY.MM.patch.devN` – monatlicher Reset des Patch-Counters.
157
+ Release-Automatisierung via `release.sh`.
158
+
159
+ ---
160
+
161
+ ## Tests
162
+
163
+ - pytest, httpx (FastAPI-Tests), playwright (E2E / Browser-Tests)
164
+ - Entwicklungs-App: `dev_app/` mit Demo-Modulen (`demo_items`, `demo_log`)
@@ -0,0 +1,31 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write # für Trusted Publishing (optional, aber empfohlen)
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0 # nötig für setuptools-scm
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+
23
+ - name: Build
24
+ run: |
25
+ pip install --quiet build
26
+ python -m build --wheel --sdist
27
+
28
+ - name: Publish to PyPI
29
+ uses: pypa/gh-action-pypi-publish@release/v1
30
+ with:
31
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+
6
+ # Virtuelle Umgebungen
7
+ .venv/
8
+ venv/
9
+ env/
10
+
11
+ # Build & Distribution
12
+ build/
13
+ dist/
14
+ *.zip
15
+
16
+ # Tests & Coverage
17
+ .pytest_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # Datenbank
22
+ app/data/*.db
23
+ app/data/*.db-shm
24
+ app/data/*.db-wal
25
+ dev_app/data/*.db
26
+ dev_app/data/*.db-shm
27
+ dev_app/data/*.db-wal
28
+
29
+ # Logs
30
+ *.log
31
+
32
+ # Secrets
33
+ .env
34
+ .env.*
35
+ *.key
36
+
37
+ # IDE
38
+ .idea/
39
+ .vscode/
40
+ .release.env
@@ -0,0 +1,24 @@
1
+ stages:
2
+ - publish
3
+
4
+ publish:
5
+ stage: publish
6
+ image: python:3.12-slim
7
+ rules:
8
+ - if: '$CI_COMMIT_TAG =~ /^v/'
9
+ before_script:
10
+ - apt-get update -qq && apt-get install -y -qq git
11
+ - git fetch --tags
12
+ script:
13
+ - pip install --quiet build twine
14
+ - python -m build --wheel --sdist
15
+ - twine upload
16
+ --repository-url "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi"
17
+ --username "__token__"
18
+ --password "$GITLAB_TOKEN_SECRET"
19
+ dist/*
20
+ - twine upload
21
+ --repository-url "https://gitlab.com/api/v4/projects/81004951/packages/pypi"
22
+ --username "__token__"
23
+ --password "$GITLAB_TOKEN_SECRET"
24
+ dist/*
@@ -0,0 +1,15 @@
1
+ {
2
+ "python-envs.pythonProjects": [
3
+ {
4
+ "path": ".",
5
+ "envManager": "ms-python.python:venv",
6
+ "packageManager": "ms-python.python:pip"
7
+ }
8
+ ],
9
+ "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
10
+ "python.testing.pytestEnabled": true,
11
+ "python.testing.unittestEnabled": false,
12
+ "python.testing.pytestArgs": [
13
+ "tests"
14
+ ]
15
+ }
@@ -0,0 +1,3 @@
1
+ # astrapi-core
2
+
3
+ @.claude-memory.md
@@ -0,0 +1,3 @@
1
+ Metadata-Version: 2.4
2
+ Name: astrapi-core
3
+ Version: 26.4.22
@@ -0,0 +1,6 @@
1
+ from pathlib import Path
2
+ from astrapi_core.ui.module_loader import load_modul
3
+ from .api import router
4
+ from .ui import router as ui_router
5
+
6
+ module = load_modul(Path(__file__).parent, "activity_log", router, ui_router)
@@ -0,0 +1,104 @@
1
+ # core/modules/activity_log/api.py
2
+ import json
3
+ from datetime import datetime, timedelta
4
+
5
+ from fastapi import APIRouter, Request
6
+ from fastapi.responses import HTMLResponse
7
+
8
+ from astrapi_core.ui.fastapi_templates import get_templates
9
+ from .engine import (
10
+ list_activity, get_activity_log, clear_activity_log, get_log_lines,
11
+ enrich, registered_modules, fmt_duration, fmt_bytes,
12
+ )
13
+
14
+ router = APIRouter(tags=["activity_log"])
15
+
16
+
17
+ @router.get("/clear-confirm", response_class=HTMLResponse)
18
+ def activity_log_clear_confirm(request: Request):
19
+ return get_templates().TemplateResponse(request, "partials/confirm_modal.html", {
20
+ "description": "Alle Activity-Log-Einträge",
21
+ "verb": "löschen",
22
+ "confirm_url": "/api/activity_log/clear",
23
+ "method": "delete",
24
+ "container_id": "tab-activity_log",
25
+ "loading_id": "activity_log-loading",
26
+ })
27
+
28
+
29
+ @router.delete("/clear", response_class=HTMLResponse)
30
+ def activity_log_clear(request: Request):
31
+ clear_activity_log()
32
+ return get_templates().TemplateResponse(request, "activity_log/content.html", {
33
+ "entries": [],
34
+ "modules": registered_modules(),
35
+ })
36
+
37
+
38
+ @router.get("/tab", response_class=HTMLResponse)
39
+ def activity_log_tab(request: Request):
40
+ entries = enrich(list_activity(limit=200))
41
+ return get_templates().TemplateResponse(request, "activity_log/content.html", {
42
+ "entries": entries,
43
+ "modules": registered_modules(),
44
+ })
45
+
46
+
47
+ @router.get("/rows", response_class=HTMLResponse)
48
+ def activity_log_rows(
49
+ request: Request,
50
+ log_type: str = "",
51
+ module: str = "",
52
+ status: str = "",
53
+ date_range: str = "30d",
54
+ search: str = "",
55
+ ):
56
+ date_from = None
57
+ if date_range == "24h":
58
+ date_from = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
59
+ elif date_range == "7d":
60
+ date_from = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
61
+ elif date_range == "30d":
62
+ date_from = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
63
+
64
+ entries = enrich(list_activity(
65
+ limit=200,
66
+ log_type=log_type or None,
67
+ module=module or None,
68
+ status=status or None,
69
+ date_from=date_from,
70
+ search=search or None,
71
+ ))
72
+ return get_templates().TemplateResponse(request, "activity_log/partials/rows.html", {
73
+ "entries": entries,
74
+ })
75
+
76
+
77
+ @router.get("/{log_id}/detail", response_class=HTMLResponse)
78
+ def activity_log_detail(request: Request, log_id: int):
79
+ entry = get_activity_log(log_id)
80
+ if not entry:
81
+ return HTMLResponse("<div>Log-Eintrag nicht gefunden</div>")
82
+ entry["duration_fmt"] = fmt_duration(entry.get("duration_s"))
83
+ entry["bytes_fmt"] = fmt_bytes(entry.get("bytes_processed"))
84
+ if entry.get("metadata"):
85
+ try:
86
+ entry["metadata_dict"] = json.loads(entry["metadata"])
87
+ except Exception:
88
+ entry["metadata_dict"] = {}
89
+ return get_templates().TemplateResponse(request, "activity_log/modals/detail.html", {
90
+ "entry": entry,
91
+ })
92
+
93
+
94
+ @router.get("/{log_id}/log", response_class=HTMLResponse)
95
+ def activity_log_viewer(request: Request, log_id: int):
96
+ entry = get_activity_log(log_id)
97
+ if not entry:
98
+ return HTMLResponse("<div>Log nicht gefunden</div>")
99
+ lines = get_log_lines(log_id)
100
+ full_log = "\n".join(f"[{r['level']}] {r['line']}" for r in lines) if lines else entry.get("full_log", "")
101
+ return get_templates().TemplateResponse(request, "activity_log/modals/log_viewer.html", {
102
+ "entry": entry,
103
+ "full_log": full_log,
104
+ })
@@ -0,0 +1,41 @@
1
+ # core/modules/activity_log/engine.py
2
+
3
+ from astrapi_core.system.activity_log import (
4
+ log_activity, update_activity_log,
5
+ list_activity, get_activity_log, clear_activity_log,
6
+ get_log_lines, get_latest_activity_log_id, list_runs_for_item,
7
+ history_start, history_finish, list_history,
8
+ append_log_line,
9
+ )
10
+
11
+ KEY = "activity_log"
12
+
13
+
14
+ def fmt_duration(s: int | None) -> str:
15
+ if s is None:
16
+ return "—"
17
+ if s < 60:
18
+ return f"{s}s"
19
+ m, sec = divmod(s, 60)
20
+ if m < 60:
21
+ return f"{m}m {sec}s"
22
+ h, min_ = divmod(m, 60)
23
+ return f"{h}h {min_}m"
24
+
25
+
26
+ from astrapi_core.system.format import fmt_bytes
27
+
28
+
29
+ def enrich(entries: list) -> list:
30
+ for e in entries:
31
+ e["duration_fmt"] = fmt_duration(e.get("duration_s"))
32
+ e["bytes_fmt"] = fmt_bytes(e.get("bytes_processed"))
33
+ return entries
34
+
35
+
36
+ def registered_modules() -> list[str]:
37
+ try:
38
+ from astrapi_core.ui.module_registry import _mod_registry
39
+ return [key for key in _mod_registry if not key.startswith("_")]
40
+ except Exception:
41
+ return []
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 2A2 2 0 0 0 2 4V20A2 2 0 0 0 4 22H12.41A7 7 0 0 0 16 23A7 7 0 0 0 23 16A7 7 0 0 0 18 9.3V8L12 2H4M4 4H11V9H16A7 7 0 0 0 9 16A7 7 0 0 0 10.26 20H4V4M16 11A5 5 0 0 1 21 16A5 5 0 0 1 16 21A5 5 0 0 1 11 16A5 5 0 0 1 16 11M15 12V17L18.61 19.16L19.36 17.94L16.5 16.25V12H15Z" /></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 2C2.89 2 2 2.89 2 4V20A2 2 0 0 0 4 22H12.41A7 7 0 0 0 16 23A7 7 0 0 0 23 16A7 7 0 0 0 18 9.3V8L12 2H4M11 3.5L16.5 9H11V3.5M16 11A5 5 0 0 1 21 16A5 5 0 0 1 16 21A5 5 0 0 1 11 16A5 5 0 0 1 16 11M15 12V17L18.61 19.16L19.36 17.94L16.5 16.25V12H15Z" /></svg>
@@ -0,0 +1,4 @@
1
+ # core/modules/activity_log/modul.yaml
2
+ label: Activity Log
3
+ nav_group: System
4
+ nav_default: false
@@ -0,0 +1,71 @@
1
+ {% extends "content.html" %}
2
+ {% block inner %}
3
+
4
+ <div class="ds-list-table-wrap">
5
+ <div class="table-toolbar" data-debug-label="table-toolbar">
6
+ <div class="table-toolbar-title">Activity Log</div>
7
+ <div class="table-toolbar-right">
8
+ <select class="form-select table-filter-select" name="log_type"
9
+ hx-get="/api/activity_log/rows" hx-target="#activity-log-rows" hx-swap="innerHTML" hx-trigger="change"
10
+ hx-include="[name='module'],[name='status'],[name='date_range']">
11
+ <option value="">Alle Typen</option>
12
+ <option value="job">Jobs</option>
13
+ <option value="scheduler">Scheduler</option>
14
+ <option value="error">Errors</option>
15
+ <option value="warning">Warnings</option>
16
+ <option value="system">System</option>
17
+ </select>
18
+ <select class="form-select table-filter-select" name="module"
19
+ hx-get="/api/activity_log/rows" hx-target="#activity-log-rows" hx-swap="innerHTML" hx-trigger="change"
20
+ hx-include="[name='log_type'],[name='status'],[name='date_range']">
21
+ <option value="">Alle Module</option>
22
+ {% for m in modules %}
23
+ <option value="{{ m }}">{{ m.replace('_',' ').title() }}</option>
24
+ {% endfor %}
25
+ </select>
26
+ <select class="form-select table-filter-select" name="status"
27
+ hx-get="/api/activity_log/rows" hx-target="#activity-log-rows" hx-swap="innerHTML" hx-trigger="change"
28
+ hx-include="[name='log_type'],[name='module'],[name='date_range']">
29
+ <option value="">Alle Status</option>
30
+ <option value="ok">OK</option>
31
+ <option value="error">Fehler</option>
32
+ <option value="warning">Warnung</option>
33
+ <option value="running">Läuft</option>
34
+ <option value="skipped">Übersprungen</option>
35
+ </select>
36
+ <select class="form-select table-filter-select" name="date_range"
37
+ hx-get="/api/activity_log/rows" hx-target="#activity-log-rows" hx-swap="innerHTML" hx-trigger="change"
38
+ hx-include="[name='log_type'],[name='module'],[name='status']">
39
+ <option value="24h">Letzte 24h</option>
40
+ <option value="7d">7 Tage</option>
41
+ <option value="30d" selected>30 Tage</option>
42
+ <option value="">Alle</option>
43
+ </select>
44
+ <button class="btn btn-danger btn-sm"
45
+ hx-get="/api/activity_log/clear-confirm"
46
+ hx-target="body" hx-swap="beforeend">
47
+ Log leeren
48
+ </button>
49
+ </div>
50
+ </div>
51
+ <div class="ds-list-table-wrap-scroll">
52
+ <table class="ds-list-table">
53
+ <thead>
54
+ <tr>
55
+ <th>Zeitpunkt</th>
56
+ <th class="col-hide-sm">Typ</th>
57
+ <th class="col-hide-sm">Modul</th>
58
+ <th>Beschreibung</th>
59
+ <th class="col-status">Status</th>
60
+ <th class="col-hide-sm">Dauer</th>
61
+ <th class="col-actions"></th>
62
+ </tr>
63
+ </thead>
64
+ <tbody id="activity-log-rows">
65
+ {% include "activity_log/partials/rows.html" %}
66
+ </tbody>
67
+ </table>
68
+ </div>
69
+ </div>
70
+
71
+ {% endblock %}
@@ -0,0 +1,111 @@
1
+ {# activity_log/modals/detail.html #}
2
+ {% from "partials/components/ui_macros.html" import modal_title, modal_footer_simple %}
3
+ <div class="ds-modal-backdrop">
4
+ <div class="ds-modal" style="max-width:600px;">
5
+
6
+ {{ modal_title(title="Activity Log – Details") }}
7
+
8
+ <div style="display:grid; grid-template-columns:130px 1fr; gap:10px 16px; font-size:13px; padding:4px 0;">
9
+
10
+ <div style="color:var(--text-3); font-weight:600;">Zeitpunkt</div>
11
+ <div>{{ entry.created_at }}</div>
12
+
13
+ <div style="color:var(--text-3); font-weight:600;">Typ</div>
14
+ <div>
15
+ <span style="font-family:var(--mono); font-size:11px; padding:2px 6px;
16
+ border-radius:3px; background:rgba(255,255,255,0.07);">
17
+ {{ entry.log_type }}
18
+ </span>
19
+ </div>
20
+
21
+ <div style="color:var(--text-3); font-weight:600;">Modul</div>
22
+ <div>
23
+ <span style="font-family:var(--mono); font-size:11px; padding:2px 6px;
24
+ border-radius:3px; background:rgba(255,255,255,0.07);">
25
+ {{ entry.module }}
26
+ </span>
27
+ </div>
28
+
29
+ <div style="color:var(--text-3); font-weight:600;">Beschreibung</div>
30
+ <div>{{ entry.description }}</div>
31
+
32
+ <div style="color:var(--text-3); font-weight:600;">Status</div>
33
+ <div>
34
+ {% if entry.status == 'ok' %}<span style="color:var(--g);">✓ OK</span>
35
+ {% elif entry.status == 'error' %}<span style="color:var(--err);">✗ Fehler</span>
36
+ {% elif entry.status == 'warning' %}<span style="color:#f59e0b;">⚠ Warnung</span>
37
+ {% elif entry.status == 'running' %}<span style="color:#60a5fa;">● läuft</span>
38
+ {% else %}<span>{{ entry.status }}</span>{% endif %}
39
+ </div>
40
+
41
+ {% if entry.mode %}
42
+ <div style="color:var(--text-3); font-weight:600;">Modus</div>
43
+ <div>{{ entry.mode }}</div>
44
+ {% endif %}
45
+
46
+ {% if entry.duration_fmt %}
47
+ <div style="color:var(--text-3); font-weight:600;">Dauer</div>
48
+ <div style="font-family:var(--mono);">{{ entry.duration_fmt }}</div>
49
+ {% endif %}
50
+
51
+ {% if entry.bytes_processed %}
52
+ <div style="color:var(--text-3); font-weight:600;">Bytes</div>
53
+ <div style="font-family:var(--mono);">{{ entry.bytes_fmt }}</div>
54
+ {% endif %}
55
+
56
+ {% if entry.items_count %}
57
+ <div style="color:var(--text-3); font-weight:600;">Items</div>
58
+ <div>{{ entry.items_count }}</div>
59
+ {% endif %}
60
+
61
+ {% if entry.finished_at %}
62
+ <div style="color:var(--text-3); font-weight:600;">Beendet</div>
63
+ <div>{{ entry.finished_at }}</div>
64
+ {% endif %}
65
+
66
+ {% if entry.error_message %}
67
+ <div style="color:var(--text-3); font-weight:600;">Fehler-Code</div>
68
+ <div style="font-family:var(--mono);">{{ entry.error_code or '—' }}</div>
69
+
70
+ <div style="color:var(--text-3); font-weight:600; grid-column:1">Fehlermeldung</div>
71
+ <div style="grid-column:2; font-family:var(--mono); font-size:11px;
72
+ padding:8px; background:rgba(255,0,0,0.06); border-radius:4px; overflow-x:auto;">
73
+ {{ entry.error_message }}
74
+ </div>
75
+ {% endif %}
76
+
77
+ {% if entry.error_traceback %}
78
+ <div style="color:var(--text-3); font-weight:600; grid-column:1/3;">Traceback</div>
79
+ <div style="grid-column:1/3; font-family:var(--mono); font-size:10px;
80
+ padding:8px; background:rgba(255,0,0,0.04); border-radius:4px;
81
+ overflow-x:auto; white-space:pre-wrap;">{{ entry.error_traceback }}</div>
82
+ {% endif %}
83
+
84
+ {% if entry.metadata_dict %}
85
+ <div style="color:var(--text-3); font-weight:600; grid-column:1/3;">Metadaten</div>
86
+ <div style="grid-column:1/3; font-family:var(--mono); font-size:11px;
87
+ padding:8px; background:rgba(255,255,255,0.03); border-radius:4px;">
88
+ {% for k, v in entry.metadata_dict.items() %}
89
+ <div><span style="color:var(--text-3);">{{ k }}:</span> {{ v }}</div>
90
+ {% endfor %}
91
+ </div>
92
+ {% endif %}
93
+
94
+ </div>
95
+
96
+ <div class="ds-modal-footer-simple">
97
+ {% if entry.log_type in ('job', 'scheduler') %}
98
+ <button class="btn btn-primary"
99
+ hx-get="/api/activity_log/{{ entry.id }}/log"
100
+ hx-target="body" hx-swap="beforeend">
101
+ Log anzeigen
102
+ </button>
103
+ {% endif %}
104
+ <button class="btn btn-secondary"
105
+ onclick="closeModal(this)">
106
+ Schließen
107
+ </button>
108
+ </div>
109
+
110
+ </div>
111
+ </div>
@@ -0,0 +1,25 @@
1
+ {# activity_log/modals/log_viewer.html #}
2
+ {% from "partials/components/ui_macros.html" import modal_title %}
3
+ <div class="ds-modal-backdrop">
4
+ <div class="ds-modal" style="max-width:1000px; width:90%;">
5
+
6
+ {{ modal_title(title="Log: " + entry.description) }}
7
+
8
+ <pre style="margin:0; padding:12px; background:var(--bg); color:var(--text-2);
9
+ font-family:var(--mono); font-size:11px; line-height:1.5;
10
+ overflow-x:auto; white-space:pre-wrap; word-break:break-word;
11
+ max-height:65vh; overflow-y:auto; border-radius:4px;">{{ full_log }}</pre>
12
+
13
+ <div class="ds-modal-footer-simple">
14
+ <button class="btn btn-secondary"
15
+ onclick="navigator.clipboard.writeText(this.closest('.ds-modal').querySelector('pre').innerText)">
16
+ Kopieren
17
+ </button>
18
+ <button class="btn btn-secondary"
19
+ onclick="closeModal(this)">
20
+ Schließen
21
+ </button>
22
+ </div>
23
+
24
+ </div>
25
+ </div>