portacode 1.5.32.dev2__tar.gz → 1.5.33.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.32.dev2 → portacode-1.5.33.dev0}/PKG-INFO +1 -1
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/_version.py +2 -2
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/cloudflare_forwarding.py +200 -35
- portacode-1.5.33.dev0/portacode/connection/handlers/test_cloudflare_forwarding.py +262 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode.egg-info/PKG-INFO +1 -1
- portacode-1.5.32.dev2/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -101
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/.claude/settings.local.json +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/.gitignore +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/.gitmodules +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/AGENTS.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/LICENSE +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/MANIFEST.in +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/Makefile +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/build_android.sh +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/connect.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/connect.sh +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/docs/creative-team-brief-portacode.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/docs/device-health-alerting-plan.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/docs/devops-messaging-ab-tests.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/docs/homepage-dashboard-positioning-fixes.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/docs/template-guide.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/simple_device/Dockerfile +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/simple_device/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/__main__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/cli.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/codex_loopback_proxy.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/codex_prepare.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/codex_usage_limit.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/client.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/codex_app_server.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/browser_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/codex_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/proxmox_infra.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/resumable_transfer_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/runtime_user.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/test_browser_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/test_file_move_copy.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/test_runtime_user.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/multiplex.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/terminal.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/test_webmin_proxy_config.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/connection/webmin_proxy_config.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/data.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/exit_codes.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/github_credential.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/keypair.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/elinks +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/links +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/links2 +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/lynx +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/w3m +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/logging_categories.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/pairing.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/provisioning_cache.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/restart.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/service.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/singleton.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/test_runtime_paths.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/test_service.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/test_singleton.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/test_terminal_exposed_services.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_codex_app_server.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_codex_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_codex_loopback_proxy.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_codex_prepare.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_github_credential.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_image_read_handler.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_resumable_transfer_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tests/test_system_handlers.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/cloudflared_login.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/ensure_cloudflared.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/ensure_pyyaml.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/forwarding_state.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/get_domain.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/privileged.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/service_install.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/tunneling/state.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/updater.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/utils/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/utils/diff_apply.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/utils/diff_renderer.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode/utils/runtime_paths.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode.egg-info/SOURCES.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/.gitignore +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/persistent_workspace/Dockerfile +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/persistent_workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/persistent_workspace/entrypoint.sh +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/persistent_workspace/manage_classroom.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/temporary_workspace/Dockerfile +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/temporary_workspace/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/temporary_workspace/docker-compose.yaml +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/temporary_workspace/docs/device_transfere.png +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/temporary_workspace/docs/pairing_approval.png +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/portacode_for_school/temporary_workspace/entrypoint.sh +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/pyproject.toml +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/run_tests.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/scripts/diagnose_codex_gateway.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/setup.cfg +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/setup.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test.sh +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_device_online.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_file_operations.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_login_flow.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_play_store_screenshots.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_request_id.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/test_session_terminal_shutdown.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/.env.example +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/README.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/cli.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/__init__.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/base_test.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/runner.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/testing_framework/requirements.txt +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/agent_context_management.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/django_server_time_sync.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/websocket_client_silently_dead.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/tools/generate_play_store_assets.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/tools/pairing_tester.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/tools/run_screenshot_suite.sh +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/tools/test_python_ntp_clock.py +0 -0
- {portacode-1.5.32.dev2 → portacode-1.5.33.dev0}/validate.sh +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.5.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 5,
|
|
21
|
+
__version__ = version = '1.5.33.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 5, 33, 'dev0')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -11,6 +11,10 @@ import subprocess
|
|
|
11
11
|
import threading
|
|
12
12
|
import tempfile
|
|
13
13
|
import time
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.parse
|
|
16
|
+
import urllib.request
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
14
18
|
from pathlib import Path
|
|
15
19
|
from typing import Any, Dict, List, Optional
|
|
16
20
|
from urllib.parse import urlparse
|
|
@@ -29,6 +33,7 @@ from .proxmox_infra import (
|
|
|
29
33
|
_resolve_vmid_for_device_in_proxmox,
|
|
30
34
|
)
|
|
31
35
|
from portacode.tunneling.forwarding_state import load_forwarding_state, persist_forwarding_state
|
|
36
|
+
from portacode.tunneling.get_domain import extract_token_json
|
|
32
37
|
from portacode.tunneling.state import default_config_path, load_state
|
|
33
38
|
from portacode.tunneling.privileged import write_text
|
|
34
39
|
from portacode.tunneling.service_install import restart_service
|
|
@@ -223,10 +228,10 @@ def _resolve_service_endpoint(
|
|
|
223
228
|
if device_id in cache:
|
|
224
229
|
ip = cache[device_id]
|
|
225
230
|
else:
|
|
226
|
-
vmid = _resolve_device_vmid(device_id, proxmox, node)
|
|
227
231
|
try:
|
|
232
|
+
vmid = _resolve_device_vmid(device_id, proxmox, node)
|
|
228
233
|
ip = _find_container_ip(proxmox, node, vmid, leases)
|
|
229
|
-
except RuntimeError:
|
|
234
|
+
except (_DeviceLookupError, RuntimeError):
|
|
230
235
|
ip = str(parsed.get("resolved_ip") or "").strip()
|
|
231
236
|
if not ip:
|
|
232
237
|
raise
|
|
@@ -353,6 +358,7 @@ def _write_cloudflared_config(state: Dict[str, Any], entries: List[Dict[str, Any
|
|
|
353
358
|
|
|
354
359
|
|
|
355
360
|
def _route_dns(hostnames: List[str], tunnel_name: str) -> None:
|
|
361
|
+
"""Compatibility fallback for nodes whose API credential cannot edit DNS."""
|
|
356
362
|
seen = set()
|
|
357
363
|
for hostname in hostnames:
|
|
358
364
|
if hostname in seen:
|
|
@@ -379,6 +385,141 @@ def _route_dns(hostnames: List[str], tunnel_name: str) -> None:
|
|
|
379
385
|
) from exc
|
|
380
386
|
|
|
381
387
|
|
|
388
|
+
def _cloudflare_api_request(
|
|
389
|
+
method: str,
|
|
390
|
+
path: str,
|
|
391
|
+
*,
|
|
392
|
+
api_token: str,
|
|
393
|
+
payload: Dict[str, Any] | None = None,
|
|
394
|
+
) -> Any:
|
|
395
|
+
body = json.dumps(payload).encode("utf-8") if payload is not None else None
|
|
396
|
+
request = urllib.request.Request(
|
|
397
|
+
f"https://api.cloudflare.com/client/v4{path}",
|
|
398
|
+
data=body,
|
|
399
|
+
method=method,
|
|
400
|
+
headers={
|
|
401
|
+
"Authorization": f"Bearer {api_token}",
|
|
402
|
+
"Content-Type": "application/json",
|
|
403
|
+
},
|
|
404
|
+
)
|
|
405
|
+
try:
|
|
406
|
+
with urllib.request.urlopen(request, timeout=10) as response:
|
|
407
|
+
response_data = json.loads(response.read().decode("utf-8"))
|
|
408
|
+
except (OSError, urllib.error.URLError, json.JSONDecodeError) as exc:
|
|
409
|
+
raise RuntimeError(f"Cloudflare DNS API request failed: {exc}") from exc
|
|
410
|
+
if not response_data.get("success"):
|
|
411
|
+
errors = response_data.get("errors") or []
|
|
412
|
+
message = errors[0].get("message") if errors else "unknown API error"
|
|
413
|
+
raise RuntimeError(f"Cloudflare DNS API error: {message}")
|
|
414
|
+
return response_data.get("result")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _upsert_tunnel_dns_record(
|
|
418
|
+
hostname: str,
|
|
419
|
+
*,
|
|
420
|
+
zone_id: str,
|
|
421
|
+
api_token: str,
|
|
422
|
+
tunnel_id: str,
|
|
423
|
+
) -> str:
|
|
424
|
+
query = urllib.parse.urlencode({"name": hostname, "per_page": 100})
|
|
425
|
+
records = _cloudflare_api_request(
|
|
426
|
+
"GET",
|
|
427
|
+
f"/zones/{zone_id}/dns_records?{query}",
|
|
428
|
+
api_token=api_token,
|
|
429
|
+
)
|
|
430
|
+
existing = records[0] if isinstance(records, list) and records else None
|
|
431
|
+
desired = {
|
|
432
|
+
"type": "CNAME",
|
|
433
|
+
"name": hostname,
|
|
434
|
+
"content": f"{tunnel_id}.cfargotunnel.com",
|
|
435
|
+
"proxied": True,
|
|
436
|
+
"ttl": 1,
|
|
437
|
+
}
|
|
438
|
+
if existing:
|
|
439
|
+
matches = (
|
|
440
|
+
str(existing.get("type") or "").upper() == desired["type"]
|
|
441
|
+
and str(existing.get("content") or "").rstrip(".").lower()
|
|
442
|
+
== desired["content"].lower()
|
|
443
|
+
and bool(existing.get("proxied"))
|
|
444
|
+
)
|
|
445
|
+
if matches:
|
|
446
|
+
return "unchanged"
|
|
447
|
+
record_id = str(existing.get("id") or "").strip()
|
|
448
|
+
if not record_id:
|
|
449
|
+
raise RuntimeError(f"Cloudflare DNS record for {hostname!r} has no ID")
|
|
450
|
+
_cloudflare_api_request(
|
|
451
|
+
"PUT",
|
|
452
|
+
f"/zones/{zone_id}/dns_records/{record_id}",
|
|
453
|
+
api_token=api_token,
|
|
454
|
+
payload=desired,
|
|
455
|
+
)
|
|
456
|
+
return "updated"
|
|
457
|
+
_cloudflare_api_request(
|
|
458
|
+
"POST",
|
|
459
|
+
f"/zones/{zone_id}/dns_records",
|
|
460
|
+
api_token=api_token,
|
|
461
|
+
payload=desired,
|
|
462
|
+
)
|
|
463
|
+
return "created"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _route_dns_via_api(hostnames: List[str], tunnel_state: Dict[str, Any]) -> None:
|
|
467
|
+
cert_path = str(tunnel_state.get("cert_path") or "").strip()
|
|
468
|
+
tunnel_id = str(tunnel_state.get("tunnel_id") or "").strip()
|
|
469
|
+
if not cert_path or not tunnel_id:
|
|
470
|
+
raise RuntimeError("Cloudflare certificate or tunnel ID missing from state")
|
|
471
|
+
credential = extract_token_json(cert_path)
|
|
472
|
+
zone_id = str(credential.get("zoneID") or "").strip()
|
|
473
|
+
api_token = str(credential.get("apiToken") or "").strip()
|
|
474
|
+
if not zone_id or not api_token:
|
|
475
|
+
raise RuntimeError("Cloudflare zone/API credential missing from certificate")
|
|
476
|
+
|
|
477
|
+
unique_hostnames = list(dict.fromkeys(hostnames))
|
|
478
|
+
failures: List[str] = []
|
|
479
|
+
with ThreadPoolExecutor(max_workers=min(4, len(unique_hostnames))) as executor:
|
|
480
|
+
futures = {
|
|
481
|
+
executor.submit(
|
|
482
|
+
_upsert_tunnel_dns_record,
|
|
483
|
+
hostname,
|
|
484
|
+
zone_id=zone_id,
|
|
485
|
+
api_token=api_token,
|
|
486
|
+
tunnel_id=tunnel_id,
|
|
487
|
+
): hostname
|
|
488
|
+
for hostname in unique_hostnames
|
|
489
|
+
}
|
|
490
|
+
for future in as_completed(futures):
|
|
491
|
+
hostname = futures[future]
|
|
492
|
+
try:
|
|
493
|
+
action = future.result()
|
|
494
|
+
logger.info("Cloudflare DNS API hostname=%s action=%s", hostname, action)
|
|
495
|
+
except Exception as exc:
|
|
496
|
+
failures.append(f"{hostname}: {exc}")
|
|
497
|
+
if failures:
|
|
498
|
+
raise RuntimeError("; ".join(failures))
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _route_new_dns(hostnames: List[str], tunnel_state: Dict[str, Any]) -> None:
|
|
502
|
+
if not hostnames:
|
|
503
|
+
return
|
|
504
|
+
started = time.monotonic()
|
|
505
|
+
try:
|
|
506
|
+
_route_dns_via_api(hostnames, tunnel_state)
|
|
507
|
+
method = "api"
|
|
508
|
+
except Exception as exc:
|
|
509
|
+
logger.warning(
|
|
510
|
+
"Direct Cloudflare DNS update failed; using cloudflared fallback: %s",
|
|
511
|
+
exc,
|
|
512
|
+
)
|
|
513
|
+
_route_dns(hostnames, str(tunnel_state.get("tunnel_name") or ""))
|
|
514
|
+
method = "cloudflared"
|
|
515
|
+
logger.info(
|
|
516
|
+
"Expose ports timing stage=route_dns_all method=%s hostname_count=%s elapsed_s=%.3f",
|
|
517
|
+
method,
|
|
518
|
+
len(hostnames),
|
|
519
|
+
time.monotonic() - started,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
382
523
|
def _reload_cloudflared_service() -> None:
|
|
383
524
|
# Restart via init-system aware logic; sudo/root handled inside.
|
|
384
525
|
started = time.monotonic()
|
|
@@ -412,21 +553,6 @@ def _sanitize_rules(rules: List[Dict[str, Any]]) -> List[Dict[str, str]]:
|
|
|
412
553
|
return sanitized
|
|
413
554
|
|
|
414
555
|
|
|
415
|
-
def _node_device_id(tunnel_state: Dict[str, Any]) -> str:
|
|
416
|
-
explicit = str(tunnel_state.get("node_device_id") or "").strip()
|
|
417
|
-
if explicit.isdigit():
|
|
418
|
-
return explicit
|
|
419
|
-
match = re.fullmatch(r"portacode-proxmox-(\d+)", str(tunnel_state.get("tunnel_name") or ""))
|
|
420
|
-
if not match:
|
|
421
|
-
raise RuntimeError("Unable to determine node device ID for wildcard forwarding")
|
|
422
|
-
return match.group(1)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def _wildcard_hostname(tunnel_state: Dict[str, Any]) -> str:
|
|
426
|
-
domain = str(tunnel_state.get("domain") or "").strip().lower().rstrip(".")
|
|
427
|
-
return f"*.{_node_device_id(tunnel_state)}.{domain}"
|
|
428
|
-
|
|
429
|
-
|
|
430
556
|
def _apply_and_persist_forwarding_rules(
|
|
431
557
|
rules: List[Dict[str, Any]],
|
|
432
558
|
*,
|
|
@@ -455,24 +581,57 @@ def _apply_and_persist_forwarding_rules(
|
|
|
455
581
|
started = time.monotonic()
|
|
456
582
|
entries = _build_ingress_entries(rules, proxmox, node)
|
|
457
583
|
_timed_stage(operation_id, "build_ingress_entries", started, rule_count=len(rules))
|
|
458
|
-
started = time.monotonic()
|
|
459
|
-
_write_cloudflared_config(tunnel_state, entries)
|
|
460
|
-
_timed_stage(operation_id, "write_cloudflared_config", started, entry_count=len(entries))
|
|
461
584
|
forwarding_state = load_forwarding_state()
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
585
|
+
existing_hostnames = {
|
|
586
|
+
str(rule.get("hostname") or "").strip().lower().rstrip(".")
|
|
587
|
+
for rule in forwarding_state.get("rules", [])
|
|
588
|
+
if isinstance(rule, dict)
|
|
589
|
+
}
|
|
590
|
+
hostnames = [
|
|
591
|
+
entry["hostname"]
|
|
592
|
+
for entry in entries
|
|
593
|
+
if entry.get("hostname")
|
|
594
|
+
and str(entry["hostname"]).strip().lower().rstrip(".") not in existing_hostnames
|
|
595
|
+
]
|
|
596
|
+
|
|
597
|
+
cfg = tunnel_state.get("config_path")
|
|
598
|
+
config_path = Path(str(cfg)) if cfg else Path(default_config_path())
|
|
599
|
+
try:
|
|
600
|
+
previous_config = config_path.read_bytes()
|
|
601
|
+
previous_mode = config_path.stat().st_mode & 0o777
|
|
602
|
+
except OSError:
|
|
603
|
+
previous_config = None
|
|
604
|
+
previous_mode = 0o644
|
|
605
|
+
|
|
606
|
+
try:
|
|
466
607
|
started = time.monotonic()
|
|
467
|
-
|
|
468
|
-
_timed_stage(operation_id, "
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
608
|
+
_write_cloudflared_config(tunnel_state, entries)
|
|
609
|
+
_timed_stage(operation_id, "write_cloudflared_config", started, entry_count=len(entries))
|
|
610
|
+
_route_new_dns(hostnames, tunnel_state)
|
|
611
|
+
started = time.monotonic()
|
|
612
|
+
_reload_cloudflared_service()
|
|
613
|
+
_timed_stage(operation_id, "reload_cloudflared_call", started)
|
|
614
|
+
except Exception:
|
|
615
|
+
logger.exception(
|
|
616
|
+
"Forwarding update failed; restoring previous cloudflared config operation_id=%s",
|
|
617
|
+
operation_id,
|
|
618
|
+
)
|
|
619
|
+
if previous_config is not None:
|
|
620
|
+
try:
|
|
621
|
+
write_text(
|
|
622
|
+
config_path,
|
|
623
|
+
previous_config.decode("utf-8"),
|
|
624
|
+
mode=previous_mode,
|
|
625
|
+
)
|
|
626
|
+
_reload_cloudflared_service()
|
|
627
|
+
except Exception:
|
|
628
|
+
logger.exception(
|
|
629
|
+
"Failed to restore previous cloudflared config operation_id=%s",
|
|
630
|
+
operation_id,
|
|
631
|
+
)
|
|
632
|
+
raise
|
|
472
633
|
started = time.monotonic()
|
|
473
|
-
state = persist_forwarding_state(
|
|
474
|
-
rules, metadata={"wildcard_dns_hostname": desired_wildcard}
|
|
475
|
-
)
|
|
634
|
+
state = persist_forwarding_state(rules)
|
|
476
635
|
_timed_stage(operation_id, "persist_forwarding_state", started, rule_count=len(rules))
|
|
477
636
|
_timed_stage(operation_id, "apply_total", total_started, rule_count=len(rules))
|
|
478
637
|
return {
|
|
@@ -499,11 +658,16 @@ def _new_container_hostname(
|
|
|
499
658
|
index: int,
|
|
500
659
|
tunnel_state: Dict[str, Any],
|
|
501
660
|
) -> str:
|
|
502
|
-
node_id = _node_device_id(tunnel_state)
|
|
503
661
|
domain = str(tunnel_state.get("domain") or "").strip().lower().rstrip(".")
|
|
504
662
|
protocol_suffix = "-https" if protocol == "https" else ""
|
|
505
663
|
label = device_id if index == 0 else f"{device_id}-{port}{protocol_suffix}"
|
|
506
|
-
return f"{label}.{
|
|
664
|
+
return f"{label}.{domain}"
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _is_single_level_hostname(hostname: str, domain: str) -> bool:
|
|
668
|
+
normalized = str(hostname or "").strip().lower().rstrip(".")
|
|
669
|
+
suffix = f".{domain}"
|
|
670
|
+
return normalized.endswith(suffix) and "." not in normalized[: -len(suffix)]
|
|
507
671
|
|
|
508
672
|
|
|
509
673
|
def _normalize_container_rule_specs(
|
|
@@ -842,7 +1006,8 @@ def set_container_forwarding_rules(
|
|
|
842
1006
|
parsed = rule.get("parsed") or {}
|
|
843
1007
|
if parsed.get("type") != "device":
|
|
844
1008
|
continue
|
|
845
|
-
|
|
1009
|
+
if _is_single_level_hostname(rule["hostname"], domain):
|
|
1010
|
+
reusable_hostnames[(int(parsed["port"]), str(parsed["scheme"]))] = rule["hostname"]
|
|
846
1011
|
recorded_target_ip = str(rule.get("resolved_ip") or recorded_target_ip)
|
|
847
1012
|
new_rules: List[Dict[str, Any]] = []
|
|
848
1013
|
exposed_ports: List[Dict[str, Any]] = []
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest import TestCase
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from portacode.connection.handlers.cloudflare_forwarding import (
|
|
8
|
+
EXPOSED_SERVICES_JSON_PATH,
|
|
9
|
+
_build_exposed_services_env_map,
|
|
10
|
+
_sync_exposed_services_into_container,
|
|
11
|
+
_build_ingress_entries,
|
|
12
|
+
_new_container_hostname,
|
|
13
|
+
_apply_and_persist_forwarding_rules,
|
|
14
|
+
_route_new_dns,
|
|
15
|
+
_upsert_tunnel_dns_record,
|
|
16
|
+
set_container_forwarding_rules,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CloudflareForwardingEnvTests(TestCase):
|
|
21
|
+
def test_new_container_hostname_stays_at_certificate_covered_level(self):
|
|
22
|
+
state = {"domain": "example.com", "tunnel_name": "portacode-proxmox-6"}
|
|
23
|
+
self.assertEqual(_new_container_hostname("598", 3001, "http", 0, state), "598.example.com")
|
|
24
|
+
self.assertEqual(_new_container_hostname("598", 8000, "http", 1, state), "598-8000.example.com")
|
|
25
|
+
self.assertEqual(
|
|
26
|
+
_new_container_hostname("598", 8000, "https", 1, state),
|
|
27
|
+
"598-8000-https.example.com",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._load_leases", return_value=[])
|
|
31
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._resolve_device_vmid", return_value=141)
|
|
32
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._find_container_ip", side_effect=RuntimeError("offline"))
|
|
33
|
+
def test_ingress_uses_recorded_ip_when_container_is_offline(
|
|
34
|
+
self, _mock_find_ip, _mock_vmid, _mock_leases
|
|
35
|
+
):
|
|
36
|
+
rules = [{
|
|
37
|
+
"hostname": "598.6.example.com",
|
|
38
|
+
"destination": "http://[598]:3001",
|
|
39
|
+
"parsed": {"type": "device", "device_id": "598", "scheme": "http", "port": 3001, "path": ""},
|
|
40
|
+
"resolved_ip": "10.77.0.25",
|
|
41
|
+
}]
|
|
42
|
+
entries = _build_ingress_entries(rules, object(), "pve2")
|
|
43
|
+
self.assertEqual(entries[0]["service"], "http://10.77.0.25:3001")
|
|
44
|
+
|
|
45
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._load_leases", return_value=[])
|
|
46
|
+
@patch(
|
|
47
|
+
"portacode.connection.handlers.cloudflare_forwarding._resolve_device_vmid",
|
|
48
|
+
side_effect=RuntimeError("missing stale mapping"),
|
|
49
|
+
)
|
|
50
|
+
def test_ingress_uses_recorded_ip_when_neighbor_mapping_is_missing(
|
|
51
|
+
self, _mock_vmid, _mock_leases
|
|
52
|
+
):
|
|
53
|
+
rules = [{
|
|
54
|
+
"hostname": "900.example.com",
|
|
55
|
+
"destination": "http://[900]:8000",
|
|
56
|
+
"parsed": {"type": "device", "device_id": "900", "scheme": "http", "port": 8000, "path": ""},
|
|
57
|
+
"resolved_ip": "10.77.0.90",
|
|
58
|
+
}]
|
|
59
|
+
|
|
60
|
+
entries = _build_ingress_entries(rules, object(), "pve2")
|
|
61
|
+
|
|
62
|
+
self.assertEqual(entries[0]["service"], "http://10.77.0.90:8000")
|
|
63
|
+
|
|
64
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding.persist_forwarding_state", return_value={"updated_at": "now"})
|
|
65
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._reload_cloudflared_service")
|
|
66
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._route_new_dns")
|
|
67
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._write_cloudflared_config")
|
|
68
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._build_ingress_entries")
|
|
69
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding.load_forwarding_state")
|
|
70
|
+
def test_only_new_hostnames_are_routed(
|
|
71
|
+
self, mock_load, mock_build, _mock_write, mock_route, _mock_reload, _mock_persist
|
|
72
|
+
):
|
|
73
|
+
tunnel = {"domain": "example.com", "tunnel_name": "portacode-proxmox-6"}
|
|
74
|
+
mock_load.return_value = {
|
|
75
|
+
"rules": [{"hostname": "other-user.example.com", "destination": "http://[10]:80"}]
|
|
76
|
+
}
|
|
77
|
+
mock_build.return_value = [
|
|
78
|
+
{"hostname": "other-user.example.com", "service": "http://10.0.0.1:80"},
|
|
79
|
+
{"hostname": "new-user.example.com", "service": "http://10.0.0.2:80"},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
_apply_and_persist_forwarding_rules([], tunnel_state=tunnel)
|
|
83
|
+
|
|
84
|
+
mock_route.assert_called_once_with(["new-user.example.com"], tunnel)
|
|
85
|
+
|
|
86
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._cloudflare_api_request")
|
|
87
|
+
def test_dns_upsert_reuses_matching_record(self, mock_request):
|
|
88
|
+
mock_request.return_value = [
|
|
89
|
+
{
|
|
90
|
+
"id": "record-1",
|
|
91
|
+
"type": "CNAME",
|
|
92
|
+
"content": "tunnel-1.cfargotunnel.com",
|
|
93
|
+
"proxied": True,
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
action = _upsert_tunnel_dns_record(
|
|
98
|
+
"598.example.com",
|
|
99
|
+
zone_id="zone-1",
|
|
100
|
+
api_token="secret",
|
|
101
|
+
tunnel_id="tunnel-1",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self.assertEqual(action, "unchanged")
|
|
105
|
+
self.assertEqual(mock_request.call_count, 1)
|
|
106
|
+
|
|
107
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._cloudflare_api_request")
|
|
108
|
+
def test_dns_upsert_updates_conflicting_record(self, mock_request):
|
|
109
|
+
mock_request.side_effect = [[{"id": "record-1", "type": "A", "content": "192.0.2.1"}], {}]
|
|
110
|
+
|
|
111
|
+
action = _upsert_tunnel_dns_record(
|
|
112
|
+
"598.example.com",
|
|
113
|
+
zone_id="zone-1",
|
|
114
|
+
api_token="secret",
|
|
115
|
+
tunnel_id="tunnel-1",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.assertEqual(action, "updated")
|
|
119
|
+
self.assertEqual(mock_request.call_args_list[1].args[:2], ("PUT", "/zones/zone-1/dns_records/record-1"))
|
|
120
|
+
|
|
121
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._route_dns")
|
|
122
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._route_dns_via_api", side_effect=RuntimeError("no API"))
|
|
123
|
+
def test_dns_api_failure_falls_back_to_cloudflared(self, _mock_api, mock_cli):
|
|
124
|
+
tunnel = {"tunnel_name": "portacode-proxmox-6"}
|
|
125
|
+
|
|
126
|
+
_route_new_dns(["598.example.com"], tunnel)
|
|
127
|
+
|
|
128
|
+
mock_cli.assert_called_once_with(["598.example.com"], "portacode-proxmox-6")
|
|
129
|
+
|
|
130
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._sync_exposed_services_into_container")
|
|
131
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._get_node_from_config", return_value="pve2")
|
|
132
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._connect_proxmox", return_value=object())
|
|
133
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._ensure_infra_configured", return_value={})
|
|
134
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._apply_and_persist_forwarding_rules")
|
|
135
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding.load_forwarding_state")
|
|
136
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._load_tunnel_state")
|
|
137
|
+
def test_container_update_preserves_neighbor_and_migrates_nested_hostname(
|
|
138
|
+
self,
|
|
139
|
+
mock_tunnel,
|
|
140
|
+
mock_load,
|
|
141
|
+
mock_apply,
|
|
142
|
+
_mock_infra,
|
|
143
|
+
_mock_connect,
|
|
144
|
+
_mock_node,
|
|
145
|
+
_mock_sync,
|
|
146
|
+
):
|
|
147
|
+
mock_tunnel.return_value = {
|
|
148
|
+
"domain": "example.com",
|
|
149
|
+
"tunnel_name": "portacode-proxmox-6",
|
|
150
|
+
}
|
|
151
|
+
neighbor = {
|
|
152
|
+
"hostname": "900.example.com",
|
|
153
|
+
"destination": "http://[900]:8000",
|
|
154
|
+
"parsed": {
|
|
155
|
+
"type": "device",
|
|
156
|
+
"device_id": "900",
|
|
157
|
+
"scheme": "http",
|
|
158
|
+
"port": 8000,
|
|
159
|
+
"path": "",
|
|
160
|
+
},
|
|
161
|
+
"resolved_ip": "10.0.0.90",
|
|
162
|
+
}
|
|
163
|
+
old_target = {
|
|
164
|
+
"hostname": "598.6.example.com",
|
|
165
|
+
"destination": "http://[598]:3001",
|
|
166
|
+
"parsed": {
|
|
167
|
+
"type": "device",
|
|
168
|
+
"device_id": "598",
|
|
169
|
+
"scheme": "http",
|
|
170
|
+
"port": 3001,
|
|
171
|
+
"path": "",
|
|
172
|
+
},
|
|
173
|
+
"resolved_ip": "10.0.0.58",
|
|
174
|
+
}
|
|
175
|
+
mock_load.return_value = {"rules": [neighbor, old_target]}
|
|
176
|
+
mock_apply.side_effect = lambda rules, **_kwargs: {
|
|
177
|
+
"rules": rules,
|
|
178
|
+
"updated_at": "now",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
result = set_container_forwarding_rules(
|
|
182
|
+
"598",
|
|
183
|
+
[{"subdomain": "598", "port": 3001, "protocol": "http"}],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
applied_rules = mock_apply.call_args.args[0]
|
|
187
|
+
self.assertEqual(applied_rules[0], neighbor)
|
|
188
|
+
self.assertEqual(applied_rules[1]["hostname"], "598.example.com")
|
|
189
|
+
self.assertEqual(applied_rules[1]["resolved_ip"], "10.0.0.58")
|
|
190
|
+
self.assertEqual(result["exposed_ports"][0]["hostname"], "598.example.com")
|
|
191
|
+
|
|
192
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding.persist_forwarding_state")
|
|
193
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._reload_cloudflared_service")
|
|
194
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._route_new_dns", side_effect=RuntimeError("DNS failed"))
|
|
195
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._build_ingress_entries")
|
|
196
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding.load_forwarding_state", return_value={"rules": []})
|
|
197
|
+
def test_failed_update_restores_previous_config(
|
|
198
|
+
self, _mock_load, mock_build, _mock_route, mock_reload, mock_persist
|
|
199
|
+
):
|
|
200
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
201
|
+
config_path = Path(tmp) / "config.yml"
|
|
202
|
+
config_path.write_text("previous config\n", encoding="utf-8")
|
|
203
|
+
mock_build.return_value = [{"hostname": "new.example.com", "service": "http://10.0.0.2"}]
|
|
204
|
+
tunnel = {
|
|
205
|
+
"domain": "example.com",
|
|
206
|
+
"tunnel_name": "portacode-proxmox-6",
|
|
207
|
+
"tunnel_id": "tunnel-1",
|
|
208
|
+
"credentials_file": str(config_path),
|
|
209
|
+
"config_path": str(config_path),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
with self.assertRaisesRegex(RuntimeError, "DNS failed"):
|
|
213
|
+
_apply_and_persist_forwarding_rules([], tunnel_state=tunnel)
|
|
214
|
+
|
|
215
|
+
self.assertEqual(config_path.read_text(encoding="utf-8"), "previous config\n")
|
|
216
|
+
mock_reload.assert_called_once()
|
|
217
|
+
mock_persist.assert_not_called()
|
|
218
|
+
|
|
219
|
+
def test_build_exposed_services_env_map_adds_indexed_public_host_aliases(self):
|
|
220
|
+
env_map = _build_exposed_services_env_map(
|
|
221
|
+
[
|
|
222
|
+
{"hostname": "123.example.com", "url": "https://123.example.com", "port": 443},
|
|
223
|
+
{"hostname": "1_123.example.com", "url": "https://1_123.example.com", "port": 12321},
|
|
224
|
+
{"hostname": "2_123.example.com", "url": "https://2_123.example.com", "port": 12322},
|
|
225
|
+
]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self.assertEqual(env_map["PORTACODE_PUBLIC_HOST"], "123.example.com")
|
|
229
|
+
self.assertEqual(env_map["PORTACODE_PUBLIC_HOST_1"], "123.example.com")
|
|
230
|
+
self.assertEqual(env_map["PORTACODE_PUBLIC_HOST_2"], "1_123.example.com")
|
|
231
|
+
self.assertEqual(env_map["PORTACODE_PUBLIC_HOST_3"], "2_123.example.com")
|
|
232
|
+
|
|
233
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._push_root_file_to_container")
|
|
234
|
+
@patch("portacode.connection.handlers.cloudflare_forwarding._resolve_device_vmid", return_value=321)
|
|
235
|
+
def test_sync_exposed_services_into_container_pushes_only_canonical_json(
|
|
236
|
+
self,
|
|
237
|
+
_mock_resolve_vmid,
|
|
238
|
+
mock_push,
|
|
239
|
+
):
|
|
240
|
+
exposed_ports = [
|
|
241
|
+
{"hostname": "123.example.com", "url": "https://123.example.com", "port": 443},
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
_sync_exposed_services_into_container(
|
|
245
|
+
container_device_id="123",
|
|
246
|
+
exposed_ports=exposed_ports,
|
|
247
|
+
proxmox=object(),
|
|
248
|
+
node="pve",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
mock_push.assert_called_once()
|
|
252
|
+
vmid, path, data = mock_push.call_args.args[:3]
|
|
253
|
+
self.assertEqual(vmid, 321)
|
|
254
|
+
self.assertEqual(path, EXPOSED_SERVICES_JSON_PATH)
|
|
255
|
+
self.assertEqual(
|
|
256
|
+
json.loads(data.decode("utf-8")),
|
|
257
|
+
{
|
|
258
|
+
"device_id": "123",
|
|
259
|
+
"exposed_services": exposed_ports,
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
self.assertEqual(mock_push.call_args.kwargs["mode"], 0o644)
|