portacode 1.5.12.dev1__tar.gz → 1.5.13.dev0__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.
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/PKG-INFO +2 -1
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docker-compose.yaml +115 -0
- portacode-1.5.13.dev0/docs/device-health-alerting-plan.md +293 -0
- portacode-1.5.13.dev0/portacode/_version.py +24 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/cli.py +16 -0
- portacode-1.5.13.dev0/portacode/codex_loopback_proxy.py +176 -0
- portacode-1.5.13.dev0/portacode/codex_prepare.py +164 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/client.py +11 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/automation_v2_handlers.py +25 -1
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/session.py +1 -1
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/system_handlers.py +99 -16
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/keypair.py +15 -4
- portacode-1.5.13.dev0/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode.egg-info/PKG-INFO +2 -1
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode.egg-info/SOURCES.txt +16 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode.egg-info/requires.txt +1 -0
- portacode-1.5.13.dev0/portacode_for_school/.gitignore +4 -0
- portacode-1.5.13.dev0/portacode_for_school/README.md +76 -0
- portacode-1.5.13.dev0/portacode_for_school/persistent_workspace/Dockerfile +32 -0
- portacode-1.5.13.dev0/portacode_for_school/persistent_workspace/README.md +77 -0
- portacode-1.5.13.dev0/portacode_for_school/persistent_workspace/entrypoint.sh +68 -0
- portacode-1.5.13.dev0/portacode_for_school/persistent_workspace/manage_classroom.py +488 -0
- portacode-1.5.13.dev0/portacode_for_school/temporary_workspace/Dockerfile +32 -0
- portacode-1.5.13.dev0/portacode_for_school/temporary_workspace/README.md +93 -0
- portacode-1.5.13.dev0/portacode_for_school/temporary_workspace/docker-compose.yaml +19 -0
- portacode-1.5.13.dev0/portacode_for_school/temporary_workspace/docs/device_transfere.png +0 -0
- portacode-1.5.13.dev0/portacode_for_school/temporary_workspace/docs/pairing_approval.png +0 -0
- portacode-1.5.13.dev0/portacode_for_school/temporary_workspace/entrypoint.sh +36 -0
- portacode-1.5.13.dev0/scripts/diagnose_codex_gateway.py +259 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/setup.py +1 -0
- portacode-1.5.12.dev1/portacode/_version.py +0 -34
- portacode-1.5.12.dev1/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/.claude/settings.local.json +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/.gitignore +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/.gitmodules +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/LICENSE +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/MANIFEST.in +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/Makefile +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/backup.sh +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/build_android.sh +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/connect.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/connect.sh +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/creative-team-brief-portacode.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/devops-messaging-ab-tests.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/homepage-dashboard-positioning-fixes.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/images/device-transfer-button.png +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/images/device-transfer-modal.png +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/images/pair-device-button.png +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/images/pairing-request.png +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/images/student-workspace.png +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/docs/template-guide.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/simple_device/Dockerfile +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/simple_device/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/__main__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/proxmox_infra.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/runtime_user.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/test_runtime_user.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/multiplex.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/terminal.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/test_webmin_proxy_config.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/connection/webmin_proxy_config.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/data.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/exit_codes.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/elinks +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/links +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/links2 +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/lynx +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/w3m +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/logging_categories.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/pairing.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/restart.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/service.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/test_runtime_paths.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/test_service.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/test_terminal_exposed_services.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/cloudflared_login.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/ensure_cloudflared.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/ensure_pyyaml.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/forwarding_state.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/get_domain.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/privileged.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/service_install.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/tunneling/state.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/updater.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/utils/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/utils/diff_apply.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/utils/diff_renderer.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode/utils/runtime_paths.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/pyproject.toml +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/restore.sh +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/run_tests.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/setup.cfg +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test.sh +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_device_online.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_file_operations.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_login_flow.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_play_store_screenshots.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_request_id.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/test_session_terminal_shutdown.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/.env.example +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/README.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/cli.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/__init__.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/base_test.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/runner.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/testing_framework/requirements.txt +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/agent_context_management.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/django_server_time_sync.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/websocket_client_silently_dead.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/tools/generate_play_store_assets.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/tools/pairing_tester.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/tools/run_screenshot_suite.sh +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/tools/test_python_ntp_clock.py +0 -0
- {portacode-1.5.12.dev1 → portacode-1.5.13.dev0}/validate.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: portacode
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.13.dev0
|
|
4
4
|
Summary: Portacode CLI client and SDK
|
|
5
5
|
Home-page: https://github.com/portacode/portacode
|
|
6
6
|
Author: Meena Erian
|
|
@@ -30,6 +30,7 @@ Requires-Dist: Pygments>=2.14.0
|
|
|
30
30
|
Requires-Dist: ntplib>=0.4.0
|
|
31
31
|
Requires-Dist: importlib_resources>=6.0
|
|
32
32
|
Requires-Dist: requests>=2.31
|
|
33
|
+
Requires-Dist: httpx>=0.27
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: black; extra == "dev"
|
|
35
36
|
Requires-Dist: flake8; extra == "dev"
|
|
@@ -82,6 +82,60 @@ services:
|
|
|
82
82
|
options:
|
|
83
83
|
max-size: "50m" # rotate when log hits 50 MB
|
|
84
84
|
max-file: "3" # keep 3 old files
|
|
85
|
+
telegram-auth-worker:
|
|
86
|
+
build:
|
|
87
|
+
context: ./server/portacode_django
|
|
88
|
+
dockerfile: Dockerfile
|
|
89
|
+
container_name: portacode-telegram-auth-worker
|
|
90
|
+
restart: unless-stopped
|
|
91
|
+
command:
|
|
92
|
+
[
|
|
93
|
+
"python",
|
|
94
|
+
"manage.py",
|
|
95
|
+
"handle_telegram_auth",
|
|
96
|
+
"--category",
|
|
97
|
+
"telegram_anonymous",
|
|
98
|
+
]
|
|
99
|
+
env_file:
|
|
100
|
+
- main.env
|
|
101
|
+
depends_on:
|
|
102
|
+
db:
|
|
103
|
+
condition: service_healthy
|
|
104
|
+
volumes:
|
|
105
|
+
- ./server/portacode_django:/app
|
|
106
|
+
logging:
|
|
107
|
+
driver: json-file
|
|
108
|
+
options:
|
|
109
|
+
max-size: "50m"
|
|
110
|
+
max-file: "3"
|
|
111
|
+
|
|
112
|
+
# Multi-tenant Codex Connect + Responses gateway (ChatGPT subscription auth).
|
|
113
|
+
# Start with 1 replica; scale later if needed. Not per-user containers.
|
|
114
|
+
codex-gateway:
|
|
115
|
+
build:
|
|
116
|
+
context: ./server/portacode_django
|
|
117
|
+
dockerfile: Dockerfile.codex-gateway
|
|
118
|
+
container_name: portacode-codex-gateway
|
|
119
|
+
restart: unless-stopped
|
|
120
|
+
command: ["python", "manage.py", "run_codex_gateway", "--host", "0.0.0.0", "--port", "8080"]
|
|
121
|
+
env_file:
|
|
122
|
+
- main.env
|
|
123
|
+
environment:
|
|
124
|
+
CODEX_GATEWAY_INTERNAL_URL: http://codex-gateway:8080
|
|
125
|
+
CODEX_GATEWAY_PUBLIC_BASE_URL: "${CODEX_GATEWAY_PUBLIC_BASE_URL:-https://codexapi.portacode.com}"
|
|
126
|
+
depends_on:
|
|
127
|
+
db:
|
|
128
|
+
condition: service_healthy
|
|
129
|
+
ports:
|
|
130
|
+
- "8082:8080"
|
|
131
|
+
volumes:
|
|
132
|
+
- ./server/portacode_django:/app
|
|
133
|
+
logging:
|
|
134
|
+
driver: json-file
|
|
135
|
+
options:
|
|
136
|
+
max-size: "50m"
|
|
137
|
+
max-file: "3"
|
|
138
|
+
|
|
85
139
|
automation-worker:
|
|
86
140
|
build:
|
|
87
141
|
context: ./server/portacode_django
|
|
@@ -161,6 +215,67 @@ services:
|
|
|
161
215
|
options:
|
|
162
216
|
max-size: "50m"
|
|
163
217
|
max-file: "3"
|
|
218
|
+
alert-evaluator:
|
|
219
|
+
profiles: ["alerts"]
|
|
220
|
+
build:
|
|
221
|
+
context: ./server/portacode_django
|
|
222
|
+
dockerfile: Dockerfile
|
|
223
|
+
container_name: portacode-alert-evaluator
|
|
224
|
+
restart: unless-stopped
|
|
225
|
+
command: ["python", "manage.py", "evaluate_alert_rules", "--run-forever", "--interval", "30"]
|
|
226
|
+
env_file:
|
|
227
|
+
- main.env
|
|
228
|
+
depends_on:
|
|
229
|
+
db:
|
|
230
|
+
condition: service_healthy
|
|
231
|
+
volumes:
|
|
232
|
+
- ./server/portacode_django:/app
|
|
233
|
+
logging:
|
|
234
|
+
driver: json-file
|
|
235
|
+
options:
|
|
236
|
+
max-size: "50m"
|
|
237
|
+
max-file: "3"
|
|
238
|
+
alert-notifier:
|
|
239
|
+
profiles: ["alerts"]
|
|
240
|
+
build:
|
|
241
|
+
context: ./server/portacode_django
|
|
242
|
+
dockerfile: Dockerfile
|
|
243
|
+
restart: unless-stopped
|
|
244
|
+
command: ["python", "manage.py", "run_alert_notification_workers", "--poll-interval", "2"]
|
|
245
|
+
env_file:
|
|
246
|
+
- main.env
|
|
247
|
+
depends_on:
|
|
248
|
+
db:
|
|
249
|
+
condition: service_healthy
|
|
250
|
+
volumes:
|
|
251
|
+
- ./server/portacode_django:/app
|
|
252
|
+
deploy:
|
|
253
|
+
replicas: 2
|
|
254
|
+
logging:
|
|
255
|
+
driver: json-file
|
|
256
|
+
options:
|
|
257
|
+
max-size: "50m"
|
|
258
|
+
max-file: "3"
|
|
259
|
+
alert-metrics-rollup:
|
|
260
|
+
profiles: ["alerts"]
|
|
261
|
+
build:
|
|
262
|
+
context: ./server/portacode_django
|
|
263
|
+
dockerfile: Dockerfile
|
|
264
|
+
container_name: portacode-alert-metrics-rollup
|
|
265
|
+
restart: unless-stopped
|
|
266
|
+
command: ["python", "manage.py", "rollup_device_metrics", "--run-forever", "--interval", "3600"]
|
|
267
|
+
env_file:
|
|
268
|
+
- main.env
|
|
269
|
+
depends_on:
|
|
270
|
+
db:
|
|
271
|
+
condition: service_healthy
|
|
272
|
+
volumes:
|
|
273
|
+
- ./server/portacode_django:/app
|
|
274
|
+
logging:
|
|
275
|
+
driver: json-file
|
|
276
|
+
options:
|
|
277
|
+
max-size: "50m"
|
|
278
|
+
max-file: "3"
|
|
164
279
|
reacher:
|
|
165
280
|
image: reacherhq/backend:latest
|
|
166
281
|
env_file:
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Device Health Alerting — Implementation Plan
|
|
2
|
+
|
|
3
|
+
Status: draft for review
|
|
4
|
+
Owner: TBD
|
|
5
|
+
Related research agent runs: [Codebase architecture](09a64206-a23c-4952-acf1-84c200c28711), [unicom/queue patterns](43eead86-e1c1-4c1e-b969-17e24d68d567)
|
|
6
|
+
|
|
7
|
+
## 1. Why, and what "good" looks like
|
|
8
|
+
|
|
9
|
+
Portacode's agent already collects CPU, RAM, and disk usage every 10s and knows connect/disconnect events in real time. None of this is used proactively today — it's a snapshot that overwrites itself (`Device.system_info`). This plan turns that gold mine into a real alerting product feature.
|
|
10
|
+
|
|
11
|
+
Market research (Datadog, Zabbix, Netdata, PRTG, Uptime Kuma, PagerDuty/Opsgenie, homelab communities — see §2) converges on the same lessons, which double as our design constraints:
|
|
12
|
+
|
|
13
|
+
1. **Sustained-duration thresholds, not instant spikes** — the #1 cause of alert fatigue is alerting on transient blips. CPU/RAM need a "how long" component; disk is the one exception (it doesn't self-resolve, so a high static threshold firing instantly is correct).
|
|
14
|
+
2. **Actionable or don't send it** — every alert should let the recipient *do something* in one click (snooze, disable, adjust threshold), not just inform.
|
|
15
|
+
3. **Root-cause beats raw numbers** — "disk is at 92%" is much less useful than "disk is at 92% because `docker/nginx/logs` has grown 2.1GB/week for 6 months." Nobody in this market does this well out of the box; it's a genuine differentiator for Portacode because we already have a live agent on the box.
|
|
16
|
+
4. **Full history, not just current state** — every tool that people trust logs when an alert fired, when it resolved, and why, so it can be reviewed later and tuned.
|
|
17
|
+
5. **Respect user trust in what leaves their machine** — several existing Portacode users may run sensitive workloads; diagnostic detail (file/folder names, future command output) needs a privacy-conscious default, not just a "fewer alerts" one.
|
|
18
|
+
|
|
19
|
+
## 2. Market scan summary
|
|
20
|
+
|
|
21
|
+
| Tool | Strength we should copy | Weakness / complaint we should avoid |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Datadog | Rich context in alert payload (variables, tags), Monitor Quality dashboard to find noisy alerts | Alert storms from tightly-coupled monitors; expensive; steep configuration curve |
|
|
24
|
+
| Zabbix / Prometheus+Grafana | Free, deeply configurable, industry-standard thresholds | Heavy to run (1–8GB RAM), requires real expertise to configure well — "easy to configure" is explicitly *not* their reputation |
|
|
25
|
+
| Netdata | Zero-config, per-second granularity, built-in anomaly detection | No first-class actionable notifications; alerting is a secondary feature |
|
|
26
|
+
| Uptime Kuma | Dead simple config, good default alert set, many channels | Uptime-only (no resource metrics), no root-cause detail, no tolerance/duration logic beyond simple retries |
|
|
27
|
+
| PagerDuty / Opsgenie | One-click Ack/Snooze/Mute directly in the notification (email/push/chat) — exactly the "quick action buttons" the user asked for | Enterprise pricing/complexity, overkill for a single device |
|
|
28
|
+
| General homelab/SRE consensus | Sustained-duration windows (CPU/RAM 10–15 min, disk instant at ~90-95%), recovery notifications, inhibition/grouping to avoid duplicate noise | — |
|
|
29
|
+
|
|
30
|
+
**Recommended default thresholds (matches both market convention and the user's own spec):**
|
|
31
|
+
|
|
32
|
+
| Metric | Warning | Critical / instant |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Disk usage | ≥ 90% sustained 10 min | ≥ 95% instant (no sustain window — it won't resolve itself) |
|
|
35
|
+
| CPU usage | ≥ 85% sustained 15 min | ≥ 95% sustained 5 min |
|
|
36
|
+
| RAM usage | ≥ 85% sustained 15 min | ≥ 95% sustained 5 min |
|
|
37
|
+
| Connectivity | offline > 5 min (configurable) | — (no separate critical tier at launch) |
|
|
38
|
+
|
|
39
|
+
All of the above ship pre-enabled as **system default rules** applied to every device, editable/disable-able per device, exactly mirroring the "storage > 90% for 10 min, or 95% instantly" example in the brief.
|
|
40
|
+
|
|
41
|
+
## 3. Architecture decisions (confirmed)
|
|
42
|
+
|
|
43
|
+
These came out of a discussion with the user and are now settled; they drive every phase below.
|
|
44
|
+
|
|
45
|
+
### 3.1 Where data lives — hybrid, split by sensitivity and by what needs central evaluation
|
|
46
|
+
|
|
47
|
+
- **Numeric health metrics + alert rules + alert history**: centrally, in the existing **Postgres** database. Rationale: disconnect-based alerts inherently *require* server-side state (an offline device cannot report its own disconnection), so the server is already the unavoidable source of truth for evaluation. Piggybacking rules/metrics on the same DB avoids new infrastructure and this data is small (a handful of numbers per device per interval).
|
|
48
|
+
- **Root-cause diagnostic detail (disk breakdown, and later: scheduled-command output)**: generated and cached **on the device** by the agent. Rationale (from user's own concern, which is valid): file/folder paths and command output can be sensitive or bulky, and users on the centralized SaaS may not want that leaving their machine by default.
|
|
49
|
+
- Only a **small, bounded summary** ("`/var/lib/docker/containers/<id>/logs` grew 2.1GB over 7 days") gets attached to the specific `AlertEvent` when it fires and is persisted centrally — that's the minimum needed to make the notification useful and to keep permanent history.
|
|
50
|
+
- The **full breakdown** (every folder/file considered) is never bulk-synced or stored centrally. It's fetched **live on demand** through the same WebSocket command channel already used for `system_info`/terminals, when the user opens the "why did this happen" view and the device is online — the same pattern Portacode already uses for terminals, so no new transport is needed.
|
|
51
|
+
- This becomes a documented, user-facing **privacy setting** later ("Diagnostic detail: keep on device (default) / include in cloud history") — schema supports it from day one (`AlertEvent.detail_summary` is intentionally a short text/JSON field, never a dump).
|
|
52
|
+
- Self-hosted deployments can be advised that this distinction is moot for them (it's their own Postgres either way), but the mechanism stays identical, so we don't fork behavior by deployment mode.
|
|
53
|
+
|
|
54
|
+
### 3.2 Background processing — reuse the existing Postgres `select_for_update(skip_locked=True)` pattern, no new infra
|
|
55
|
+
|
|
56
|
+
No Celery/Redis today; the codebase already has a proven convention (`AutomationTask`, `cleanup_expired_devices`, `notify_expiring_devices`) for both queue-style claiming and continuous "run-forever" sweeps. We copy it exactly rather than introducing new infrastructure:
|
|
57
|
+
|
|
58
|
+
- **Rule evaluation**: a continuous sweep management command (same shape as `disk_usage_monitor_runner` / `notify_expiring_devices --run-forever`), not a per-item queue — it periodically walks active rules + latest metrics and decides state transitions.
|
|
59
|
+
- **Notification delivery**: a claim-based queue table (`AlertNotification`, modeled field-for-field on `AutomationTask`'s `status` / `worker_uuid` / `claimed_at` pattern) so sending (SMTP/Telegram API calls) gets retries, backoff, and concurrency for free, and doubles as the delivery history ledger.
|
|
60
|
+
- This identical pattern is what we'll extend later for scheduled recurring diagnostic commands (§8), so the investment compounds.
|
|
61
|
+
|
|
62
|
+
### 3.3 Channels — built on `unicom`, not reinvented
|
|
63
|
+
|
|
64
|
+
- `unicom` already supports Email, Telegram, WhatsApp, WebChat as first-class channels with a polymorphic `channel.send_message()` and reply/callback plumbing. Portacode-side code should be **channel-agnostic**: an alert rule references an abstract "notification target" (a `unicom.Account` + platform), and delivery goes through `unicom`'s existing `send_message` router — exactly how `unicrm.send_red_alert` already does it for staff alerts.
|
|
65
|
+
- **Critical guardrail (explicitly required by the user)**: a channel must never be selectable/relied upon unless the user has actually authenticated it. Before showing Telegram/WhatsApp/etc. as an option, or before dispatching to one, we check `Account.objects.filter(member__user=user, platform=X, blocked=False, channel__active=True).exists()`. If a previously-linked channel becomes unavailable (unlinked, blocked), the rule must be surfaced as "degraded" in the UI (e.g. "Telegram alerts are configured but Telegram isn't connected — connect it or alerts for this channel won't be delivered") rather than silently failing.
|
|
66
|
+
- WebChat is excluded from alert delivery (no notification/offline delivery support). Future native mobile push will most likely be added as a new `unicom` platform (decision deferred, but the abstraction here doesn't care — it's just another platform string).
|
|
67
|
+
- Email ships first (phase 1) since it needs zero additional user setup; Telegram is the natural phase-2 addition given it's nearly free to wire up.
|
|
68
|
+
|
|
69
|
+
### 3.4 Quick actions in notifications — reuse `unicom`'s existing primitives, don't reinvent
|
|
70
|
+
|
|
71
|
+
- **Email**: signed, expiring tokens via `django.core.signing` (`signing.dumps(payload, salt=...)`), exactly the pattern already used for unsubscribe links and demo-claim links. No login required to act — this is the accepted industry pattern (PagerDuty/Opsgenie/unsubscribe links all work this way).
|
|
72
|
+
- **Telegram**: `unicom`'s existing `CallbackExecution` + inline-keyboard + signal (`telegram_callback_received`) system — already built for exactly this ("tool_call" confirm/cancel buttons already do this for terminal commands).
|
|
73
|
+
- **Portacode-side abstraction**: a single channel-agnostic action executor (e.g. `execute_alert_action(rule_id, event_id, action, actor)` supporting `disable_rule`, `snooze(duration)`, `adjust_threshold(new_value)`, `acknowledge`) that both the email-link view and the Telegram callback handler call into. Adding a new channel later means only building the channel-specific *trigger* (a link vs. a button), never touching the action logic itself.
|
|
74
|
+
|
|
75
|
+
## 4. Data model (introduced across phases, shown together for coherence)
|
|
76
|
+
|
|
77
|
+
Rules and their actions are modeled as a **template/instance split**, not a flat table — this is what makes them a pluggable, shareable library rather than hardcoded rows (see §4.1 for the reasoning).
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
AlertRuleTemplate -- the pluggable, shareable "kind of rule" (the library entry)
|
|
81
|
+
id, slug, name, description
|
|
82
|
+
visibility enum: system | public | private
|
|
83
|
+
owner FK User, null for system/public
|
|
84
|
+
forked_from FK self, null -- lineage when a user copies a public/system template into their own
|
|
85
|
+
kind enum: metric_threshold | connectivity | command_result (phase 4) | composite (later)
|
|
86
|
+
metric enum: cpu | ram | disk | connectivity | ... (used when kind=metric_threshold/connectivity)
|
|
87
|
+
default_comparator, default_threshold_value, default_sustain_seconds, default_severity
|
|
88
|
+
compatibility_expression small sandboxed expression string (§4.2) — empty = universal, matches every device
|
|
89
|
+
evaluation_stages array of enum (§4.3): which onboarding/lifecycle stage(s) this template should be
|
|
90
|
+
(re-)considered at, e.g. [on_pairing_registered] for the universal system defaults,
|
|
91
|
+
[on_first_system_info] for anything OS/capability-filtered
|
|
92
|
+
default_application_mode enum: auto_apply | recommend (§4.3) — what happens on a compatibility match
|
|
93
|
+
recommendation_reason short author-provided explanation text shown on "recommended" cards
|
|
94
|
+
is_reviewed bool (moderation flag, relevant once/if public submissions open up — §9)
|
|
95
|
+
source_one_click_template FK devops template, null — set when this template was generated from a
|
|
96
|
+
one-click template's `alerts:` section (§4.3), for traceability
|
|
97
|
+
created_at, updated_at
|
|
98
|
+
|
|
99
|
+
AlertActionTemplate -- pluggable action bundled to a rule template
|
|
100
|
+
id, rule_template (FK)
|
|
101
|
+
key e.g. disable_rule | snooze | adjust_threshold | acknowledge | run_command (phase 4)
|
|
102
|
+
label, requires_confirmation
|
|
103
|
+
params_schema JSON (e.g. snooze duration options; command template + args in phase 4)
|
|
104
|
+
compatibility_expression same sandboxed expression language, null = inherit rule template's expression
|
|
105
|
+
(e.g. a "restart container" action can be *more* restrictive than its parent
|
|
106
|
+
disk-alert rule, which is universal)
|
|
107
|
+
|
|
108
|
+
AlertRule -- an activation of a template: per-device, or a user's default profile entry
|
|
109
|
+
id, template (FK AlertRuleTemplate), user (FK), device (FK, null = user-level default profile entry)
|
|
110
|
+
comparator, threshold_value, sustain_seconds, severity -- override; null = inherit template default
|
|
111
|
+
enabled, snoozed_until
|
|
112
|
+
notify_channels JSON list of {platform, account_id} — validated against authenticated Accounts at save + send time
|
|
113
|
+
created_at, updated_at
|
|
114
|
+
|
|
115
|
+
AlertRuleRecommendation -- durable "we suggested this, don't ask again" record (§4.3)
|
|
116
|
+
id, device (FK), template (FK AlertRuleTemplate), stage (enum, which evaluation_stage triggered it)
|
|
117
|
+
status enum: pending | applied | dismissed
|
|
118
|
+
matched_at, resolved_at
|
|
119
|
+
reason snapshot of recommendation_reason at match time (kept even if template text changes later)
|
|
120
|
+
|
|
121
|
+
AlertEvent -- the permanent history ledger
|
|
122
|
+
id, rule (FK AlertRule), device (FK)
|
|
123
|
+
state enum: triggered | resolved | acknowledged
|
|
124
|
+
triggered_at, resolved_at, acknowledged_at
|
|
125
|
+
metric_value_at_trigger
|
|
126
|
+
detail_summary short text/JSON — root-cause hint, bounded size, generated on-device (§3.1)
|
|
127
|
+
created_at
|
|
128
|
+
|
|
129
|
+
AlertNotification -- delivery queue + history, mirrors AutomationTask's claim pattern
|
|
130
|
+
id, event (FK), platform, account (FK to unicom.Account)
|
|
131
|
+
status enum: pending | sending | sent | failed
|
|
132
|
+
worker_uuid, claimed_at, sent_at, error
|
|
133
|
+
retry_count
|
|
134
|
+
|
|
135
|
+
DeviceMetricSample -- new: turns the "snapshot only" gap into real history
|
|
136
|
+
id, device (FK), sampled_at
|
|
137
|
+
cpu_percent, ram_percent, disk_percent
|
|
138
|
+
(rollup/retention job keeps this bounded — see §5.4)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This directly satisfies the brief's requirement to "keep track of full history" — `AlertEvent` is never overwritten, and `AlertNotification` gives per-channel delivery audit trail for free via the same mechanism that provides retries.
|
|
142
|
+
|
|
143
|
+
### 4.1 Why template/instance, not a flat table
|
|
144
|
+
|
|
145
|
+
A flat `AlertRule` table (my original phase-1 sketch) can't represent "a reusable, shareable definition that may or may not apply to a given device." Splitting it means:
|
|
146
|
+
|
|
147
|
+
- **Your own library**: any `AlertRuleTemplate` with `visibility=private, owner=you` — create from scratch, or "Add to my library" from the public/system catalog, which **copies** the template (`forked_from` set for lineage) rather than linking live. Copy-not-link is the deliberate default: it matches how Grafana dashboards / n8n workflow templates / Zapier "zap templates" behave, so editing your copy never surprises you with upstream changes, and a later official update to the public original doesn't silently alter something already running on a device.
|
|
148
|
+
- **The public library**: curated `visibility=public` templates (a handful of premade, genuinely useful examples — the same four defaults in §2, plus more as phases 3/4 unlock command-based ones). For now this is **Portacode-curated only** (staff-authored, `is_reviewed=True`); open community submission/publishing is a real feature but also a real security surface once command-based actions exist (see §9) — deferring that decision rather than baking in an unreviewed marketplace from day one.
|
|
149
|
+
- **Your profile's default alerts**: `AlertRule` rows with `device=null` — literally the same row shape used for a real per-device rule, just not yet bound to a device. At each relevant lifecycle stage (§4.3) for a device — pairing, first `system_info`, provisioning completion — we diff the user's `auto_apply` default-profile rules against the device's compatibility expression (§4.2) and auto-create matching per-device `AlertRule` rows; `recommend`-mode ones become `AlertRuleRecommendation`s instead. This is exactly the "filters based on the OS of the new device" behavior requested, applied at the point in the device's lifecycle where the relevant data actually exists.
|
|
150
|
+
- **Actions inherit the same shape**: most rules only ever need the four generic, universally-compatible actions (disable/snooze/adjust threshold/acknowledge), but a future template (e.g. "Docker container stuck restarting") can bundle a command-based action ("restart container") that's only compatible with devices where Docker is detected — without needing a different schema.
|
|
151
|
+
|
|
152
|
+
### 4.2 Compatibility expression — concise, code-like, but safely sandboxed
|
|
153
|
+
|
|
154
|
+
Both a nested JSON condition tree and free-text OS matching turned out to be the wrong shape: JSON is verbose for anything beyond one or two conditions, and exact-string OS matching is the *binary matching* the user explicitly wants to avoid. What we actually want is a small **expression language** — genuinely code-like and far more concise than JSON — evaluated through a **sandboxed interpreter that only understands a tiny, fixed grammar**, so a public/shared template can never carry arbitrary logic. This is the same approach behind Ansible's `when:` conditions, GitHub Actions' `if:` expressions, and (more formally) Google's CEL — small boolean expression languages purpose-built for "safe rule evaluation," not general-purpose scripting.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
os_family == "debian" and "docker" in capabilities
|
|
158
|
+
arch in ["x86_64", "arm64"] and version_gte(os_version_id, "20.04")
|
|
159
|
+
"docker" in capabilities or "podman" in capabilities
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
An empty string always matches (the common case today: CPU/RAM/disk/connectivity are collected identically on every OS `psutil` supports, so the four default templates ship with an empty expression).
|
|
163
|
+
|
|
164
|
+
**Implementation — a custom AST allowlist, not `eval()`/a template engine, and no new dependency:** parse with Python's `ast.parse(expr, mode="eval")`, then walk the tree and reject anything outside a fixed allowlist before ever evaluating it:
|
|
165
|
+
|
|
166
|
+
- Allowed nodes: `BoolOp` (`and`/`or`), `UnaryOp` (`not`), `Compare` (`==`, `!=`, `in`, `not in`, `<`, `<=`, `>`, `>=`), `Name` (only names present in the device-profile context — no attribute/subscript access, no builtins), `Constant` (str/int/float/bool), `List`/`Tuple` literals, and `Call` — but only for a fixed, explicitly whitelisted function registry (`version_gte`, `version_lte`, `version_gt`, `version_lt` for semantic-version-aware comparisons that plain string comparison gets wrong).
|
|
167
|
+
- Everything else (attribute access, subscripting, comprehensions, lambdas, imports, arbitrary calls, dunder names) is rejected at parse-time, before execution — so there is no code-injection surface even for fully public, user-authored templates.
|
|
168
|
+
- Expression length and AST depth are capped (defensive, not because the sandbox is unsafe otherwise) so a pathological expression can't cause pathological evaluation cost.
|
|
169
|
+
- This is intentionally hand-rolled rather than pulling in `simpleeval` or `cel-python`: the grammar we need is tiny, and keeping it in-house keeps the entire security-relevant surface auditable in one small module rather than trusting a third-party library's sandbox guarantees.
|
|
170
|
+
|
|
171
|
+
**Device profile fields available in the expression context** (new, added to `Device` + populated by the agent):
|
|
172
|
+
|
|
173
|
+
- `os_family` — a normalized family tag (`debian`, `rhel`, `arch`, `darwin`, `windows`, …), not a display string. Linux already self-describes this via `/etc/os-release`'s `ID` + `ID_LIKE` fields (e.g. Ubuntu declares `ID=ubuntu, ID_LIKE=debian`) — the agent's existing `_get_os_info()` only keeps a human-readable `os_version` string today (`system_handlers.py:413-477`); this phase extends it to also emit `os_family`/`os_id`/`os_version_id` so filtering is family-tree-aware ("debian-family" matches Ubuntu, Mint, Debian, etc.) instead of exact-string matching.
|
|
174
|
+
- `arch` — already collected (`platform.machine()`).
|
|
175
|
+
- `capabilities` — a small opportunistically-detected tag list (`docker`, `systemd`, `apt`, `yum`, `cgroup_v2`, …), extensible over time; only populated for what a given phase actually needs (an unlisted capability just fails an `in` check, correctly excluding that template rather than erroring).
|
|
176
|
+
|
|
177
|
+
This is a small, additive agent change (independent of phase 3's heavier disk-scanning release) and is worth shipping in the phase 1 agent bump since the schema depends on it existing.
|
|
178
|
+
|
|
179
|
+
**UI note:** most users shouldn't need to type expressions at all — the library builder UI (phase 2) offers a guided field/operator/value picker for the common cases (which happens to compile to exactly this expression syntax under the hood), with a "advanced/raw expression" toggle for power users. The stored, authoritative form is always the expression string; the guided UI is just one way to produce it.
|
|
180
|
+
|
|
181
|
+
### 4.3 Multi-stage evaluation — rules get (re-)considered at the right point in a device's lifecycle
|
|
182
|
+
|
|
183
|
+
Devices enter the system through genuinely different paths with different data-availability timelines (confirmed in the codebase, not assumed):
|
|
184
|
+
|
|
185
|
+
| Path | What's known, and when | Reference |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| **Pairing** | Ownership assigned immediately (`_register_paired_device()`), but OS/arch/capabilities are **not** known yet — the first real `system_info` only arrives after the device establishes its live connection and a dashboard client session triggers it, which can be seconds to indefinitely later | `pairing_dashboard_consumer.py:111-138`, `device_gateway_consumer.py:628-634`, `terminal.py:909-924` |
|
|
188
|
+
| **One-click template provisioning** | OS is actually *known upfront* (the template picks `source_template`), but the thing an alert would care about (e.g. "is the app actually running") is only true after the template's `instructions` finish — i.e. `AutomationTask` success, later than bare VM creation (`proxmox_container_created`) | `device_gateway_consumer.py:656-673`, `run_automation_workers.py:~1784` |
|
|
189
|
+
| **Manual key / CLI connect without pairing** | Similar to pairing: ownership known immediately, OS known only once connected | `device_views.py:162-195`, `cli.py:248-268` |
|
|
190
|
+
|
|
191
|
+
Given this, `AlertRuleTemplate.evaluation_stages` names the specific lifecycle moment(s) a template becomes eligible for (re-)evaluation against a device, rather than assuming one global "check compatibility now" moment:
|
|
192
|
+
|
|
193
|
+
- **`on_pairing_registered`** — fires right after ownership is assigned, before OS info exists. Only meaningful for templates with an empty/OS-independent expression — this is exactly where the four universal system defaults (disk/CPU/RAM/connectivity) get applied, unchanged from the original phase 1 design, just now named precisely.
|
|
194
|
+
- **`on_first_system_info`** — fires the first time a device's `system_info` arrives and `os_family`/`capabilities` become known (detected as a one-time transition, not re-fired on every 10s push). This is where OS/capability-filtered templates (e.g. a Docker-only template) get their first real shot at matching.
|
|
195
|
+
- **`on_provisioning_complete`** — fires when a one-click template's post-provision `instructions` finish successfully (`AutomationTask` success), scoped **only to the device just deployed by that template** — this is where a template's own bundled `alerts:` entries (below) get evaluated, not the general public/private library.
|
|
196
|
+
- **`on_profile_change`** (deferred per §9, but now a named stage rather than an ad-hoc idea) — re-runs `on_first_system_info`-equivalent matching if a device's detected OS/capabilities change later (e.g. Docker gets installed after initial registration).
|
|
197
|
+
- **`manual`** — a user can always browse the library and apply any compatible template to a device explicitly, regardless of stage.
|
|
198
|
+
|
|
199
|
+
**What happens on a match — `auto_apply` vs `recommend`:**
|
|
200
|
+
|
|
201
|
+
- **`auto_apply`**: immediately instantiates a real `AlertRule` for the device. This stays the behavior for the four system defaults (matches the brief's "certain alerts should be active by default").
|
|
202
|
+
- **`recommend`**: creates an `AlertRuleRecommendation` row instead of silently applying anything, surfaced in the dashboard as a card — name, `recommendation_reason`, and **Apply** / **Dismiss** actions. Once dismissed, that template is never re-recommended for that device (the same anti-spam philosophy as the alerts themselves — recommendations shouldn't nag either). A user can also promote any template from their own or the public library into their **default profile** as `auto_apply`, meaning "apply this automatically to every future compatible device without asking me" — this is what "define default alerts on your profile" concretely means now that recommend/auto_apply are distinct.
|
|
203
|
+
- Public/private templates default to `recommend` unless explicitly promoted; system defaults ship as `auto_apply`.
|
|
204
|
+
|
|
205
|
+
**One-click templates defining their own alerts.** The `portafile.yaml` schema (`content/one-click/*.md`, served via `devops/views.py:one_click_portafile_yaml()`) gains an optional top-level `alerts:` list, sitting alongside the existing `instructions`/`expose_ports` keys:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
alerts:
|
|
209
|
+
- template: docker-log-growth # reference an existing public/system template by slug, with overrides
|
|
210
|
+
mode: recommend
|
|
211
|
+
- name: Uptime Kuma unreachable # or define one inline, scoped to this one-click template
|
|
212
|
+
metric: connectivity
|
|
213
|
+
threshold: n/a
|
|
214
|
+
mode: auto_apply
|
|
215
|
+
reason: "Uptime Kuma's entire purpose is uptime monitoring, so we watch it by default."
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Inline entries get materialized into `AlertRuleTemplate(visibility=system, source_one_click_template=<template>)` rows the first time the template is synced (mirrors how `sync_content.py` already syncs the rest of the YAML), and are evaluated at `on_provisioning_complete` for the specific device that just ran that template — never applied to unrelated devices.
|
|
219
|
+
|
|
220
|
+
## 5. Phase 1 — Foundations: metric history, default rules, email alerts, full history (MVP)
|
|
221
|
+
|
|
222
|
+
**Goal at the end of this phase: a user can see their device's health history and receives an email — with working one-click actions — for the default disk/CPU/RAM/connectivity rules, without configuring anything.** This is the smallest slice that is genuinely useful end-to-end.
|
|
223
|
+
|
|
224
|
+
1. **Server models**: `AlertRuleTemplate`, `AlertActionTemplate`, `AlertRule`, `AlertRuleRecommendation`, `AlertEvent`, `AlertNotification`, `DeviceMetricSample` (§4, minus disk root-cause fields, added in phase 3). Building the template/instance split now — even though only four universal, non-command templates exist at this point — avoids a schema migration later; it's barely more work than a flat table since the four defaults all have an empty `compatibility_expression` anyway. `AlertRuleRecommendation` is created now but stays unused until phase 2 (nothing in phase 1 is `recommend`-mode).
|
|
225
|
+
2. **Device profile fields**: add `os_family`, `os_id`, `os_version_id`, `capabilities` to `Device`; extend the agent's `_get_os_info()` to emit them from `/etc/os-release` (`ID`/`ID_LIKE`) instead of only a display string (§4.2). Small, additive, low-risk agent change — bundle it into this phase's agent release rather than waiting for phase 3's heavier disk-scanning release.
|
|
226
|
+
3. **Compatibility expression evaluator**: implement the sandboxed AST-allowlist interpreter from §4.2 as a standalone, unit-testable module (no Django dependency) — this is a security-relevant piece worth building and testing in isolation before anything calls into it.
|
|
227
|
+
4. **Metric history capture**: on each `system_info` persist in `DeviceGatewayConsumer._persist_device_system_info_snapshot`, also write a `DeviceMetricSample` row (cheap; one row per device per ~10s while connected). Add a retention/rollup job (mirrors existing cron-style management commands) that downsamples samples older than N days (e.g. keep raw for 48h, 5-min averages for 30 days, hourly averages beyond that) so the table doesn't grow unbounded — this is the "prepared for full history" requirement without needing TimescaleDB yet.
|
|
228
|
+
5. **Seed the four system templates** (disk/CPU/RAM/connectivity from §2's table) as `AlertRuleTemplate(visibility=system, evaluation_stages=[on_pairing_registered], default_application_mode=auto_apply)` rows with matching `AlertActionTemplate`s for the four generic actions (disable/snooze/adjust_threshold/acknowledge, all universally compatible). A data migration creates each existing user's default-profile `AlertRule` rows (`device=null`) referencing these templates, and per-device `AlertRule` rows for every existing device. New devices get the same auto-instantiation wired into `_register_paired_device()` (§4.3's `on_pairing_registered` hook) — trivial at this phase since every template is universal and pairing already has everything needed, but this is exactly the hook phase 2's OS-filtered examples will reuse for real at a later stage.
|
|
229
|
+
6. **Connectivity tracking hook**: reuse existing `_broadcast_device_status` connect/disconnect signal path to feed the evaluator instead of building a new detector.
|
|
230
|
+
7. **Evaluator** (`evaluate_alert_rules --run-forever` management command, modeled on `disk_usage_monitor_runner`): every ~30s, for each enabled `AlertRule`, resolves effective threshold/sustain/severity (override, else template default), checks the relevant `DeviceMetricSample` window (or connectivity state), creates/transitions `AlertEvent` rows (open → resolved), and enqueues `AlertNotification` rows for each configured channel. Snoozed/disabled rules are skipped but still logged as suppressed in a debug counter (useful later for a "why didn't I get notified" view).
|
|
231
|
+
8. **Notification worker** (`run_alert_notification_workers`, modeled field-for-field on `run_automation_workers`'s claim loop): claims pending `AlertNotification` rows, renders and sends via `unicom`'s email channel (reusing `unicom.services.email` / the same abstraction `send_red_alert` and system emails already use), marks sent/failed with retry+backoff.
|
|
232
|
+
9. **Email template** with one-click actions: "Disable this alert" / "Snooze 1h / 24h" / "Increase threshold to X%" — signed links (`django.core.signing`, dedicated salt e.g. `data.alert_action`) pointing at a small public view that validates the token and calls the shared `execute_alert_action()`, then shows a plain confirmation page. A resolved notification is sent too ("Disk usage back to normal") so users aren't left wondering — addresses the "recovery notification" best practice from §2.
|
|
233
|
+
10. **Dashboard UI (minimal but real)**:
|
|
234
|
+
- Per-device "Alerts" tab: list of active `AlertRule`s (defaults pre-populated, editable threshold/sustain/enabled/snooze), and an event history table (triggered/resolved timestamps, severity).
|
|
235
|
+
- A simple sparkline/small history chart per metric using `DeviceMetricSample`, addressing the existing gap that health % badges have no trend view today.
|
|
236
|
+
- No library browsing UI yet (phase 2) — rule editing at this stage only tweaks the override fields on existing instances.
|
|
237
|
+
11. **Guardrails baked in from day 1**: channel-authentication check before allowing a channel to be selected (§3.3) — at phase 1 this just means "email is always available since it's the user's account email," but the check function is written generically so phase 2 (Telegram) plugs in without touching rule-save logic.
|
|
238
|
+
|
|
239
|
+
**Exit criteria (testable):** disconnect a real device for >5 min → email fires with correct copy and working action buttons; fill a test disk to 90%+ for 10 min → warning email; hit 95% → instant email; clicking "disable" in the email actually disables the rule and stops further emails; dashboard shows rule list + event history + a metric trend chart; a device's `os_family`/`capabilities` are visible on the `Device` record (even though nothing filters on them yet).
|
|
240
|
+
|
|
241
|
+
## 6. Phase 2 — Multi-channel delivery + real configurability
|
|
242
|
+
|
|
243
|
+
**Goal: users can add Telegram (and any other already-authenticated unicom channel) as an alert destination, tune tolerance per rule, and the system clearly guards against depending on unauthenticated channels.**
|
|
244
|
+
|
|
245
|
+
1. Channel picker UI queries authenticated `Account`s for the user (§3.3 check) per platform; unauthenticated platforms show a "Connect Telegram" CTA instead of being silently selectable.
|
|
246
|
+
2. Telegram delivery: notification worker gains a Telegram sender using `unicom`'s existing send/callback stack; action buttons become native Telegram inline keyboards via `CallbackExecution` (§3.4) instead of links.
|
|
247
|
+
3. Per-rule notify-channel selection (a rule can go to email only, Telegram only, or both) plus a user-level default channel preference so new default rules don't need per-rule setup.
|
|
248
|
+
4. "Degraded rule" indicator when a rule's configured channel becomes unauthenticated (e.g., user disconnects Telegram later) — matches the explicit requirement to never let users silently rely on something not actually connected.
|
|
249
|
+
5. Tolerance/threshold self-tuning helpers: since the research consistently says thresholds should reflect actual baseline behavior, show the device's own historical p95 CPU/RAM (from `DeviceMetricSample`) next to the threshold input in the UI ("your device typically peaks at 62% — consider a threshold above that") — this is a cheap, high-value differentiator vs. competitors that just show a blank input box.
|
|
250
|
+
6. Connectivity-tolerance becomes explicitly configurable per device (not just a global 5 min default), directly addressing the "if every disconnect sends a warning, it's spammy" concern.
|
|
251
|
+
7. **"My Library" management UI**: create/edit/delete your own `AlertRuleTemplate`s (`visibility=private`) from scratch — pick a `kind`/metric, defaults, and (optionally) a compatibility condition via a guided field/operator/value builder (compiling to the §4.2 expression string), with a raw-expression toggle for power users.
|
|
252
|
+
8. **Public Library browse UI**: the Portacode-curated catalog (still just the four defaults plus maybe a couple more non-command examples at this point), each card showing a compatibility badge resolved against the currently-selected device ("Compatible" / "Requires Debian-based OS" / "Requires Docker"). "Add to my library" copies the template into the user's private library (`forked_from` set) — never a live link — per §4.1.
|
|
253
|
+
9. **`on_first_system_info` hook**: wire the one-time OS/capability-detection transition (§4.3) into the same instantiation path used at pairing — this is what lets OS-filtered templates (phase 3's Docker example) actually get applied/recommended once real device data exists.
|
|
254
|
+
10. **Recommendation engine + UI**: `recommend`-mode matches create `AlertRuleRecommendation` rows surfaced as dashboard cards (name, `recommendation_reason`, Apply/Dismiss); dismissed recommendations are never re-shown for that device. "Promote to my default profile as auto-apply" lets a user turn any template (their own or public) into something that's silently applied to every future compatible device — this is what "default alerts on your profile" means now that recommend/auto-apply are distinct concepts.
|
|
255
|
+
11. Applying a library template to a device (or to your default profile) is the same "instantiate `AlertRule` from `AlertRuleTemplate`" code path used by auto-instantiation in phase 1 — this phase is mostly UI + the recommendation layer on top of already-working plumbing.
|
|
256
|
+
|
|
257
|
+
**Exit criteria:** a user can connect Telegram, assign it to a rule, receive and act on a Telegram alert with inline buttons; disconnect Telegram and see the rule marked degraded without alerts silently vanishing unexplained; a user can write their own rule template with a guided compatibility condition, save it to their private library, and apply it to a device; browsing the public library and hovering an incompatible example clearly explains why it's greyed out for the selected device; a recommendation appears on a real device once its OS/capabilities are known, and dismissing it makes it not reappear.
|
|
258
|
+
|
|
259
|
+
## 7. Phase 3 — Root-cause disk insights
|
|
260
|
+
|
|
261
|
+
**Goal: a disk alert email says *why*, not just *that*.**
|
|
262
|
+
|
|
263
|
+
This phase requires a new `portacode` PyPI release (agent-side work), per the user's direction.
|
|
264
|
+
|
|
265
|
+
1. **Agent-side lightweight always-on scan**: alongside the existing 10s `system_info` push, periodically (e.g. every 15–30 min, cheap) snapshot sizes of a curated set of "usual suspects" (docker data root, common log dirs, package manager caches, user home top-level folders) — not a full recursive filesystem walk.
|
|
266
|
+
2. **Agent-side local growth comparison**: each snapshot is diffed against the previous one, stored **locally** (SQLite file alongside existing agent state). Only when a folder shows meaningful/sustained growth does the agent escalate to a deeper recursive scan of *that* subtree to find the actual biggest/fastest-growing items — bounding cost to the rare case that matters.
|
|
267
|
+
3. **Compact reason generation on-device**: when a disk `AlertEvent` is about to fire, the agent is asked (via the existing WS command channel, same pattern as `system_info`) for a short structured summary of top offenders + growth trend; this becomes `AlertEvent.detail_summary`, included in the email body ("...largely because `/var/lib/docker/containers/<id>-json.log` grew 2.1GB in 7 days").
|
|
268
|
+
4. **Live drill-down in dashboard**: an "Investigate" view issues a live WS request to the (online) device for the fuller breakdown — never persisted centrally, per the privacy decision in §3.1. If the device is offline, the view falls back to the compact summary already stored with the event.
|
|
269
|
+
5. Same mechanism generalizes trivially to RAM alerts later (top memory-consuming processes) and is intentionally metric-agnostic in its plumbing.
|
|
270
|
+
6. First real payoff of the compatibility-expression work from §4.2: a "Docker logs growing unbounded" `AlertRuleTemplate` (`compatibility_expression = '"docker" in capabilities'`, `evaluation_stages=[on_first_system_info]`, `default_application_mode=recommend`) only makes sense on devices where Docker is detected — this phase adds it to the public library as the first genuinely-filtered example (everything in phases 1–2 was universal), surfaced through the recommendation UI built in phase 2.
|
|
271
|
+
7. **One-click template `alerts:` integration**: extend the `portafile.yaml` schema (§4.3) and `sync_content.py` to materialize a template's inline `alerts:` entries as `AlertRuleTemplate(source_one_click_template=...)` rows, and wire the `on_provisioning_complete` hook (`AutomationTask` success) to evaluate them against the specific device that was just provisioned. This is scoped separately from the general public library and can slip independently if the `devops` app side needs more design time — it doesn't block this phase's disk-insight exit criteria.
|
|
272
|
+
|
|
273
|
+
**Exit criteria:** trigger a real disk-growing scenario (e.g. a runaway log file) → the resulting alert email names the specific offending path and growth rate; opening "Investigate" in the dashboard while the device is online shows a fuller breakdown fetched live; a device with Docker detected gets the log-growth template recommended via `on_first_system_info`, and deploying a one-click template with an `alerts:` section results in the right rule appearing on that specific device once provisioning finishes, and only that device.
|
|
274
|
+
|
|
275
|
+
## 8. Phase 4 — Scheduled diagnostic commands + historical parsing (forward-looking foundation)
|
|
276
|
+
|
|
277
|
+
Not building the full feature yet, but this plan is written so phases 1–3 don't need rework when we get here.
|
|
278
|
+
|
|
279
|
+
1. New model, deliberately modeled on `AutomationTask`'s claim pattern but supporting **recurrence** (cron-like interval) and **repeated runs with a result history table** (`DeviceScheduledCommand` + `DeviceCommandRun`), rather than repurposing `AutomationTask` (which is explicitly single-execution).
|
|
280
|
+
2. `DeviceCommandRun.output` follows the same sensitivity posture as §3.1: **local-first**, with only a bounded, user-configured "store output centrally" opt-in per scheduled command (defaults off for anything not explicitly marked safe) — because arbitrary command output is a much higher sensitivity/size concern than disk path names.
|
|
281
|
+
3. Result parsing (LLM-based anomaly/issue detection over historical run output) plugs in as a consumer of `DeviceCommandRun` history — doesn't require any change to the ingestion/storage design above, only an additional analysis worker.
|
|
282
|
+
4. This is where `AlertRuleTemplate.kind` gains real new values: `command_result` (rule triggers on a scheduled command's parsed output/exit code) and `AlertActionTemplate.key=run_command` (an action that dispatches a command template, e.g. "restart container", "clear old logs in this folder"). Both plug into the *same* compatibility-expression mechanism from §4.2 — a `run_command` action naturally declares stricter requirements (e.g. `"docker" in capabilities`) than the metric-based rules that exist today, and the *same* evaluator/notification/history pipeline handles them; no parallel system.
|
|
283
|
+
5. Command-based templates and actions are exactly where the "public library, curated-only for now" decision in §4.1 matters most — letting arbitrary users publish executable command templates to a shared catalog is a real injection/security surface, so this phase should keep new command-based public entries Portacode-authored/reviewed unless/until a moderation workflow exists (see §9).
|
|
284
|
+
|
|
285
|
+
## 9. Open items intentionally deferred (flagged, not forgotten)
|
|
286
|
+
|
|
287
|
+
- Native mobile push channel: likely a new `unicom` platform; decide when a mobile app materializes (§3.3).
|
|
288
|
+
- Multi-instance scaling of the in-memory Channels layer (would need Redis) — orthogonal to this feature, only matters if Django itself scales horizontally.
|
|
289
|
+
- Any plan tiering/limits — explicitly out of scope per user (not a monetization discussion).
|
|
290
|
+
- Alert *grouping/inhibition* (e.g. "CPU + RAM + disk all firing at once because of one root event → send one message") — worth revisiting after phase 3 ships, once we have real usage data on whether correlated alerts are actually a problem for a single-device product (less likely than in Datadog's microservices use case, but worth watching per the "alert storm" research in §2).
|
|
291
|
+
- **Open community submissions to the public library**: this plan assumes the public library stays Portacode-curated (`is_reviewed=True`, staff-authored) through phase 4. Letting any user publish a template — especially a command-based one — into a shared catalog that other users can one-click "add to my library" is a real feature but needs a moderation/review workflow (and probably a sandboxed preview of exactly what a command template will run) before it's safe to open up. `AlertRuleTemplate.visibility` already has room for a future `community_pending` state; the workflow itself is deferred.
|
|
292
|
+
- **`on_profile_change` re-evaluation**: named as a stage in §4.3, but not actually wired up until it's needed — worth doing in phase 3 alongside the first real filtered template — since phase 1/2's exit criteria don't require it (nothing changes a device's OS family after the fact in those phases' test scenarios).
|
|
293
|
+
- **Expression language growth**: §4.2's function registry starts with just `version_gte`/`version_lte`/`version_gt`/`version_lt`. If real templates need more (e.g. regex-ish matching), each addition should go through the same "explicit allowlist" discipline rather than loosening the sandbox generally — resist the temptation to "just add attribute access" for convenience.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '1.5.13.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 5, 13, 'dev0')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -26,6 +26,7 @@ from .pairing import PairingError, pair_device_with_code
|
|
|
26
26
|
from .connection.client import ConnectionManager, run_until_interrupt
|
|
27
27
|
from .updater import build_pip_install_command, run_pip_install_command
|
|
28
28
|
from .utils.runtime_paths import expand_runtime_path
|
|
29
|
+
from .codex_prepare import CodexPreparationError, prepare_codex
|
|
29
30
|
|
|
30
31
|
GATEWAY_URL = "wss://portacode.com/gateway"
|
|
31
32
|
GATEWAY_ENV = "PORTACODE_GATEWAY"
|
|
@@ -38,6 +39,21 @@ def cli() -> None:
|
|
|
38
39
|
"""Portacode command-line interface."""
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
@cli.group()
|
|
43
|
+
def prepare() -> None:
|
|
44
|
+
"""Prepare supported developer tools for this Portacode device."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@prepare.command("codex")
|
|
48
|
+
def prepare_codex_command() -> None:
|
|
49
|
+
"""Install and configure Codex CLI for the local device-authenticated proxy."""
|
|
50
|
+
try:
|
|
51
|
+
config_path = prepare_codex()
|
|
52
|
+
except CodexPreparationError as exc:
|
|
53
|
+
raise click.ClickException(str(exc)) from exc
|
|
54
|
+
click.echo(click.style(f"Codex is ready: {config_path}", fg="green"))
|
|
55
|
+
|
|
56
|
+
|
|
41
57
|
@cli.command()
|
|
42
58
|
@click.option("--gateway", "gateway", "-g", help="Gateway websocket URL (overrides env/ default)")
|
|
43
59
|
@click.option("--debug", "debug", is_flag=True, help="Enable debug logging")
|