tina4-python 3.10.69__tar.gz → 3.10.70__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.
- {tina4_python-3.10.69 → tina4_python-3.10.70}/PKG-INFO +1 -1
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/CLAUDE.md +3 -3
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/__init__.py +1 -1
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/response.py +27 -1
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/server.py +74 -13
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/dev_admin/__init__.py +4 -2
- tina4_python-3.10.70/tina4_python/public/js/tina4-dev-admin.js +329 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/.gitignore +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/README.md +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/pyproject.toml +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/Testing.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/events.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/request.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/core/router.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/dev_reload.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_alerts.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_badges.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_buttons.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_cards.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_forms.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_grid.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_modals.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_nav.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_reset.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_tables.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_typography.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_utilities.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/_variables.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/base.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/colors.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/scss/tina4css/tina4.scss +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/websocket/__init__.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -470,6 +470,7 @@ return response("Not found", 404) # With status code
|
|
|
470
470
|
return response.redirect("/login") # Redirect
|
|
471
471
|
return response.render("page.twig", data) # Render Twig template
|
|
472
472
|
return response.file("doc.pdf") # Serve a file
|
|
473
|
+
return response.stream(generator) # SSE/streaming response (text/event-stream)
|
|
473
474
|
```
|
|
474
475
|
|
|
475
476
|
Add custom headers before returning:
|
|
@@ -1767,7 +1768,7 @@ async def dashboard(request, response):
|
|
|
1767
1768
|
|
|
1768
1769
|
## v3 Features Summary
|
|
1769
1770
|
|
|
1770
|
-
- **
|
|
1771
|
+
- **55 built-in features**, zero third-party dependencies
|
|
1771
1772
|
- **2,066 tests** passing across all modules
|
|
1772
1773
|
- **Production server auto-detect**: `tina4python serve --production` auto-installs uvicorn
|
|
1773
1774
|
- **`tina4python generate`**: model, route, migration, middleware scaffolding
|
|
@@ -1783,5 +1784,4 @@ async def dashboard(request, response):
|
|
|
1783
1784
|
- **SameSite=Lax** default on session cookies (`TINA4_SESSION_SAMESITE`)
|
|
1784
1785
|
- **`tina4 init`** generates Dockerfile and .dockerignore
|
|
1785
1786
|
- **Gallery**: 7 interactive examples with Try It deploy at `/__dev/`
|
|
1786
|
-
|
|
1787
|
-
|
|
1787
|
+
- **SSE/Streaming**: `response.stream()` for Server-Sent Events — pass an async generator, framework handles chunked transfer encoding and keep-alive
|
|
@@ -75,7 +75,7 @@ class Response:
|
|
|
75
75
|
|
|
76
76
|
__slots__ = (
|
|
77
77
|
"status_code", "content", "content_type",
|
|
78
|
-
"_headers", "_cookies",
|
|
78
|
+
"_headers", "_cookies", "_is_streaming", "_stream_source",
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
def __init__(self):
|
|
@@ -84,6 +84,8 @@ class Response:
|
|
|
84
84
|
self.content_type: str = "text/html; charset=utf-8"
|
|
85
85
|
self._headers: list[tuple[str, str]] = []
|
|
86
86
|
self._cookies: list[str] = []
|
|
87
|
+
self._is_streaming: bool = False
|
|
88
|
+
self._stream_source = None
|
|
87
89
|
|
|
88
90
|
def __call__(self, data=None, status_code: int = 200, content_type: str = None) -> "Response":
|
|
89
91
|
"""Smart callable — auto-detects content type from data.
|
|
@@ -156,6 +158,30 @@ class Response:
|
|
|
156
158
|
self._cookies.append("; ".join(parts))
|
|
157
159
|
return self
|
|
158
160
|
|
|
161
|
+
def stream(self, source, content_type: str = "text/event-stream") -> "Response":
|
|
162
|
+
"""Stream response from an async generator or sync iterable.
|
|
163
|
+
|
|
164
|
+
Usage (SSE):
|
|
165
|
+
@get("/events")
|
|
166
|
+
async def events(request, response):
|
|
167
|
+
async def generate():
|
|
168
|
+
for i in range(5):
|
|
169
|
+
yield f"data: message {i}\\n\\n"
|
|
170
|
+
await asyncio.sleep(1)
|
|
171
|
+
return response.stream(generate())
|
|
172
|
+
|
|
173
|
+
Usage (custom content type):
|
|
174
|
+
return response.stream(generate(), "application/octet-stream")
|
|
175
|
+
"""
|
|
176
|
+
self._is_streaming = True
|
|
177
|
+
self._stream_source = source
|
|
178
|
+
self.content_type = content_type
|
|
179
|
+
if content_type == "text/event-stream":
|
|
180
|
+
self._headers.append(("Cache-Control", "no-cache"))
|
|
181
|
+
self._headers.append(("Connection", "keep-alive"))
|
|
182
|
+
self._headers.append(("X-Accel-Buffering", "no"))
|
|
183
|
+
return self
|
|
184
|
+
|
|
159
185
|
def json(self, data, status_code: int = None) -> "Response":
|
|
160
186
|
"""JSON response."""
|
|
161
187
|
if status_code:
|
|
@@ -674,7 +674,13 @@ def _handle_rate_limit(request: Request, response: Response) -> Response | None:
|
|
|
674
674
|
async def _handle_dev_admin(request: Request, response: Response) -> Response:
|
|
675
675
|
"""Serve the /__dev dashboard and API routes."""
|
|
676
676
|
from tina4_python.dev_admin import get_api_handlers, render_dashboard
|
|
677
|
-
if request.path in ("/__dev/", "/__dev"):
|
|
677
|
+
if request.path in ("/__dev/v2", "/__dev/v2/"):
|
|
678
|
+
# New unified SPA dev admin
|
|
679
|
+
response.html("""<!DOCTYPE html>
|
|
680
|
+
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Tina4 Dev Admin</title></head>
|
|
681
|
+
<body><div id="app" data-framework="python" data-color="#3b82f6"></div>
|
|
682
|
+
<script src="/js/tina4-dev-admin.js"></script></body></html>""")
|
|
683
|
+
elif request.path in ("/__dev/", "/__dev"):
|
|
678
684
|
response.html(render_dashboard())
|
|
679
685
|
else:
|
|
680
686
|
handlers = get_api_handlers()
|
|
@@ -1043,12 +1049,43 @@ async def app(scope: dict, receive, send):
|
|
|
1043
1049
|
request = Request.from_scope(scope, body)
|
|
1044
1050
|
response = await handle(request)
|
|
1045
1051
|
|
|
1052
|
+
# Streaming responses bypass ETag/compression — send immediately
|
|
1053
|
+
_streaming = getattr(response, "_is_streaming", False)
|
|
1054
|
+
if _streaming:
|
|
1055
|
+
# Streaming response — send headers then stream chunks
|
|
1056
|
+
stream_headers = [
|
|
1057
|
+
(b"content-type", response.content_type.encode()),
|
|
1058
|
+
]
|
|
1059
|
+
for name, value in response._headers:
|
|
1060
|
+
stream_headers.append((name.lower().encode(), value.encode()))
|
|
1061
|
+
for cookie_str in response._cookies:
|
|
1062
|
+
stream_headers.append((b"set-cookie", cookie_str.encode()))
|
|
1063
|
+
await send({"type": "http.response.start", "status": response.status_code, "headers": stream_headers})
|
|
1064
|
+
|
|
1065
|
+
import asyncio
|
|
1066
|
+
source = response._stream_source
|
|
1067
|
+
if hasattr(source, "__aiter__"):
|
|
1068
|
+
# Async generator
|
|
1069
|
+
async for chunk in source:
|
|
1070
|
+
if isinstance(chunk, str):
|
|
1071
|
+
chunk = chunk.encode()
|
|
1072
|
+
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
1073
|
+
elif hasattr(source, "__iter__"):
|
|
1074
|
+
# Sync iterable
|
|
1075
|
+
for chunk in source:
|
|
1076
|
+
if isinstance(chunk, str):
|
|
1077
|
+
chunk = chunk.encode()
|
|
1078
|
+
await send({"type": "http.response.body", "body": chunk, "more_body": True})
|
|
1079
|
+
await asyncio.sleep(0) # yield control
|
|
1080
|
+
|
|
1081
|
+
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
|
1082
|
+
return
|
|
1083
|
+
|
|
1046
1084
|
# ETag check — 304 Not Modified
|
|
1047
1085
|
if_none_match = request.headers.get("if-none-match", "")
|
|
1048
1086
|
accept_encoding = request.headers.get("accept-encoding", "")
|
|
1049
1087
|
headers = response.build_headers(accept_encoding)
|
|
1050
1088
|
|
|
1051
|
-
# Check ETag after building (since build_headers computes it)
|
|
1052
1089
|
etag = ""
|
|
1053
1090
|
for name, value in headers:
|
|
1054
1091
|
if name == b"etag":
|
|
@@ -1503,26 +1540,50 @@ def run(host: str | None = None, port: int | None = None, no_browser: bool = Fal
|
|
|
1503
1540
|
async def receive():
|
|
1504
1541
|
return {"type": "http.request", "body": body, "more_body": False}
|
|
1505
1542
|
|
|
1543
|
+
_headers_sent = False
|
|
1544
|
+
|
|
1506
1545
|
async def send(msg):
|
|
1507
|
-
nonlocal resp_started, resp_status, resp_headers, resp_body
|
|
1546
|
+
nonlocal resp_started, resp_status, resp_headers, resp_body, _headers_sent
|
|
1508
1547
|
if msg["type"] == "http.response.start":
|
|
1509
1548
|
resp_started = True
|
|
1510
1549
|
resp_status = msg["status"]
|
|
1511
1550
|
resp_headers = msg.get("headers", [])
|
|
1512
1551
|
elif msg["type"] == "http.response.body":
|
|
1513
|
-
|
|
1552
|
+
chunk = msg.get("body", b"")
|
|
1553
|
+
more = msg.get("more_body", False)
|
|
1554
|
+
|
|
1555
|
+
if more or _headers_sent:
|
|
1556
|
+
# Streaming mode — flush headers on first chunk, then write each chunk immediately
|
|
1557
|
+
if not _headers_sent:
|
|
1558
|
+
_headers_sent = True
|
|
1559
|
+
writer.write(f"HTTP/1.1 {resp_status} OK\r\n".encode())
|
|
1560
|
+
for name, value in resp_headers:
|
|
1561
|
+
writer.write(name + b": " + value + b"\r\n")
|
|
1562
|
+
writer.write(b"\r\n")
|
|
1563
|
+
await writer.drain()
|
|
1564
|
+
|
|
1565
|
+
if chunk:
|
|
1566
|
+
writer.write(chunk)
|
|
1567
|
+
await writer.drain()
|
|
1568
|
+
|
|
1569
|
+
if not more:
|
|
1570
|
+
writer.close()
|
|
1571
|
+
else:
|
|
1572
|
+
# Buffered mode — accumulate body
|
|
1573
|
+
resp_body = chunk
|
|
1514
1574
|
|
|
1515
1575
|
await app(scope, receive, send)
|
|
1516
1576
|
|
|
1517
|
-
# Write HTTP/1.1 response
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1577
|
+
# Write HTTP/1.1 response (only if headers weren't already sent by streaming)
|
|
1578
|
+
if not _headers_sent:
|
|
1579
|
+
status_line = f"HTTP/1.1 {resp_status} OK\r\n"
|
|
1580
|
+
writer.write(status_line.encode())
|
|
1581
|
+
for name, value in resp_headers:
|
|
1582
|
+
writer.write(name + b": " + value + b"\r\n")
|
|
1583
|
+
writer.write(b"\r\n")
|
|
1584
|
+
writer.write(resp_body)
|
|
1585
|
+
await writer.drain()
|
|
1586
|
+
writer.close()
|
|
1526
1587
|
|
|
1527
1588
|
server = await start_server(_handle_connection, host, port)
|
|
1528
1589
|
|
|
@@ -515,11 +515,13 @@ async def _api_query(request, response):
|
|
|
515
515
|
is_read = upper.startswith("SELECT") or upper.startswith("PRAGMA") or upper.startswith("SHOW") or upper.startswith("DESCRIBE")
|
|
516
516
|
|
|
517
517
|
if is_read:
|
|
518
|
-
|
|
518
|
+
limit = int(body.get("limit", 100))
|
|
519
|
+
offset = int(body.get("offset", 0))
|
|
520
|
+
result = db.fetch(statements[0], limit=limit, offset=offset)
|
|
519
521
|
data = result.records
|
|
520
522
|
MessageLog.log("query", f"SQL: {statements[0][:80]}", {"rows": result.count}, level="info")
|
|
521
523
|
db.close()
|
|
522
|
-
return response({"rows": data, "count": result.count})
|
|
524
|
+
return response({"rows": data, "count": result.count, "limit": limit, "offset": offset})
|
|
523
525
|
|
|
524
526
|
# Execute all statements (single write or multi-statement batch)
|
|
525
527
|
total_affected = 0
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
(function(){"use strict";const Z={python:{color:"#3b82f6",name:"Python"},php:{color:"#8b5cf6",name:"PHP"},ruby:{color:"#ef4444",name:"Ruby"},nodejs:{color:"#22c55e",name:"Node.js"}};function Et(){const t=document.getElementById("app"),n=(t==null?void 0:t.dataset.framework)??"python",o=t==null?void 0:t.dataset.color,r=Z[n]??Z.python;return{framework:n,color:o??r.color,name:r.name}}function Tt(t){const n=document.documentElement;n.style.setProperty("--primary",t.color),n.style.setProperty("--bg","#0f172a"),n.style.setProperty("--surface","#1e293b"),n.style.setProperty("--border","#334155"),n.style.setProperty("--text","#e2e8f0"),n.style.setProperty("--muted","#94a3b8"),n.style.setProperty("--success","#22c55e"),n.style.setProperty("--danger","#ef4444"),n.style.setProperty("--warn","#f59e0b"),n.style.setProperty("--info","#3b82f6")}const Mt=`
|
|
2
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); }
|
|
4
|
+
|
|
5
|
+
.dev-admin { display: flex; flex-direction: column; height: 100vh; }
|
|
6
|
+
.dev-header { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 1rem; background: var(--surface); border-bottom: 1px solid var(--border); }
|
|
7
|
+
.dev-header h1 { font-size: 1rem; font-weight: 700; }
|
|
8
|
+
.dev-header h1 span { color: var(--primary); }
|
|
9
|
+
|
|
10
|
+
.dev-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); background: var(--surface); padding: 0 0.5rem; overflow-x: auto; }
|
|
11
|
+
.dev-tab { padding: 0.5rem 0.75rem; border: none; background: none; color: var(--muted); cursor: pointer; font-size: 0.8rem; font-weight: 500; white-space: nowrap; border-bottom: 2px solid transparent; transition: all 0.15s; }
|
|
12
|
+
.dev-tab:hover { color: var(--text); }
|
|
13
|
+
.dev-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
|
14
|
+
|
|
15
|
+
.dev-content { flex: 1; overflow-y: auto; }
|
|
16
|
+
.dev-panel { padding: 1rem; display: none; }
|
|
17
|
+
.dev-panel.active { display: block; }
|
|
18
|
+
.dev-panel-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
|
|
19
|
+
.dev-panel-header h2 { font-size: 0.95rem; font-weight: 600; }
|
|
20
|
+
|
|
21
|
+
.btn { padding: 0.35rem 0.75rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--surface); color: var(--text); cursor: pointer; font-size: 0.8rem; transition: all 0.15s; height: 30px; line-height: 1; }
|
|
22
|
+
.btn:hover { background: var(--border); }
|
|
23
|
+
.btn-primary { background: var(--primary); border-color: var(--primary); color: white; }
|
|
24
|
+
.btn-primary:hover { opacity: 0.9; }
|
|
25
|
+
.btn-danger { background: var(--danger); border-color: var(--danger); color: white; }
|
|
26
|
+
.btn-sm { padding: 0.2rem 0.5rem; font-size: 0.75rem; }
|
|
27
|
+
|
|
28
|
+
.input { padding: 0.35rem 0.5rem; border: 1px solid var(--border); border-radius: 0.375rem; background: var(--bg); color: var(--text); font-size: 0.8rem; height: 30px; }
|
|
29
|
+
select.input { height: 30px; }
|
|
30
|
+
.input:focus { outline: none; border-color: var(--primary); }
|
|
31
|
+
textarea.input { font-family: "SF Mono", "Fira Code", Consolas, monospace; resize: vertical; height: auto; }
|
|
32
|
+
|
|
33
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
34
|
+
th { text-align: left; padding: 0.5rem; color: var(--muted); font-weight: 600; border-bottom: 1px solid var(--border); }
|
|
35
|
+
td { padding: 0.5rem; border-bottom: 1px solid var(--border); }
|
|
36
|
+
tr:hover { background: rgba(255,255,255,0.03); }
|
|
37
|
+
|
|
38
|
+
.badge { display: inline-block; padding: 0.1rem 0.4rem; border-radius: 9999px; font-size: 0.7rem; font-weight: 600; }
|
|
39
|
+
.badge-success { background: rgba(34,197,94,0.15); color: var(--success); }
|
|
40
|
+
.badge-danger { background: rgba(239,68,68,0.15); color: var(--danger); }
|
|
41
|
+
.badge-warn { background: rgba(245,158,11,0.15); color: var(--warn); }
|
|
42
|
+
.badge-info { background: rgba(59,130,246,0.15); color: var(--info); }
|
|
43
|
+
.badge-muted { background: rgba(148,163,184,0.15); color: var(--muted); }
|
|
44
|
+
|
|
45
|
+
.method { font-weight: 700; font-size: 0.7rem; padding: 0.1rem 0.3rem; border-radius: 0.2rem; }
|
|
46
|
+
.method-get { color: var(--success); }
|
|
47
|
+
.method-post { color: var(--info); }
|
|
48
|
+
.method-put { color: var(--warn); }
|
|
49
|
+
.method-patch { color: var(--warn); }
|
|
50
|
+
.method-delete { color: var(--danger); }
|
|
51
|
+
.method-any { color: var(--muted); }
|
|
52
|
+
|
|
53
|
+
.flex { display: flex; }
|
|
54
|
+
.gap-sm { gap: 0.5rem; }
|
|
55
|
+
.items-center { align-items: center; }
|
|
56
|
+
.text-mono { font-family: "SF Mono", "Fira Code", Consolas, monospace; }
|
|
57
|
+
.text-sm { font-size: 0.8rem; }
|
|
58
|
+
.text-muted { color: var(--muted); }
|
|
59
|
+
.empty-state { text-align: center; padding: 2rem; color: var(--muted); }
|
|
60
|
+
|
|
61
|
+
.metric-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 0.75rem; margin-bottom: 1rem; }
|
|
62
|
+
.metric-card { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0.75rem; }
|
|
63
|
+
.metric-card .label { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
64
|
+
.metric-card .value { font-size: 1.5rem; font-weight: 700; margin-top: 0.25rem; }
|
|
65
|
+
|
|
66
|
+
.chat-container { display: flex; flex-direction: column; height: calc(100vh - 140px); }
|
|
67
|
+
.chat-messages { flex: 1; overflow-y: auto; padding: 0.75rem; }
|
|
68
|
+
.chat-msg { padding: 0.5rem 0.75rem; border-radius: 0.5rem; margin-bottom: 0.5rem; font-size: 0.85rem; line-height: 1.5; max-width: 85%; }
|
|
69
|
+
.chat-user { background: var(--primary); color: white; margin-left: auto; }
|
|
70
|
+
.chat-bot { background: var(--surface); border: 1px solid var(--border); }
|
|
71
|
+
.chat-input-row { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid var(--border); }
|
|
72
|
+
.chat-input-row input { flex: 1; }
|
|
73
|
+
|
|
74
|
+
.error-trace { background: var(--bg); border: 1px solid var(--border); border-radius: 0.375rem; padding: 0.5rem; font-family: monospace; font-size: 0.75rem; white-space: pre-wrap; max-height: 200px; overflow-y: auto; margin-top: 0.5rem; }
|
|
75
|
+
|
|
76
|
+
.bubble-chart { width: 100%; height: 400px; background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; overflow: hidden; }
|
|
77
|
+
`,St="/__dev/api";async function x(t,n="GET",o){const r={method:n,headers:{}};return o&&(r.headers["Content-Type"]="application/json",r.body=JSON.stringify(o)),(await fetch(St+t,r)).json()}function s(t){const n=document.createElement("span");return n.textContent=t,n.innerHTML}function It(t){t.innerHTML=`
|
|
78
|
+
<div class="dev-panel-header">
|
|
79
|
+
<h2>Routes <span id="routes-count" class="text-muted text-sm"></span></h2>
|
|
80
|
+
<button class="btn btn-sm" onclick="window.__loadRoutes()">Refresh</button>
|
|
81
|
+
</div>
|
|
82
|
+
<table>
|
|
83
|
+
<thead><tr><th>Method</th><th>Path</th><th>Auth</th><th>Handler</th></tr></thead>
|
|
84
|
+
<tbody id="routes-body"></tbody>
|
|
85
|
+
</table>
|
|
86
|
+
`,tt()}async function tt(){const t=await x("/routes"),n=document.getElementById("routes-count");n&&(n.textContent=`(${t.count})`);const o=document.getElementById("routes-body");o&&(o.innerHTML=(t.routes||[]).map(r=>`
|
|
87
|
+
<tr>
|
|
88
|
+
<td><span class="method method-${r.method.toLowerCase()}">${s(r.method)}</span></td>
|
|
89
|
+
<td class="text-mono"><a href="${s(r.path)}" target="_blank" style="color:inherit;text-decoration:underline dotted">${s(r.path)}</a></td>
|
|
90
|
+
<td>${r.auth_required?'<span class="badge badge-warn">auth</span>':'<span class="badge badge-success">open</span>'}</td>
|
|
91
|
+
<td class="text-sm text-muted">${s(r.handler||"")} <small>(${s(r.module||"")})</small></td>
|
|
92
|
+
</tr>
|
|
93
|
+
`).join(""))}window.__loadRoutes=tt;let H=[],z=[],S=JSON.parse(localStorage.getItem("tina4_query_history")||"[]");function Lt(t){t.innerHTML=`
|
|
94
|
+
<div class="dev-panel-header">
|
|
95
|
+
<h2>Database</h2>
|
|
96
|
+
<button class="btn btn-sm" onclick="window.__loadTables()">Refresh</button>
|
|
97
|
+
</div>
|
|
98
|
+
<div style="display:flex;gap:1rem;height:calc(100vh - 140px)">
|
|
99
|
+
<div style="width:200px;flex-shrink:0;overflow-y:auto;border-right:1px solid var(--border);padding-right:0.75rem">
|
|
100
|
+
<div style="font-weight:600;font-size:0.75rem;color:var(--muted);text-transform:uppercase;margin-bottom:0.5rem">Tables</div>
|
|
101
|
+
<div id="db-table-list"></div>
|
|
102
|
+
<div style="margin-top:1.5rem;border-top:1px solid var(--border);padding-top:0.75rem">
|
|
103
|
+
<div style="font-weight:600;font-size:0.75rem;color:var(--muted);text-transform:uppercase;margin-bottom:0.5rem">Seed Data</div>
|
|
104
|
+
<select id="db-seed-table" class="input" style="width:100%;margin-bottom:0.5rem">
|
|
105
|
+
<option value="">Pick table...</option>
|
|
106
|
+
</select>
|
|
107
|
+
<div class="flex gap-sm">
|
|
108
|
+
<input type="number" id="db-seed-count" class="input" value="10" style="width:60px">
|
|
109
|
+
<button class="btn btn-sm btn-primary" onclick="window.__seedTable()">Seed</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div style="flex:1;display:flex;flex-direction:column;min-width:0">
|
|
114
|
+
<div class="flex gap-sm items-center" style="margin-bottom:0.5rem;flex-wrap:wrap">
|
|
115
|
+
<select id="db-type" class="input" style="width:80px">
|
|
116
|
+
<option value="sql">SQL</option>
|
|
117
|
+
<option value="graphql">GraphQL</option>
|
|
118
|
+
</select>
|
|
119
|
+
<span class="text-sm text-muted">Limit</span>
|
|
120
|
+
<select id="db-limit" class="input" style="width:60px">
|
|
121
|
+
<option value="20">20</option>
|
|
122
|
+
<option value="50">50</option>
|
|
123
|
+
<option value="100">100</option>
|
|
124
|
+
<option value="500">500</option>
|
|
125
|
+
</select>
|
|
126
|
+
<span class="text-sm text-muted">Offset</span>
|
|
127
|
+
<input type="number" id="db-offset" class="input" value="0" style="width:60px" min="0">
|
|
128
|
+
<button class="btn btn-primary" onclick="window.__runQuery()">Run</button>
|
|
129
|
+
<button class="btn" onclick="window.__copyCSV()">Copy CSV</button>
|
|
130
|
+
<button class="btn" onclick="window.__copyJSON()">Copy JSON</button>
|
|
131
|
+
<button class="btn" onclick="window.__showPaste()">Paste</button>
|
|
132
|
+
<span class="text-sm text-muted">Ctrl+Enter</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="flex gap-sm items-center" style="margin-bottom:0.25rem">
|
|
135
|
+
<select id="db-history" class="input text-mono" style="flex:1" onchange="window.__loadHistory(this.value)">
|
|
136
|
+
<option value="">Query history...</option>
|
|
137
|
+
</select>
|
|
138
|
+
<button class="btn btn-sm" onclick="window.__clearHistory()" title="Clear history" style="height:30px">Clear</button>
|
|
139
|
+
</div>
|
|
140
|
+
<textarea id="db-query" class="input text-mono" style="width:100%;height:80px;resize:vertical" placeholder="SELECT * FROM users" onkeydown="if(event.ctrlKey&&event.key==='Enter')window.__runQuery()"></textarea>
|
|
141
|
+
<div id="db-result" style="flex:1;overflow:auto;margin-top:0.75rem"></div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div id="db-paste-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:1000;display:none;align-items:center;justify-content:center">
|
|
145
|
+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:1.5rem;width:600px;max-height:80vh;overflow:auto">
|
|
146
|
+
<h3 style="margin-bottom:0.75rem;font-size:0.9rem">Paste Data</h3>
|
|
147
|
+
<p class="text-sm text-muted" style="margin-bottom:0.5rem">Paste CSV or JSON array. First row = column headers for CSV.</p>
|
|
148
|
+
<div class="flex gap-sm items-center" style="margin-bottom:0.5rem">
|
|
149
|
+
<select id="paste-table" class="input" style="flex:1"><option value="">Select existing table...</option></select>
|
|
150
|
+
<span class="text-sm text-muted">or</span>
|
|
151
|
+
<input type="text" id="paste-new-table" class="input" placeholder="New table name..." style="flex:1">
|
|
152
|
+
</div>
|
|
153
|
+
<textarea id="paste-data" class="input text-mono" style="width:100%;height:200px" placeholder='CSV data or JSON'></textarea>
|
|
154
|
+
<div class="flex gap-sm" style="margin-top:0.75rem;justify-content:flex-end">
|
|
155
|
+
<button class="btn" onclick="window.__hidePaste()">Cancel</button>
|
|
156
|
+
<button class="btn btn-primary" onclick="window.__doPaste()">Import</button>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
`,X(),J()}async function X(){const n=(await x("/tables")).tables||[],o=document.getElementById("db-table-list");o&&(o.innerHTML=n.length?n.map(l=>`<div style="padding:0.3rem 0.5rem;cursor:pointer;border-radius:0.25rem;font-size:0.8rem;font-family:monospace" class="db-table-item" onclick="window.__selectTable('${s(l)}')" onmouseover="this.style.background='var(--border)'" onmouseout="this.style.background=''">${s(l)}</div>`).join(""):'<div class="text-sm text-muted">No tables</div>');const r=document.getElementById("db-seed-table");r&&(r.innerHTML='<option value="">Pick table...</option>'+n.map(l=>`<option value="${s(l)}">${s(l)}</option>`).join(""));const a=document.getElementById("paste-table");a&&(a.innerHTML='<option value="">Select table...</option>'+n.map(l=>`<option value="${s(l)}">${s(l)}</option>`).join(""))}function Y(t){var o;(o=document.getElementById("db-limit"))!=null&&o.value;const n=document.getElementById("db-query");n&&(n.value=`SELECT * FROM ${t}`),document.querySelectorAll(".db-table-item").forEach(r=>{r.style.background=r.textContent===t?"var(--border)":""}),et()}function Ct(){var o;const t=document.getElementById("db-query"),n=((o=document.getElementById("db-limit"))==null?void 0:o.value)||"20";t!=null&&t.value&&(t.value=t.value.replace(/LIMIT\s+\d+/i,`LIMIT ${n}`))}function At(t){const n=t.trim();n&&(S=S.filter(o=>o!==n),S.unshift(n),S.length>50&&(S=S.slice(0,50)),localStorage.setItem("tina4_query_history",JSON.stringify(S)),J())}function J(){const t=document.getElementById("db-history");t&&(t.innerHTML='<option value="">Query history...</option>'+S.map((n,o)=>`<option value="${o}">${s(n.length>80?n.substring(0,80)+"...":n)}</option>`).join(""))}function Bt(t){const n=parseInt(t);if(isNaN(n)||!S[n])return;const o=document.getElementById("db-query");o&&(o.value=S[n]),document.getElementById("db-history").selectedIndex=0}function Ht(){S=[],localStorage.removeItem("tina4_query_history"),J()}async function et(){var a,l,h;const t=document.getElementById("db-query"),n=(a=t==null?void 0:t.value)==null?void 0:a.trim();if(!n)return;At(n);const o=document.getElementById("db-result"),r=((l=document.getElementById("db-type"))==null?void 0:l.value)||"sql";o&&(o.innerHTML='<p class="text-muted">Running...</p>');try{const w=parseInt(((h=document.getElementById("db-limit"))==null?void 0:h.value)||"20"),v=await x("/query","POST",{query:n,type:r,limit:w});if(v.error){o&&(o.innerHTML=`<p style="color:var(--danger)">${s(v.error)}</p>`);return}v.rows&&v.rows.length>0?(z=Object.keys(v.rows[0]),H=v.rows,o&&(o.innerHTML=`<p class="text-sm text-muted" style="margin-bottom:0.5rem">${v.count??v.rows.length} rows</p>
|
|
161
|
+
<div style="overflow-x:auto"><table><thead><tr>${z.map(_=>`<th>${s(_)}</th>`).join("")}</tr></thead>
|
|
162
|
+
<tbody>${v.rows.map(_=>`<tr>${z.map(b=>`<td class="text-sm">${s(String(_[b]??""))}</td>`).join("")}</tr>`).join("")}</tbody></table></div>`)):v.affected!==void 0?(o&&(o.innerHTML=`<p class="text-muted">${v.affected} rows affected. ${v.success?"Success.":""}</p>`),H=[],z=[]):(o&&(o.innerHTML='<p class="text-muted">No results</p>'),H=[],z=[])}catch(w){o&&(o.innerHTML=`<p style="color:var(--danger)">${s(w.message)}</p>`)}}function zt(){if(!H.length)return;const t=z.join(","),n=H.map(o=>z.map(r=>{const a=String(o[r]??"");return a.includes(",")||a.includes('"')?`"${a.replace(/"/g,'""')}"`:a}).join(","));navigator.clipboard.writeText([t,...n].join(`
|
|
163
|
+
`))}function Pt(){H.length&&navigator.clipboard.writeText(JSON.stringify(H,null,2))}function Ot(){const t=document.getElementById("db-paste-modal");t&&(t.style.display="flex")}function nt(){const t=document.getElementById("db-paste-modal");t&&(t.style.display="none")}async function jt(){var a,l,h,w,v;const t=(a=document.getElementById("paste-table"))==null?void 0:a.value,n=(h=(l=document.getElementById("paste-new-table"))==null?void 0:l.value)==null?void 0:h.trim(),o=n||t,r=(v=(w=document.getElementById("paste-data"))==null?void 0:w.value)==null?void 0:v.trim();if(!o||!r){alert("Select a table or enter a new table name, and paste data.");return}try{let _;try{_=JSON.parse(r),Array.isArray(_)||(_=[_])}catch{const M=r.split(`
|
|
164
|
+
`).map($=>$.trim()).filter(Boolean);if(M.length<2){alert("CSV needs at least a header row and one data row.");return}const C=M[0].split(",").map($=>$.trim().replace(/[^a-zA-Z0-9_]/g,""));_=M.slice(1).map($=>{const I=$.split(",").map(k=>k.trim()),A={};return C.forEach((k,P)=>{A[k]=I[P]??""}),A})}if(!_.length){alert("No data rows found.");return}if(n){const C=["id INTEGER PRIMARY KEY AUTOINCREMENT",...Object.keys(_[0]).filter(I=>I.toLowerCase()!=="id").map(I=>`"${I}" TEXT`)],$=await x("/query","POST",{query:`CREATE TABLE IF NOT EXISTS "${n}" (${C.join(", ")})`,type:"sql"});if($.error){alert("Create table failed: "+$.error);return}}let b=0;for(const M of _){const C=n?Object.keys(M).filter(k=>k.toLowerCase()!=="id"):Object.keys(M),$=C.map(k=>`"${k}"`).join(","),I=C.map(k=>`'${String(M[k]).replace(/'/g,"''")}'`).join(","),A=await x("/query","POST",{query:`INSERT INTO "${o}" (${$}) VALUES (${I})`,type:"sql"});if(A.error){alert(`Row ${b+1} failed: ${A.error}`);break}b++}document.getElementById("paste-data").value="",document.getElementById("paste-new-table").value="",document.getElementById("paste-table").selectedIndex=0,nt(),X(),b>0&&Y(o)}catch(_){alert("Import error: "+_.message)}}async function Nt(){var o,r;const t=(o=document.getElementById("db-seed-table"))==null?void 0:o.value,n=parseInt(((r=document.getElementById("db-seed-count"))==null?void 0:r.value)||"10");if(t)try{const a=await x("/seed","POST",{table:t,count:n});a.error?alert(a.error):Y(t)}catch(a){alert("Seed error: "+a.message)}}window.__loadTables=X,window.__selectTable=Y,window.__updateLimit=Ct,window.__runQuery=et,window.__copyCSV=zt,window.__copyJSON=Pt,window.__showPaste=Ot,window.__hidePaste=nt,window.__doPaste=jt,window.__seedTable=Nt,window.__loadHistory=Bt,window.__clearHistory=Ht;function Rt(t){t.innerHTML=`
|
|
165
|
+
<div class="dev-panel-header">
|
|
166
|
+
<h2>Errors <span id="errors-count" class="text-muted text-sm"></span></h2>
|
|
167
|
+
<div class="flex gap-sm">
|
|
168
|
+
<button class="btn btn-sm" onclick="window.__loadErrors()">Refresh</button>
|
|
169
|
+
<button class="btn btn-sm btn-danger" onclick="window.__clearErrors()">Clear All</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div id="errors-body"></div>
|
|
173
|
+
`,V()}async function V(){const t=await x("/broken"),n=document.getElementById("errors-count"),o=document.getElementById("errors-body");if(!o)return;const r=t.errors||[];if(n&&(n.textContent=`(${r.length})`),!r.length){o.innerHTML='<div class="empty-state">No errors</div>';return}o.innerHTML=r.map((a,l)=>`
|
|
174
|
+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:0.75rem;margin-bottom:0.75rem">
|
|
175
|
+
<div class="flex items-center" style="justify-content:space-between">
|
|
176
|
+
<div>
|
|
177
|
+
<span class="badge badge-danger">UNRESOLVED</span>
|
|
178
|
+
<strong style="margin-left:0.5rem;font-size:0.85rem">${s(a.error||a.message||"Unknown error")}</strong>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="flex gap-sm">
|
|
181
|
+
<button class="btn btn-sm" onclick="window.__resolveError('${s(a.id||String(l))}')">Resolve</button>
|
|
182
|
+
<button class="btn btn-sm btn-primary" onclick="window.__askAboutError(${l})">Ask Tina4</button>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
${a.traceback?`<div class="error-trace">${s(a.traceback)}</div>`:""}
|
|
186
|
+
<div class="text-sm text-muted" style="margin-top:0.5rem">${s(a.timestamp||"")}</div>
|
|
187
|
+
</div>
|
|
188
|
+
`).join(""),window.__errorData=r}async function qt(t){await x("/broken/resolve","POST",{id:t}),V()}async function Ft(){await x("/broken/clear","POST"),V()}function Dt(t){const o=(window.__errorData||[])[t];if(!o)return;const r=document.querySelector('[data-tab="chat"]');r&&r.click(),setTimeout(()=>{const a=document.getElementById("chat-input");a&&(a.value=`I have this error: ${o.error||o.message}
|
|
189
|
+
|
|
190
|
+
${o.traceback||""}`,a.focus())},100)}window.__loadErrors=V,window.__clearErrors=Ft,window.__resolveError=qt,window.__askAboutError=Dt;function Vt(t){t.innerHTML=`
|
|
191
|
+
<div class="dev-panel-header">
|
|
192
|
+
<h2>System</h2>
|
|
193
|
+
<button class="btn btn-sm" onclick="window.__loadSystem()">Refresh</button>
|
|
194
|
+
</div>
|
|
195
|
+
<div id="system-grid" class="metric-grid"></div>
|
|
196
|
+
`,ot()}async function ot(){const t=await x("/system"),n=document.getElementById("system-grid");if(!n)return;const o=[{label:"Framework",value:t.framework||"Tina4"},{label:"Version",value:t.version||"?"},{label:"Runtime",value:t.runtime||t.python_version||t.php_version||t.ruby_version||t.node_version||"?"},{label:"Database",value:t.database||t.db_type||"none"},{label:"Uptime",value:t.uptime||"?"},{label:"Memory",value:t.memory||"?"},{label:"Platform",value:t.platform||"?"},{label:"Routes",value:String(t.route_count??t.routes??"?")},{label:"Debug",value:t.debug?"ON":"OFF"}];n.innerHTML=o.map(r=>`
|
|
197
|
+
<div class="metric-card">
|
|
198
|
+
<div class="label">${s(r.label)}</div>
|
|
199
|
+
<div class="value" style="font-size:1.1rem">${s(r.value)}</div>
|
|
200
|
+
</div>
|
|
201
|
+
`).join("")}window.__loadSystem=ot;function Qt(t){t.innerHTML=`
|
|
202
|
+
<div class="dev-panel-header">
|
|
203
|
+
<h2>Code Metrics</h2>
|
|
204
|
+
<div class="flex gap-sm">
|
|
205
|
+
<button class="btn" onclick="window.__loadQuickMetrics()">Quick Scan</button>
|
|
206
|
+
<button class="btn btn-primary" onclick="window.__loadFullMetrics()">Full Analysis</button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
<div id="metrics-quick" class="metric-grid"></div>
|
|
210
|
+
<div id="metrics-scan-info" class="text-sm text-muted" style="margin:0.5rem 0"></div>
|
|
211
|
+
<div id="metrics-chart" style="display:none;margin:1rem 0"></div>
|
|
212
|
+
<div id="metrics-complex" style="margin-top:1rem"></div>
|
|
213
|
+
<div id="metrics-detail" style="margin-top:1rem"></div>
|
|
214
|
+
`,rt()}async function Xt(){const t=await x("/metrics"),n=document.getElementById("metrics-quick");!n||t.error||(n.innerHTML=[y("Files",t.file_count),y("Lines of Code",t.total_loc),y("Blank Lines",t.total_blank),y("Comments",t.total_comment),y("Classes",t.classes),y("Functions",t.functions),y("Routes",t.route_count),y("ORM Models",t.orm_count),y("Templates",t.template_count),y("Migrations",t.migration_count),y("Avg File Size",(t.avg_file_size??0)+" LOC")].join(""))}async function rt(){var l;const t=document.getElementById("metrics-chart"),n=document.getElementById("metrics-complex"),o=document.getElementById("metrics-scan-info");t&&(t.style.display="block",t.innerHTML='<p class="text-muted">Analyzing...</p>');const r=await x("/metrics/full");if(r.error||!r.file_metrics){t&&(t.innerHTML=`<p style="color:var(--danger)">${s(r.error||"No data")}</p>`);return}o&&(o.textContent=`${r.files_analyzed} files analyzed | ${r.total_functions} functions | Mode: ${r.scan_mode||"project"}`);const a=document.getElementById("metrics-quick");a&&(a.innerHTML=[y("Files Analyzed",r.files_analyzed),y("Total Functions",r.total_functions),y("Avg Complexity",r.avg_complexity),y("Avg Maintainability",r.avg_maintainability),y("Scan Mode",r.scan_mode||"project")].join("")),t&&r.file_metrics.length>0?Yt(r.file_metrics,t,r.dependency_graph||{}):t&&(t.innerHTML='<p class="text-muted">No files to visualize</p>'),n&&((l=r.most_complex_functions)!=null&&l.length)&&(n.innerHTML=`
|
|
215
|
+
<h3 style="font-size:0.85rem;margin-bottom:0.5rem">Most Complex Functions</h3>
|
|
216
|
+
<table>
|
|
217
|
+
<thead><tr><th>Function</th><th>File</th><th>Line</th><th>Complexity</th><th>LOC</th></tr></thead>
|
|
218
|
+
<tbody>${r.most_complex_functions.slice(0,15).map(h=>`
|
|
219
|
+
<tr>
|
|
220
|
+
<td class="text-mono">${s(h.name)}</td>
|
|
221
|
+
<td class="text-sm text-muted" style="cursor:pointer;text-decoration:underline dotted" onclick="window.__drillDown('${s(h.file)}')">${s(h.file)}</td>
|
|
222
|
+
<td>${h.line}</td>
|
|
223
|
+
<td><span class="${h.complexity>10?"badge badge-danger":h.complexity>5?"badge badge-warn":"badge badge-success"}">${h.complexity}</span></td>
|
|
224
|
+
<td>${h.loc}</td>
|
|
225
|
+
</tr>`).join("")}
|
|
226
|
+
</tbody>
|
|
227
|
+
</table>
|
|
228
|
+
`)}function Yt(t,n,o){var ht,vt,ft,xt,wt,_t;n.clientWidth;const r=450,a=Math.max(...t.map(e=>e.loc||1)),l=18,h=50,w=1e3,v=1e3,b=[...t].sort((e,i)=>{const c=(e.avg_complexity??0)*2+(e.loc||0);return(i.avg_complexity??0)*2+(i.loc||0)-c}).map(e=>({...e,r:Math.max(l,Math.min(h,Math.sqrt((e.loc||1)/a)*h)),x:w,y:v}));for(let e=0;e<b.length;e++){if(e===0)continue;let i=0,c=0,m=!1;for(;!m;){const d=w+Math.cos(i)*c,g=v+Math.sin(i)*c;let p=!1;for(let f=0;f<e;f++){const L=d-b[f].x,D=g-b[f].y;if(Math.sqrt(L*L+D*D)<b[e].r+b[f].r+4){p=!0;break}}p||(b[e].x=d,b[e].y=g,m=!0),i+=.3,c+=.5}}let M=1/0,C=-1/0,$=1/0,I=-1/0;for(const e of b)M=Math.min(M,e.x-e.r-15),C=Math.max(C,e.x+e.r+15),$=Math.min($,e.y-e.r-15),I=Math.max(I,e.y+e.r+25);const A=30,k=M-A,P=$-A,O=C-M+A*2,j=I-$+A*2,B=Math.max(20,Math.round(Math.max(O,j)/20));n.innerHTML=`
|
|
229
|
+
<div style="position:relative;display:flex;gap:0">
|
|
230
|
+
<div style="flex:1;position:relative">
|
|
231
|
+
<div style="position:absolute;top:8px;left:8px;z-index:2;display:flex;gap:4px;flex-direction:column">
|
|
232
|
+
<button class="btn btn-sm" id="metrics-zoom-in" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">+</button>
|
|
233
|
+
<button class="btn btn-sm" id="metrics-zoom-out" style="width:28px;height:28px;padding:0;font-size:14px;font-weight:700;line-height:1">−</button>
|
|
234
|
+
<button class="btn btn-sm" id="metrics-zoom-fit" style="width:28px;height:28px;padding:0;font-size:10px;font-weight:700;line-height:1">Fit</button>
|
|
235
|
+
</div>
|
|
236
|
+
<svg id="metrics-svg" width="100%" height="${r}" viewBox="${k} ${P} ${O} ${j}" style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;cursor:grab"></svg>
|
|
237
|
+
</div>
|
|
238
|
+
<div id="metrics-hover-panel" style="width:200px;flex-shrink:0;background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:0.75rem;font-size:0.75rem;margin-left:0.5rem;overflow-y:auto;height:${r}px">
|
|
239
|
+
<div class="text-muted" style="text-align:center;padding-top:2rem">Hover a bubble<br>to see stats</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
`;const E=document.getElementById("metrics-svg");if(!E)return;const W={};for(const e of b)e.path&&(W[e.path]={x:e.x,y:e.y,r:e.r});let T="";const mt=Math.floor((k-O)/B)*B,ut=Math.ceil((k+O*3)/B)*B,pt=Math.floor((P-j)/B)*B,bt=Math.ceil((P+j*3)/B)*B;T+='<g class="metrics-grid">';for(let e=mt;e<=ut;e+=B)T+=`<line x1="${e}" y1="${pt}" x2="${e}" y2="${bt}" stroke="var(--border)" stroke-width="0.5" stroke-opacity="0.4" />`;for(let e=pt;e<=bt;e+=B)T+=`<line x1="${mt}" y1="${e}" x2="${ut}" y2="${e}" stroke="var(--border)" stroke-width="0.5" stroke-opacity="0.4" />`;T+="</g>",T+='<g class="dep-lines">';for(const[e,i]of Object.entries(o)){const c=W[e];if(c)for(const m of i){const d=Object.entries(W).find(([g])=>{var f;const p=((f=g.split("/").pop())==null?void 0:f.replace(/\.\w+$/,""))||"";return g===m||p===m||g.endsWith("/"+m)||g.endsWith("/"+m+".py")});if(d){const[,g]=d;T+=`<line x1="${c.x}" y1="${c.y}" x2="${g.x}" y2="${g.y}" stroke="var(--info)" stroke-width="1" stroke-opacity="0.3" stroke-dasharray="4 3" />`}}}T+="</g>";for(const e of b){const i=e.maintainability??50,m=`hsl(${Math.min(120,Math.max(0,i*1.2))}, 80%, 45%)`,d=((ht=e.path)==null?void 0:ht.split("/").pop())||"?",g=e.has_tests===!0,p=e.dep_count??0;if(T+=`<circle cx="${e.x}" cy="${e.y}" r="${e.r}" fill="${m}" fill-opacity="0.6" stroke="${m}" stroke-width="1.5" style="cursor:pointer" data-drill="${s(e.path)}" />`,T+=`<title>${s(e.path)}
|
|
243
|
+
LOC: ${e.loc} | CC: ${e.avg_complexity} | MI: ${i}${g?" | Tested":""}${p>0?" | Deps: "+p:""}</title>`,e.r>15){const f=d.length>12?d.substring(0,10)+"..":d;T+=`<text x="${e.x}" y="${e.y+2}" text-anchor="middle" fill="white" font-size="8" font-weight="600" style="pointer-events:none" data-for="${s(e.path)}" data-role="label">${s(f)}</text>`}if(g){const f=e.x+e.r*.6,L=e.y-e.r*.6;T+=`<circle cx="${f}" cy="${L}" r="7" fill="var(--success)" stroke="var(--surface)" stroke-width="1" data-for="${s(e.path)}" data-role="t-circle" />`,T+=`<text x="${f}" y="${L+3}" text-anchor="middle" fill="white" font-size="7" font-weight="700" style="pointer-events:none" data-for="${s(e.path)}" data-role="t-text">T</text>`}if(p>0){const f=e.x-e.r*.6,L=e.y-e.r*.6;T+=`<circle cx="${f}" cy="${L}" r="7" fill="var(--info)" stroke="var(--surface)" stroke-width="1" data-for="${s(e.path)}" data-role="d-circle" />`,T+=`<text x="${f}" y="${L+3}" text-anchor="middle" fill="white" font-size="7" font-weight="700" style="pointer-events:none" data-for="${s(e.path)}" data-role="d-text">D</text>`}}E.innerHTML=T;let q=!1,Q=!1,N=null,F={x:0,y:0,vbX:0,vbY:0},u={x:k,y:P,w:O,h:j};const ee={x:k,y:P,w:O,h:j},gt=4,ne=document.getElementById("metrics-hover-panel");function K(){E.setAttribute("viewBox",`${u.x} ${u.y} ${u.w} ${u.h}`)}function yt(e){const i=u.x+u.w/2,c=u.y+u.h/2;u.w*=e,u.h*=e,u.x=i-u.w/2,u.y=c-u.h/2,K()}function oe(e,i){const c=E.getBoundingClientRect();return{x:u.x+(e-c.left)/c.width*u.w,y:u.y+(i-c.top)/c.height*u.h}}function re(){E.querySelectorAll(".dep-lines line").forEach(i=>i.remove());const e=E.querySelector(".dep-lines");if(e)for(const[i,c]of Object.entries(o)){const m=b.find(d=>d.path===i);if(m)for(const d of c){const g=b.find(p=>{var L,D,$t,kt;const f=((D=(L=p.path)==null?void 0:L.split("/").pop())==null?void 0:D.replace(/\.\w+$/,""))||"";return p.path===d||f===d||(($t=p.path)==null?void 0:$t.endsWith("/"+d))||((kt=p.path)==null?void 0:kt.endsWith("/"+d+".py"))});if(g){const p=document.createElementNS("http://www.w3.org/2000/svg","line");p.setAttribute("x1",String(m.x)),p.setAttribute("y1",String(m.y)),p.setAttribute("x2",String(g.x)),p.setAttribute("y2",String(g.y)),p.setAttribute("stroke","var(--info)"),p.setAttribute("stroke-width","1"),p.setAttribute("stroke-opacity","0.3"),p.setAttribute("stroke-dasharray","4 3"),e.appendChild(p)}}}E.querySelectorAll("[data-drill]").forEach(i=>{const c=i.getAttribute("data-drill"),m=b.find(d=>d.path===c);m&&(i.setAttribute("cx",String(m.x)),i.setAttribute("cy",String(m.y)))}),E.querySelectorAll("[data-for]").forEach(i=>{const c=i.getAttribute("data-for"),m=i.getAttribute("data-role"),d=b.find(g=>g.path===c);d&&(m==="label"?(i.setAttribute("x",String(d.x)),i.setAttribute("y",String(d.y+2))):m==="t-circle"?(i.setAttribute("cx",String(d.x+d.r*.6)),i.setAttribute("cy",String(d.y-d.r*.6))):m==="t-text"?(i.setAttribute("x",String(d.x+d.r*.6)),i.setAttribute("y",String(d.y-d.r*.6+3))):m==="d-circle"?(i.setAttribute("cx",String(d.x-d.r*.6)),i.setAttribute("cy",String(d.y-d.r*.6))):m==="d-text"&&(i.setAttribute("x",String(d.x-d.r*.6)),i.setAttribute("y",String(d.y-d.r*.6+3))))})}function ie(e){const i=e.maintainability??0,m=`hsl(${Math.min(120,Math.max(0,i*1.2))}, 80%, 45%)`;ne.innerHTML=`
|
|
244
|
+
<div style="font-weight:700;font-size:0.85rem;margin-bottom:0.5rem;word-break:break-all">${s(e.path||"?")}</div>
|
|
245
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:0.5rem">
|
|
246
|
+
<div><span class="text-muted">LOC</span><br><strong>${e.loc??0}</strong></div>
|
|
247
|
+
<div><span class="text-muted">Lines</span><br><strong>${e.total_lines??e.loc??0}</strong></div>
|
|
248
|
+
<div><span class="text-muted">Complexity</span><br><strong>${e.avg_complexity??0}</strong></div>
|
|
249
|
+
<div><span class="text-muted">MI</span><br><strong style="color:${m}">${i}</strong></div>
|
|
250
|
+
<div><span class="text-muted">Functions</span><br><strong>${e.function_count??0}</strong></div>
|
|
251
|
+
<div><span class="text-muted">Deps</span><br><strong>${e.dep_count??0}</strong></div>
|
|
252
|
+
</div>
|
|
253
|
+
<div style="margin-bottom:0.25rem">${e.has_tests?'<span class="badge badge-success">Tested</span>':'<span class="badge badge-muted">No tests</span>'}</div>
|
|
254
|
+
${(e.dep_count??0)>0?'<div><span class="badge badge-info">'+e.dep_count+" dependencies</span></div>":""}
|
|
255
|
+
<div style="margin-top:0.75rem;font-size:0.7rem;color:var(--muted)">Click to drill down</div>
|
|
256
|
+
`}E.querySelectorAll("[data-drill]").forEach(e=>{e.addEventListener("mouseenter",()=>{const i=e.getAttribute("data-drill"),c=b.find(m=>m.path===i);c&&ie(c)}),e.addEventListener("click",i=>{if(q)return;i.stopPropagation();const c=e.getAttribute("data-drill");c&&it(c)})}),E.addEventListener("mousedown",e=>{var m;if(e.button!==0)return;q=!1;const i=e.target,c=(m=i==null?void 0:i.getAttribute)==null?void 0:m.call(i,"data-drill");if(c){const d=b.find(g=>g.path===c);if(d){N=d,E.style.cursor="move",e.preventDefault();return}}Q=!0,F={x:e.clientX,y:e.clientY,vbX:u.x,vbY:u.y}}),window.addEventListener("mousemove",e=>{if(N){q=!0;const d=oe(e.clientX,e.clientY);N.x=d.x,N.y=d.y,re();return}if(!Q)return;const i=e.clientX-F.x,c=e.clientY-F.y;if(!q&&Math.abs(i)<gt&&Math.abs(c)<gt)return;q=!0,E.style.cursor="grabbing";const m=E.getBoundingClientRect();u.x=F.vbX-i/m.width*u.w,u.y=F.vbY-c/m.height*u.h,K()}),window.addEventListener("mouseup",()=>{N&&(N=null,E.style.cursor="grab"),Q&&(Q=!1,E.style.cursor="grab")}),(vt=document.getElementById("metrics-zoom-in"))==null||vt.addEventListener("click",()=>yt(.7)),(ft=document.getElementById("metrics-zoom-out"))==null||ft.addEventListener("click",()=>yt(1.4)),(xt=document.getElementById("metrics-zoom-fit"))==null||xt.addEventListener("click",()=>{u={...ee},K()});const G=document.createElement("div");G.style.cssText="position:absolute;bottom:8px;left:8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:6px 10px;font-size:11px;line-height:1.6;opacity:0.9;z-index:2",G.innerHTML=`
|
|
257
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:hsl(0,80%,45%);vertical-align:middle"></span> Low MI
|
|
258
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:hsl(60,80%,45%);vertical-align:middle"></span> Med
|
|
259
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:hsl(120,80%,45%);vertical-align:middle"></span> High MI
|
|
260
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--success);vertical-align:middle"></span> T
|
|
261
|
+
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--info);vertical-align:middle"></span> D
|
|
262
|
+
<span style="color:var(--info)">---</span> Dep
|
|
263
|
+
`,(_t=(wt=n.querySelector("div > div:first-child"))==null?void 0:wt.parentElement)==null||_t.appendChild(G)}async function it(t){const n=document.getElementById("metrics-detail");if(!n)return;n.innerHTML='<p class="text-muted">Loading file analysis...</p>';const o=await x("/metrics/file?path="+encodeURIComponent(t));if(o.error){n.innerHTML=`<p style="color:var(--danger)">${s(o.error)}</p>`;return}const r=o.functions||[],a=o.warnings||[];n.innerHTML=`
|
|
264
|
+
<div style="background:var(--surface);border:1px solid var(--border);border-radius:0.5rem;padding:1rem">
|
|
265
|
+
<div class="flex items-center" style="justify-content:space-between;margin-bottom:0.75rem">
|
|
266
|
+
<h3 style="font-size:0.9rem">${s(o.path)}</h3>
|
|
267
|
+
<button class="btn btn-sm" onclick="document.getElementById('metrics-detail').innerHTML=''">Close</button>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="metric-grid" style="margin-bottom:0.75rem">
|
|
270
|
+
${y("LOC",o.loc)}
|
|
271
|
+
${y("Total Lines",o.total_lines)}
|
|
272
|
+
${y("Classes",o.classes)}
|
|
273
|
+
${y("Functions",r.length)}
|
|
274
|
+
</div>
|
|
275
|
+
${r.length?`
|
|
276
|
+
<table>
|
|
277
|
+
<thead><tr><th>Function</th><th>Line</th><th>Complexity</th><th>LOC</th><th>Args</th></tr></thead>
|
|
278
|
+
<tbody>${r.map(l=>`
|
|
279
|
+
<tr>
|
|
280
|
+
<td class="text-mono">${s(l.name)}</td>
|
|
281
|
+
<td>${l.line}</td>
|
|
282
|
+
<td><span class="${l.complexity>10?"badge badge-danger":l.complexity>5?"badge badge-warn":"badge badge-success"}">${l.complexity}</span></td>
|
|
283
|
+
<td>${l.loc}</td>
|
|
284
|
+
<td class="text-sm text-muted">${(l.args||[]).join(", ")}</td>
|
|
285
|
+
</tr>`).join("")}
|
|
286
|
+
</tbody>
|
|
287
|
+
</table>
|
|
288
|
+
`:'<p class="text-muted">No functions</p>'}
|
|
289
|
+
${a.length?`
|
|
290
|
+
<div style="margin-top:0.75rem">
|
|
291
|
+
<h4 style="font-size:0.8rem;color:var(--warn);margin-bottom:0.25rem">Warnings</h4>
|
|
292
|
+
${a.map(l=>`<p class="text-sm" style="color:var(--warn)">Line ${l.line}: ${s(l.message)}</p>`).join("")}
|
|
293
|
+
</div>
|
|
294
|
+
`:""}
|
|
295
|
+
</div>
|
|
296
|
+
`}function y(t,n){return`<div class="metric-card"><div class="label">${s(t)}</div><div class="value">${s(String(n??0))}</div></div>`}window.__loadQuickMetrics=Xt,window.__loadFullMetrics=rt,window.__drillDown=it;let at="anthropic",R="";function Jt(t){t.innerHTML=`
|
|
297
|
+
<div class="dev-panel-header">
|
|
298
|
+
<h2>Code With Me</h2>
|
|
299
|
+
<div class="flex gap-sm items-center">
|
|
300
|
+
<select id="ai-provider" class="input" style="width:120px" onchange="window.__setProvider(this.value)">
|
|
301
|
+
<option value="anthropic">Claude</option>
|
|
302
|
+
<option value="openai">OpenAI</option>
|
|
303
|
+
<option value="ollama">Ollama</option>
|
|
304
|
+
</select>
|
|
305
|
+
<input type="password" id="ai-key" class="input" placeholder="API key..." style="width:200px">
|
|
306
|
+
<button class="btn btn-sm btn-primary" onclick="window.__setAiKey()">Set</button>
|
|
307
|
+
<span class="text-sm text-muted" id="ai-status">${R?"Key set":"No key"}</span>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
<div class="chat-container">
|
|
311
|
+
<div class="chat-messages" id="chat-messages">
|
|
312
|
+
<div class="chat-msg chat-bot">Hi! I'm Tina4. Ask me to build routes, templates, models — or ask questions about your project. I can read and write files directly.</div>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="chat-input-row">
|
|
315
|
+
<input type="text" id="chat-input" class="input" placeholder="Ask Tina4 to build something..." onkeydown="if(event.key==='Enter')window.__sendChat()" style="flex:1">
|
|
316
|
+
<button class="btn btn-primary" onclick="window.__sendChat()">Send</button>
|
|
317
|
+
<button class="btn btn-sm" onclick="window.__undoChat()" title="Undo last file change">Undo</button>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
`}async function Ut(){var a;const t=document.getElementById("chat-input"),n=(a=t==null?void 0:t.value)==null?void 0:a.trim();if(!n)return;t.value="";const o=document.getElementById("chat-messages");if(!o)return;o.innerHTML+=`<div class="chat-msg chat-user">${s(n)}</div>`,o.innerHTML+='<div class="chat-msg chat-bot" id="chat-loading" style="color:var(--muted)">Thinking...</div>',o.scrollTop=o.scrollHeight;const r={message:n,provider:at};R&&(r.api_key=R);try{const l=await x("/chat","POST",r),h=document.getElementById("chat-loading");h&&h.remove();let w=Zt(l.reply||"No response");l.files_changed&&l.files_changed.length>0&&(w+='<div style="margin-top:0.5rem;padding:0.5rem;background:var(--bg);border-radius:0.375rem;border:1px solid var(--border)">',w+='<div class="text-sm" style="color:var(--success);font-weight:600;margin-bottom:0.25rem">Files changed:</div>',l.files_changed.forEach(v=>{w+=`<div class="text-sm text-mono">${s(v)}</div>`}),w+="</div>"),o.innerHTML+=`<div class="chat-msg chat-bot">${w}</div>`,o.innerHTML+=`<div class="text-sm text-muted" style="text-align:right;margin-bottom:0.25rem">${s(l.source||"")}</div>`,o.scrollTop=o.scrollHeight}catch{const l=document.getElementById("chat-loading");l&&(l.textContent="Error connecting",l.id="")}}async function Wt(){try{const t=await x("/chat/undo","POST"),n=document.getElementById("chat-messages");n&&(n.innerHTML+=`<div class="chat-msg chat-bot" style="color:var(--warn)">${s(t.message||"Undo complete")}</div>`,n.scrollTop=n.scrollHeight)}catch{alert("Nothing to undo")}}function Kt(){const t=document.getElementById("ai-key");R=(t==null?void 0:t.value)||"";const n=document.getElementById("ai-status");n&&(n.textContent=R?"Key set":"No key")}function Gt(t){at=t}function Zt(t){return t.replace(/```(\w*)\n([\s\S]*?)```/g,'<pre style="background:var(--bg);padding:0.5rem;border-radius:0.375rem;overflow-x:auto;margin:0.5rem 0;font-size:0.8rem"><code>$2</code></pre>').replace(/`([^`]+)`/g,'<code style="background:var(--bg);padding:0.1rem 0.25rem;border-radius:0.2rem;font-size:0.8em">$1</code>').replace(/\n/g,"<br>")}window.__sendChat=Ut,window.__undoChat=Wt,window.__setAiKey=Kt,window.__setProvider=Gt;const st=document.createElement("style");st.textContent=Mt,document.head.appendChild(st);const dt=Et();Tt(dt);const lt=[{id:"chat",label:"Code With Me",render:Jt},{id:"routes",label:"Routes",render:It},{id:"database",label:"Database",render:Lt},{id:"errors",label:"Errors",render:Rt},{id:"metrics",label:"Metrics",render:Qt},{id:"system",label:"System",render:Vt}];let U="chat";function te(){const t=document.getElementById("app");if(!t)return;t.innerHTML=`
|
|
321
|
+
<div class="dev-admin">
|
|
322
|
+
<div class="dev-header">
|
|
323
|
+
<h1><span>Tina4</span> Dev Admin</h1>
|
|
324
|
+
<span class="text-sm text-muted">${dt.name} • v3.10</span>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="dev-tabs" id="tab-bar"></div>
|
|
327
|
+
<div class="dev-content" id="tab-content"></div>
|
|
328
|
+
</div>
|
|
329
|
+
`;const n=document.getElementById("tab-bar");n.innerHTML=lt.map(o=>`<button class="dev-tab ${o.id===U?"active":""}" data-tab="${o.id}" onclick="window.__switchTab('${o.id}')">${o.label}</button>`).join(""),ct(U)}function ct(t){U=t,document.querySelectorAll(".dev-tab").forEach(a=>{a.classList.toggle("active",a.dataset.tab===t)});const n=document.getElementById("tab-content");if(!n)return;const o=document.createElement("div");o.className="dev-panel active",n.innerHTML="",n.appendChild(o);const r=lt.find(a=>a.id===t);r&&r.render(o)}window.__switchTab=ct,te()})();
|
|
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
|
|
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
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/public/swagger/oauth2-redirect.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
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
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/templates/docker/python/Dockerfile
RENAMED
|
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
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.10.69 → tina4_python-3.10.70}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|