portacode 1.4.15.dev0__tar.gz → 1.4.15.dev1__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.4.15.dev0 → portacode-1.4.15.dev1}/PKG-INFO +1 -1
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/_version.py +2 -2
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +87 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/proxmox_infra.py +286 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/.claude/settings.local.json +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/.gitignore +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/.gitmodules +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/LICENSE +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/MANIFEST.in +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/Makefile +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/backup.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/connect.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/connect.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/docker-compose.yaml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/docs/images/device-transfer-button.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/docs/images/device-transfer-modal.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/docs/images/pair-device-button.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/docs/images/pairing-request.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/docs/images/student-workspace.png +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/simple_device/Dockerfile +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/simple_device/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/__main__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/cli.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/client.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/multiplex.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/terminal.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/data.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/keypair.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/elinks +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/links +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/links2 +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/lynx +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/w3m +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/logging_categories.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/pairing.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/service.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/utils/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/utils/diff_apply.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/utils/diff_renderer.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode.egg-info/SOURCES.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/pyproject.toml +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/restore.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/run_tests.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/setup.cfg +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/setup.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_device_online.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_file_operations.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_login_flow.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_play_store_screenshots.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/test_request_id.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/.env.example +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/README.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/cli.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/__init__.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/base_test.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/runner.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/testing_framework/requirements.txt +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/agent_context_management.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/django_server_time_sync.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/tools/generate_play_store_assets.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/tools/pairing_tester.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/tools/run_screenshot_suite.sh +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/tools/test_python_ntp_clock.py +0 -0
- {portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/validate.sh +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.4.15.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 15, '
|
|
31
|
+
__version__ = version = '1.4.15.dev1'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 15, 'dev1')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
{portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
RENAMED
|
@@ -331,6 +331,8 @@ Configures a Proxmox node for Portacode infrastructure usage (API token validati
|
|
|
331
331
|
* `verify_ssl` (boolean, optional): When true, the handler verifies SSL certificates; defaults to `false`. When omitted, the last configured value is preserved.
|
|
332
332
|
* `cloudflare_api_token` (string, optional): Cloudflare API token the host can reuse later to provision tunnels.
|
|
333
333
|
|
|
334
|
+
The setup command also installs `cloudflared` on the node so Cloudflare tunnels can be created afterward.
|
|
335
|
+
|
|
334
336
|
**Responses:**
|
|
335
337
|
|
|
336
338
|
* On success, the device will emit a [`proxmox_infra_configured`](#proxmox_infra_configured-event) event with the persisted infra snapshot.
|
|
@@ -410,6 +412,51 @@ Deletes a managed container from Proxmox (stopping it first if necessary) and re
|
|
|
410
412
|
* Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="remove"` and the refreshed infra snapshot after deletion.
|
|
411
413
|
* Emits an [`error`](#error) event on failure.
|
|
412
414
|
|
|
415
|
+
### `create_cloudflare_tunnel`
|
|
416
|
+
|
|
417
|
+
Creates or updates a Cloudflare tunnel in front of a managed container. The container must be running and a Cloudflare API token must already be configured via `setup_proxmox_infra`.
|
|
418
|
+
|
|
419
|
+
**Payload Fields:**
|
|
420
|
+
|
|
421
|
+
* `ctid` (string, required): Identifier of the container.
|
|
422
|
+
* `container_port` (integer, required): Port inside the container to expose through Cloudflare.
|
|
423
|
+
* `cloudflare_url` (string, required): Hostname (e.g., `app.example.com`) that the tunnel should serve.
|
|
424
|
+
* `protocol` (string, optional): One of `http`, `https`, or `tcp` (defaults to `http`).
|
|
425
|
+
|
|
426
|
+
**Responses:**
|
|
427
|
+
|
|
428
|
+
* Emits a [`cloudflare_tunnel_created`](#cloudflare_tunnel_created-event) event with the tunnel metadata.
|
|
429
|
+
* Emits an [`error`](#error) event on failure.
|
|
430
|
+
|
|
431
|
+
### `update_cloudflare_tunnel`
|
|
432
|
+
|
|
433
|
+
Refreshes the tunnel configuration for an existing tunnel (different port, URL, or protocol).
|
|
434
|
+
|
|
435
|
+
**Payload Fields:**
|
|
436
|
+
|
|
437
|
+
* `ctid` (string, required): Identifier of the container.
|
|
438
|
+
* `container_port` (integer, optional): New container port.
|
|
439
|
+
* `cloudflare_url` (string, optional): New hostname.
|
|
440
|
+
* `protocol` (string, optional): New protocol (`http`, `https`, or `tcp`).
|
|
441
|
+
|
|
442
|
+
**Responses:**
|
|
443
|
+
|
|
444
|
+
* Emits a [`cloudflare_tunnel_updated`](#cloudflare_tunnel_updated-event) event with the refreshed tunnel metadata.
|
|
445
|
+
* Emits an [`error`](#error) event on failure.
|
|
446
|
+
|
|
447
|
+
### `remove_cloudflare_tunnel`
|
|
448
|
+
|
|
449
|
+
Stops and removes any tunnel metadata associated with a container.
|
|
450
|
+
|
|
451
|
+
**Payload Fields:**
|
|
452
|
+
|
|
453
|
+
* `ctid` (string, required): Identifier of the container.
|
|
454
|
+
|
|
455
|
+
**Responses:**
|
|
456
|
+
|
|
457
|
+
* Emits a [`cloudflare_tunnel_removed`](#cloudflare_tunnel_removed-event) event.
|
|
458
|
+
* Emits an [`error`](#error) event on failure.
|
|
459
|
+
|
|
413
460
|
### `proxmox_container_created`
|
|
414
461
|
|
|
415
462
|
Emitted after a successful `create_proxmox_container` action. Contains the new container ID, the Portacode public key produced inside the container, and the bootstrap logs.
|
|
@@ -1146,6 +1193,11 @@ Provides system information in response to a `system_info` action. Handled by [`
|
|
|
1146
1193
|
* `cpu_share` (number): vCPU-equivalent share requested at creation.
|
|
1147
1194
|
* `status` (string): Lowercase lifecycle status (e.g., `running`, `stopped`, `deleted`).
|
|
1148
1195
|
* `created_at` (string|null): ISO timestamp recorded when the CT was provisioned.
|
|
1196
|
+
* `tunnel` (object|null): Tunnel metadata that includes:
|
|
1197
|
+
* `url` (string): Public hostname assigned for this tunnel.
|
|
1198
|
+
* `container_port` (integer): Container port exposed via the tunnel.
|
|
1199
|
+
* `protocol` (string): Protocol advertised when the tunnel was configured.
|
|
1200
|
+
* `status` (string): `running`, `stopped`, or `unknown`.
|
|
1149
1201
|
* `portacode_version` (string): Installed CLI version returned by `portacode.__version__`.
|
|
1150
1202
|
|
|
1151
1203
|
### `proxmox_infra_configured`
|
|
@@ -1211,6 +1263,41 @@ Emitted after `start_proxmox_container`, `stop_proxmox_container`, or `remove_pr
|
|
|
1211
1263
|
* `details` (object, optional): Exit status information (e.g., `exitstatus`, `stop_exitstatus`, `delete_exitstatus`).
|
|
1212
1264
|
* `infra` (object): Same snapshot described under [`system_info`](#system_info-event) `proxmox.infra`, including the updated `managed_containers` summary.
|
|
1213
1265
|
|
|
1266
|
+
### `cloudflare_tunnel_created`
|
|
1267
|
+
|
|
1268
|
+
Submitted after a successful `create_cloudflare_tunnel` action.
|
|
1269
|
+
|
|
1270
|
+
**Event Fields:**
|
|
1271
|
+
|
|
1272
|
+
* `event` (string): `cloudflare_tunnel_created`.
|
|
1273
|
+
* `ctid` (string): Container ID associated with the tunnel.
|
|
1274
|
+
* `success` (boolean): True when the tunnel is running.
|
|
1275
|
+
* `message` (string): Summary text.
|
|
1276
|
+
* `tunnel` (object): Tunnel metadata matching the manager view.
|
|
1277
|
+
|
|
1278
|
+
### `cloudflare_tunnel_updated`
|
|
1279
|
+
|
|
1280
|
+
Sent when `update_cloudflare_tunnel` completes.
|
|
1281
|
+
|
|
1282
|
+
**Event Fields:**
|
|
1283
|
+
|
|
1284
|
+
* `event` (string): `cloudflare_tunnel_updated`.
|
|
1285
|
+
* `ctid` (string): Container ID associated with the tunnel.
|
|
1286
|
+
* `success` (boolean): True on success.
|
|
1287
|
+
* `message` (string): Summary text.
|
|
1288
|
+
* `tunnel` (object): Updated tunnel metadata.
|
|
1289
|
+
|
|
1290
|
+
### `cloudflare_tunnel_removed`
|
|
1291
|
+
|
|
1292
|
+
Sent after a tunnel has been removed from a container record.
|
|
1293
|
+
|
|
1294
|
+
**Event Fields:**
|
|
1295
|
+
|
|
1296
|
+
* `event` (string): `cloudflare_tunnel_removed`.
|
|
1297
|
+
* `ctid` (string): Container ID that no longer has a tunnel.
|
|
1298
|
+
* `success` (boolean): True on success.
|
|
1299
|
+
* `message` (string): Summary text.
|
|
1300
|
+
|
|
1214
1301
|
### <a name="clock_sync_response"></a>`clock_sync_response`
|
|
1215
1302
|
|
|
1216
1303
|
Reply sent by the gateway immediately after receiving a `clock_sync_request`. Devices use this event plus the measured round-trip time to keep their local `ntp_clock` offset accurate.
|
{portacode-1.4.15.dev0 → portacode-1.4.15.dev1}/portacode/connection/handlers/proxmox_infra.py
RENAMED
|
@@ -15,6 +15,7 @@ import sys
|
|
|
15
15
|
import tempfile
|
|
16
16
|
import time
|
|
17
17
|
import threading
|
|
18
|
+
import urllib.request
|
|
18
19
|
from datetime import datetime
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
@@ -40,12 +41,15 @@ BRIDGE_IP = SUBNET_CIDR.split("/", 1)[0]
|
|
|
40
41
|
DHCP_START = "10.10.0.100"
|
|
41
42
|
DHCP_END = "10.10.0.200"
|
|
42
43
|
DNS_SERVER = "1.1.1.1"
|
|
44
|
+
CLOUDFLARE_DEB_URL = "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb"
|
|
43
45
|
IFACES_PATH = Path("/etc/network/interfaces")
|
|
44
46
|
SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
|
|
45
47
|
UNIT_DIR = Path("/etc/systemd/system")
|
|
46
48
|
_MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
|
|
47
49
|
_MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
|
|
48
50
|
_MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
|
|
51
|
+
_CLOUDFLARE_TUNNEL_PROCESSES: Dict[str, subprocess.Popen] = {}
|
|
52
|
+
_CLOUDFLARE_TUNNELS_LOCK = threading.Lock()
|
|
49
53
|
|
|
50
54
|
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
51
55
|
|
|
@@ -279,6 +283,25 @@ def _ensure_bridge(bridge: str = DEFAULT_BRIDGE) -> Dict[str, Any]:
|
|
|
279
283
|
return {"applied": True, "bridge": bridge, "message": f"Bridge {bridge} configured"}
|
|
280
284
|
|
|
281
285
|
|
|
286
|
+
def _ensure_cloudflared_installed() -> None:
|
|
287
|
+
if shutil.which("cloudflared"):
|
|
288
|
+
return
|
|
289
|
+
apt = shutil.which("apt-get")
|
|
290
|
+
if not apt:
|
|
291
|
+
raise RuntimeError("cloudflared is missing and apt-get is unavailable to install it")
|
|
292
|
+
download_dir = Path(tempfile.mkdtemp())
|
|
293
|
+
deb_path = download_dir / "cloudflared.deb"
|
|
294
|
+
try:
|
|
295
|
+
urllib.request.urlretrieve(CLOUDFLARE_DEB_URL, deb_path)
|
|
296
|
+
try:
|
|
297
|
+
_call_subprocess(["dpkg", "-i", str(deb_path)], check=True)
|
|
298
|
+
except subprocess.CalledProcessError:
|
|
299
|
+
_call_subprocess([apt, "install", "-f", "-y"], check=True)
|
|
300
|
+
_call_subprocess(["dpkg", "-i", str(deb_path)], check=True)
|
|
301
|
+
finally:
|
|
302
|
+
shutil.rmtree(download_dir, ignore_errors=True)
|
|
303
|
+
|
|
304
|
+
|
|
282
305
|
def _verify_connectivity(timeout: float = 5.0) -> bool:
|
|
283
306
|
try:
|
|
284
307
|
_call_subprocess(["/bin/ping", "-c", "2", "1.1.1.1"], check=True, timeout=timeout)
|
|
@@ -355,6 +378,7 @@ def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str
|
|
|
355
378
|
"cpu_share": cpu_share,
|
|
356
379
|
"created_at": record.get("created_at"),
|
|
357
380
|
"status": status,
|
|
381
|
+
"tunnel": record.get("tunnel"),
|
|
358
382
|
}
|
|
359
383
|
)
|
|
360
384
|
|
|
@@ -592,6 +616,88 @@ def _remove_container_record(vmid: int) -> None:
|
|
|
592
616
|
_invalidate_managed_containers_cache()
|
|
593
617
|
|
|
594
618
|
|
|
619
|
+
def _update_container_tunnel(vmid: int, tunnel: Optional[Dict[str, Any]]) -> None:
|
|
620
|
+
record = _read_container_record(vmid)
|
|
621
|
+
if tunnel:
|
|
622
|
+
record["tunnel"] = tunnel
|
|
623
|
+
else:
|
|
624
|
+
record.pop("tunnel", None)
|
|
625
|
+
_write_container_record(vmid, record)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _ensure_cloudflare_token(config: Dict[str, Any]) -> str:
|
|
629
|
+
cloudflare = config.get("cloudflare") or {}
|
|
630
|
+
token = cloudflare.get("api_token")
|
|
631
|
+
if not token:
|
|
632
|
+
raise RuntimeError("Cloudflare API token is not configured.")
|
|
633
|
+
return token
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _launch_container_tunnel(proxmox: Any, node: str, vmid: int, tunnel: Dict[str, Any]) -> Dict[str, Any]:
|
|
637
|
+
port = int(tunnel.get("container_port") or 0)
|
|
638
|
+
if not port:
|
|
639
|
+
raise ValueError("container_port is required to create a tunnel.")
|
|
640
|
+
hostname = tunnel.get("url")
|
|
641
|
+
if not hostname:
|
|
642
|
+
raise ValueError("cloudflare_url is required to expose the tunnel.")
|
|
643
|
+
protocol = (tunnel.get("protocol") or "http").lower()
|
|
644
|
+
ip_address = _resolve_container_ip(proxmox, node, vmid)
|
|
645
|
+
target_url = f"{protocol}://{ip_address}:{port}"
|
|
646
|
+
name = tunnel.get("name") or _build_tunnel_name(vmid, port)
|
|
647
|
+
_stop_cloudflare_process(name)
|
|
648
|
+
proc = _start_cloudflare_process(name, hostname, target_url)
|
|
649
|
+
updated = {
|
|
650
|
+
"name": name,
|
|
651
|
+
"container_port": port,
|
|
652
|
+
"url": hostname,
|
|
653
|
+
"protocol": protocol,
|
|
654
|
+
"status": "running",
|
|
655
|
+
"pid": proc.pid,
|
|
656
|
+
"target_ip": ip_address,
|
|
657
|
+
"target_url": target_url,
|
|
658
|
+
"last_updated": datetime.utcnow().isoformat() + "Z",
|
|
659
|
+
}
|
|
660
|
+
_update_container_tunnel(vmid, updated)
|
|
661
|
+
return updated
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _stop_container_tunnel(vmid: int) -> None:
|
|
665
|
+
try:
|
|
666
|
+
record = _read_container_record(vmid)
|
|
667
|
+
except FileNotFoundError:
|
|
668
|
+
return
|
|
669
|
+
tunnel = record.get("tunnel")
|
|
670
|
+
if not tunnel:
|
|
671
|
+
return
|
|
672
|
+
name = tunnel.get("name") or _build_tunnel_name(vmid, int(tunnel.get("container_port") or 0))
|
|
673
|
+
stopped = _stop_cloudflare_process(name)
|
|
674
|
+
if stopped or tunnel.get("status") != "stopped":
|
|
675
|
+
tunnel_update = {
|
|
676
|
+
**tunnel,
|
|
677
|
+
"status": "stopped",
|
|
678
|
+
"pid": None,
|
|
679
|
+
"last_updated": datetime.utcnow().isoformat() + "Z",
|
|
680
|
+
}
|
|
681
|
+
_update_container_tunnel(vmid, tunnel_update)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _remove_container_tunnel_state(vmid: int) -> None:
|
|
685
|
+
_stop_container_tunnel(vmid)
|
|
686
|
+
_update_container_tunnel(vmid, None)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _ensure_container_tunnel_running(proxmox: Any, node: str, vmid: int) -> None:
|
|
690
|
+
try:
|
|
691
|
+
record = _read_container_record(vmid)
|
|
692
|
+
except FileNotFoundError:
|
|
693
|
+
return
|
|
694
|
+
tunnel = record.get("tunnel")
|
|
695
|
+
if not tunnel:
|
|
696
|
+
return
|
|
697
|
+
_ensure_cloudflare_token(_load_config())
|
|
698
|
+
_launch_container_tunnel(proxmox, node, vmid, tunnel)
|
|
699
|
+
|
|
700
|
+
|
|
595
701
|
def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
596
702
|
templates = config.get("templates") or []
|
|
597
703
|
default_template = templates[0] if templates else ""
|
|
@@ -707,6 +813,74 @@ def _run_pct_push(vmid: int, src: str, dest: str) -> subprocess.CompletedProcess
|
|
|
707
813
|
return _call_subprocess(["pct", "push", str(vmid), src, dest])
|
|
708
814
|
|
|
709
815
|
|
|
816
|
+
def _build_tunnel_name(vmid: int, port: int) -> str:
|
|
817
|
+
return f"portacode-ct{vmid}-{port}"
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _get_cloudflared_binary() -> str:
|
|
821
|
+
binary = shutil.which("cloudflared")
|
|
822
|
+
if not binary:
|
|
823
|
+
raise RuntimeError(
|
|
824
|
+
"cloudflared is required for Cloudflare tunnels but was not found on PATH. "
|
|
825
|
+
"Install cloudflared and run 'cloudflared tunnel login' before creating tunnels."
|
|
826
|
+
)
|
|
827
|
+
return binary
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _start_cloudflare_process(name: str, hostname: str, target_url: str) -> subprocess.Popen:
|
|
831
|
+
binary = _get_cloudflared_binary()
|
|
832
|
+
cmd = [
|
|
833
|
+
binary,
|
|
834
|
+
"tunnel",
|
|
835
|
+
"--hostname",
|
|
836
|
+
hostname,
|
|
837
|
+
"--url",
|
|
838
|
+
target_url,
|
|
839
|
+
"--no-autoupdate",
|
|
840
|
+
]
|
|
841
|
+
proc = subprocess.Popen(
|
|
842
|
+
cmd,
|
|
843
|
+
stdout=subprocess.DEVNULL,
|
|
844
|
+
stderr=subprocess.DEVNULL,
|
|
845
|
+
)
|
|
846
|
+
with _CLOUDFLARE_TUNNELS_LOCK:
|
|
847
|
+
_CLOUDFLARE_TUNNEL_PROCESSES[name] = proc
|
|
848
|
+
return proc
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _stop_cloudflare_process(name: str) -> bool:
|
|
852
|
+
with _CLOUDFLARE_TUNNELS_LOCK:
|
|
853
|
+
proc = _CLOUDFLARE_TUNNEL_PROCESSES.pop(name, None)
|
|
854
|
+
if not proc:
|
|
855
|
+
return False
|
|
856
|
+
try:
|
|
857
|
+
proc.terminate()
|
|
858
|
+
proc.wait(timeout=5)
|
|
859
|
+
except subprocess.TimeoutExpired:
|
|
860
|
+
proc.kill()
|
|
861
|
+
proc.wait()
|
|
862
|
+
return True
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _resolve_container_ip(proxmox: Any, node: str, vmid: int) -> str:
|
|
866
|
+
status = proxmox.nodes(node).lxc(vmid).status.current.get()
|
|
867
|
+
if status:
|
|
868
|
+
ip_field = status.get("ip")
|
|
869
|
+
if isinstance(ip_field, list):
|
|
870
|
+
for entry in ip_field:
|
|
871
|
+
if isinstance(entry, str) and "." in entry:
|
|
872
|
+
return entry.split("/")[0]
|
|
873
|
+
elif isinstance(ip_field, str) and "." in ip_field:
|
|
874
|
+
return ip_field.split("/")[0]
|
|
875
|
+
res = _run_pct_exec(vmid, ["ip", "-4", "-o", "addr", "show", "eth0"])
|
|
876
|
+
line = res.stdout.splitlines()[0] if res.stdout else ""
|
|
877
|
+
parts = line.split()
|
|
878
|
+
if len(parts) >= 4:
|
|
879
|
+
addr = parts[3]
|
|
880
|
+
return addr.split("/")[0]
|
|
881
|
+
raise RuntimeError("Unable to determine container IP address")
|
|
882
|
+
|
|
883
|
+
|
|
710
884
|
def _push_bytes_to_container(
|
|
711
885
|
vmid: int, user: str, path: str, data: bytes, mode: int = 0o600
|
|
712
886
|
) -> None:
|
|
@@ -1091,6 +1265,7 @@ def configure_infrastructure(
|
|
|
1091
1265
|
default_storage = _pick_storage(storages)
|
|
1092
1266
|
templates = _list_templates(client, node, storages)
|
|
1093
1267
|
network = dict(existing.get("network", {}) or {})
|
|
1268
|
+
_ensure_cloudflared_installed()
|
|
1094
1269
|
if not network.get("applied"):
|
|
1095
1270
|
try:
|
|
1096
1271
|
network = _ensure_bridge()
|
|
@@ -1602,6 +1777,10 @@ class StartProxmoxContainerHandler(SyncHandler):
|
|
|
1602
1777
|
|
|
1603
1778
|
status, elapsed = _start_container(proxmox, node, vmid)
|
|
1604
1779
|
_update_container_record(vmid, {"status": "running"})
|
|
1780
|
+
try:
|
|
1781
|
+
_ensure_container_tunnel_running(proxmox, node, vmid)
|
|
1782
|
+
except Exception as exc:
|
|
1783
|
+
raise RuntimeError(f"Failed to start Cloudflare tunnel for container {vmid}: {exc}") from exc
|
|
1605
1784
|
|
|
1606
1785
|
infra = get_infra_snapshot()
|
|
1607
1786
|
return {
|
|
@@ -1631,6 +1810,7 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
1631
1810
|
_ensure_container_managed(proxmox, node, vmid)
|
|
1632
1811
|
|
|
1633
1812
|
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
1813
|
+
_stop_container_tunnel(vmid)
|
|
1634
1814
|
final_status = status.get("status") or "stopped"
|
|
1635
1815
|
_update_container_record(vmid, {"status": final_status})
|
|
1636
1816
|
|
|
@@ -1666,8 +1846,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
1666
1846
|
node = _get_node_from_config(config)
|
|
1667
1847
|
_ensure_container_managed(proxmox, node, vmid)
|
|
1668
1848
|
|
|
1849
|
+
_stop_container_tunnel(vmid)
|
|
1669
1850
|
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
1670
1851
|
delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
|
|
1852
|
+
try:
|
|
1853
|
+
_update_container_tunnel(vmid, None)
|
|
1854
|
+
except FileNotFoundError:
|
|
1855
|
+
pass
|
|
1671
1856
|
_remove_container_record(vmid)
|
|
1672
1857
|
|
|
1673
1858
|
infra = get_infra_snapshot()
|
|
@@ -1686,6 +1871,107 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
1686
1871
|
}
|
|
1687
1872
|
|
|
1688
1873
|
|
|
1874
|
+
class CreateCloudflareTunnelHandler(SyncHandler):
|
|
1875
|
+
"""Create a Cloudflare tunnel for a container."""
|
|
1876
|
+
|
|
1877
|
+
@property
|
|
1878
|
+
def command_name(self) -> str:
|
|
1879
|
+
return "create_cloudflare_tunnel"
|
|
1880
|
+
|
|
1881
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1882
|
+
vmid = _parse_ctid(message)
|
|
1883
|
+
container_port = int(message.get("container_port") or 0)
|
|
1884
|
+
if container_port <= 0:
|
|
1885
|
+
raise ValueError("container_port is required and must be greater than zero.")
|
|
1886
|
+
hostname = (message.get("cloudflare_url") or message.get("hostname") or "").strip()
|
|
1887
|
+
if not hostname:
|
|
1888
|
+
raise ValueError("cloudflare_url is required.")
|
|
1889
|
+
protocol = (message.get("protocol") or "http").strip().lower()
|
|
1890
|
+
if protocol not in {"http", "https", "tcp"}:
|
|
1891
|
+
raise ValueError("protocol must be one of http, https, or tcp.")
|
|
1892
|
+
config = _ensure_infra_configured()
|
|
1893
|
+
_ensure_cloudflare_token(config)
|
|
1894
|
+
proxmox = _connect_proxmox(config)
|
|
1895
|
+
node = _get_node_from_config(config)
|
|
1896
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1897
|
+
status = proxmox.nodes(node).lxc(vmid).status.current.get().get("status")
|
|
1898
|
+
if status != "running":
|
|
1899
|
+
raise RuntimeError("Container must be running to create a tunnel.")
|
|
1900
|
+
tunnel = {
|
|
1901
|
+
"container_port": container_port,
|
|
1902
|
+
"url": hostname,
|
|
1903
|
+
"protocol": protocol,
|
|
1904
|
+
}
|
|
1905
|
+
created = _launch_container_tunnel(proxmox, node, vmid, tunnel)
|
|
1906
|
+
return {
|
|
1907
|
+
"event": "cloudflare_tunnel_created",
|
|
1908
|
+
"ctid": str(vmid),
|
|
1909
|
+
"success": True,
|
|
1910
|
+
"message": f"Created Cloudflare tunnel for container {vmid}.",
|
|
1911
|
+
"tunnel": created,
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
class UpdateCloudflareTunnelHandler(SyncHandler):
|
|
1916
|
+
"""Update an existing Cloudflare tunnel for a container."""
|
|
1917
|
+
|
|
1918
|
+
@property
|
|
1919
|
+
def command_name(self) -> str:
|
|
1920
|
+
return "update_cloudflare_tunnel"
|
|
1921
|
+
|
|
1922
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1923
|
+
vmid = _parse_ctid(message)
|
|
1924
|
+
config = _ensure_infra_configured()
|
|
1925
|
+
_ensure_cloudflare_token(config)
|
|
1926
|
+
proxmox = _connect_proxmox(config)
|
|
1927
|
+
node = _get_node_from_config(config)
|
|
1928
|
+
_ensure_container_managed(proxmox, node, vmid)
|
|
1929
|
+
record = _read_container_record(vmid)
|
|
1930
|
+
tunnel = record.get("tunnel")
|
|
1931
|
+
if not tunnel:
|
|
1932
|
+
raise RuntimeError("No Cloudflare tunnel configured for this container.")
|
|
1933
|
+
container_port = int(message.get("container_port") or tunnel.get("container_port") or 0)
|
|
1934
|
+
if container_port <= 0:
|
|
1935
|
+
raise ValueError("container_port must be greater than zero.")
|
|
1936
|
+
hostname = (message.get("cloudflare_url") or tunnel.get("url") or "").strip()
|
|
1937
|
+
if not hostname:
|
|
1938
|
+
raise ValueError("cloudflare_url is required.")
|
|
1939
|
+
protocol = (message.get("protocol") or tunnel.get("protocol") or "http").strip().lower()
|
|
1940
|
+
if protocol not in {"http", "https", "tcp"}:
|
|
1941
|
+
raise ValueError("protocol must be one of http, https, or tcp.")
|
|
1942
|
+
updated_tunnel = {
|
|
1943
|
+
"container_port": container_port,
|
|
1944
|
+
"url": hostname,
|
|
1945
|
+
"protocol": protocol,
|
|
1946
|
+
}
|
|
1947
|
+
result = _launch_container_tunnel(proxmox, node, vmid, updated_tunnel)
|
|
1948
|
+
return {
|
|
1949
|
+
"event": "cloudflare_tunnel_updated",
|
|
1950
|
+
"ctid": str(vmid),
|
|
1951
|
+
"success": True,
|
|
1952
|
+
"message": f"Updated Cloudflare tunnel for container {vmid}.",
|
|
1953
|
+
"tunnel": result,
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
class RemoveCloudflareTunnelHandler(SyncHandler):
|
|
1958
|
+
"""Remove any Cloudflare tunnel associated with a container."""
|
|
1959
|
+
|
|
1960
|
+
@property
|
|
1961
|
+
def command_name(self) -> str:
|
|
1962
|
+
return "remove_cloudflare_tunnel"
|
|
1963
|
+
|
|
1964
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1965
|
+
vmid = _parse_ctid(message)
|
|
1966
|
+
_remove_container_tunnel_state(vmid)
|
|
1967
|
+
return {
|
|
1968
|
+
"event": "cloudflare_tunnel_removed",
|
|
1969
|
+
"ctid": str(vmid),
|
|
1970
|
+
"success": True,
|
|
1971
|
+
"message": f"Removed Cloudflare tunnel state for container {vmid}.",
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
|
|
1689
1975
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
1690
1976
|
@property
|
|
1691
1977
|
def command_name(self) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|