From 903669d6eab5eb8f2324eed5d1e27577d16e4d2d Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 24 Nov 2025 09:58:56 +0000 Subject: [PATCH] ger --- .perm_test_apache | 0 .perm_test_exec | 0 ai/__init__.py | 3 + ai/local_ai_api.py | 420 ++++++++++++++++++ config/__pycache__/__init__.cpython-311.pyc | Bin 159 -> 159 bytes config/__pycache__/settings.cpython-311.pyc | Bin 4210 -> 4210 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1143 -> 1288 bytes config/__pycache__/wsgi.cpython-311.pyc | Bin 679 -> 679 bytes config/urls.py | 9 +- core/__pycache__/__init__.cpython-311.pyc | Bin 157 -> 157 bytes core/__pycache__/admin.cpython-311.pyc | Bin 777 -> 745 bytes core/__pycache__/apps.cpython-311.pyc | Bin 524 -> 524 bytes core/__pycache__/forms.cpython-311.pyc | Bin 847 -> 1073 bytes core/__pycache__/models.cpython-311.pyc | Bin 1409 -> 1499 bytes core/__pycache__/urls.cpython-311.pyc | Bin 347 -> 665 bytes core/__pycache__/views.cpython-311.pyc | Bin 2006 -> 4233 bytes core/admin.py | 12 +- core/context_processors.py | 10 + core/forms.py | 14 +- core/management/__init__.py | 0 core/management/commands/__init__.py | 0 core/management/commands/run_simulation.py | 35 ++ core/migrations/0002_unit.py | 30 ++ core/migrations/0003_seed_units.py | 37 ++ .../__pycache__/0001_initial.cpython-311.pyc | Bin 1660 -> 1660 bytes .../__pycache__/0002_unit.cpython-311.pyc | Bin 0 -> 1642 bytes .../0003_seed_units.cpython-311.pyc | Bin 0 -> 1832 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 168 -> 168 bytes core/models.py | 36 +- core/templates/base.html | 50 +++ core/templates/core/article_detail.html | 14 + core/templates/core/dashboard.html | 52 +++ core/templates/core/simulation_control.html | 28 ++ core/templates/core/unit_form.html | 36 ++ core/templates/core/unit_list.html | 47 ++ core/urls.py | 9 +- core/views.py | 86 ++-- static/css/custom.css | 100 +++++ staticfiles/css/custom.css | 74 ++- 39 files changed, 1023 insertions(+), 79 deletions(-) create mode 100644 .perm_test_apache create mode 100644 .perm_test_exec create mode 100644 ai/__init__.py create mode 100644 ai/local_ai_api.py create mode 100644 core/context_processors.py create mode 100644 core/management/__init__.py create mode 100644 core/management/commands/__init__.py create mode 100644 core/management/commands/run_simulation.py create mode 100644 core/migrations/0002_unit.py create mode 100644 core/migrations/0003_seed_units.py create mode 100644 core/migrations/__pycache__/0002_unit.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0003_seed_units.cpython-311.pyc create mode 100644 core/templates/base.html create mode 100644 core/templates/core/article_detail.html create mode 100644 core/templates/core/dashboard.html create mode 100644 core/templates/core/simulation_control.html create mode 100644 core/templates/core/unit_form.html create mode 100644 core/templates/core/unit_list.html create mode 100644 static/css/custom.css diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..37a7b09 --- /dev/null +++ b/ai/__init__.py @@ -0,0 +1,3 @@ +"""Helpers for interacting with the Flatlogic AI proxy from Django code.""" + +from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401 diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py new file mode 100644 index 0000000..bcff732 --- /dev/null +++ b/ai/local_ai_api.py @@ -0,0 +1,420 @@ +""" +LocalAIApi — lightweight Python client for the Flatlogic AI proxy. + +Usage (inside the Django workspace): + + from ai.local_ai_api import LocalAIApi + + response = LocalAIApi.create_response({ + "input": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Summarise this text in two sentences."}, + ], + "text": {"format": {"type": "json_object"}}, + }) + + if response.get("success"): + data = LocalAIApi.decode_json_from_response(response) + # ... + +# Typical successful payload (truncated): +# { +# "id": "resp_xxx", +# "status": "completed", +# "output": [ +# {"type": "reasoning", "summary": []}, +# {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]} +# ], +# "usage": { "input_tokens": 123, "output_tokens": 456 } +# } + +The helper automatically injects the project UUID header and falls back to +reading executor/.env if environment variables are missing. +""" + +from __future__ import annotations + +import json +import os +import time +import ssl +from typing import Any, Dict, Iterable, Optional +from urllib import error as urlerror +from urllib import request as urlrequest + +__all__ = [ + "LocalAIApi", + "create_response", + "request", + "fetch_status", + "await_response", + "extract_text", + "decode_json_from_response", +] + + +_CONFIG_CACHE: Optional[Dict[str, Any]] = None + + +class LocalAIApi: + """Static helpers mirroring the PHP implementation.""" + + @staticmethod + def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return create_response(params, options or {}) + + @staticmethod + def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, + options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return request(path, payload or {}, options or {}) + + @staticmethod + def extract_text(response: Dict[str, Any]) -> str: + return extract_text(response) + + @staticmethod + def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + return decode_json_from_response(response) + + +def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Signature compatible with the OpenAI Responses API.""" + options = options or {} + payload = dict(params) + + if not isinstance(payload.get("input"), list) or not payload["input"]: + return { + "success": False, + "error": "input_missing", + "message": 'Parameter "input" is required and must be a non-empty list.', + } + + cfg = _config() + if not payload.get("model"): + payload["model"] = cfg["default_model"] + + initial = request(options.get("path"), payload, options) + if not initial.get("success"): + return initial + + data = initial.get("data") + if isinstance(data, dict) and "ai_request_id" in data: + ai_request_id = data["ai_request_id"] + poll_timeout = int(options.get("poll_timeout", 300)) + poll_interval = int(options.get("poll_interval", 5)) + return await_response(ai_request_id, { + "interval": poll_interval, + "timeout": poll_timeout, + "headers": options.get("headers"), + "timeout_per_call": options.get("timeout"), + }) + + return initial + + +def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Perform a raw request to the AI proxy.""" + cfg = _config() + options = options or {} + + resolved_path = path or options.get("path") or cfg["responses_path"] + if not resolved_path: + return { + "success": False, + "error": "project_id_missing", + "message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", + } + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting AI request.", + } + + if "project_uuid" not in payload and project_uuid: + payload["project_uuid"] = project_uuid + + url = _build_url(resolved_path, cfg["base_url"]) + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + return _http_request(url, "POST", body, headers, timeout, verify_tls) + + +def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Fetch status for a queued AI request.""" + cfg = _config() + options = options or {} + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting status check.", + } + + status_path = _resolve_status_path(ai_request_id, cfg) + url = _build_url(status_path, cfg["base_url"]) + + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + return _http_request(url, "GET", None, headers, timeout, verify_tls) + + +def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Poll status endpoint until the request is complete or timed out.""" + options = options or {} + timeout = int(options.get("timeout", 300)) + interval = int(options.get("interval", 5)) + if interval <= 0: + interval = 5 + per_call_timeout = options.get("timeout_per_call") + + deadline = time.time() + max(timeout, interval) + + while True: + status_resp = fetch_status(ai_request_id, { + "headers": options.get("headers"), + "timeout": per_call_timeout, + "verify_tls": options.get("verify_tls"), + }) + if status_resp.get("success"): + data = status_resp.get("data") or {} + if isinstance(data, dict): + status_value = data.get("status") + if status_value == "success": + return { + "success": True, + "status": 200, + "data": data.get("response", data), + } + if status_value == "failed": + return { + "success": False, + "status": 500, + "error": str(data.get("error") or "AI request failed"), + "data": data, + } + else: + return status_resp + + if time.time() >= deadline: + return { + "success": False, + "error": "timeout", + "message": "Timed out waiting for AI response.", + } + time.sleep(interval) + + +def extract_text(response: Dict[str, Any]) -> str: + """Public helper to extract plain text from a Responses payload.""" + return _extract_text(response) + + +def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Attempt to decode JSON emitted by the model (handles markdown fences).""" + text = _extract_text(response) + if text == "": + return None + + try: + decoded = json.loads(text) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + pass + + stripped = text.strip() + if stripped.startswith("```json"): + stripped = stripped[7:] + if stripped.endswith("```"): + stripped = stripped[:-3] + stripped = stripped.strip() + if stripped and stripped != text: + try: + decoded = json.loads(stripped) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + return None + return None + + +def _extract_text(response: Dict[str, Any]) -> str: + payload = response.get("data") if response.get("success") else response.get("response") + if isinstance(payload, dict): + output = payload.get("output") + if isinstance(output, list): + combined = "" + for item in output: + content = item.get("content") if isinstance(item, dict) else None + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"): + combined += str(block["text"]) + if combined: + return combined + choices = payload.get("choices") + if isinstance(choices, list) and choices: + message = choices[0].get("message") + if isinstance(message, dict) and message.get("content"): + return str(message["content"]) + if isinstance(payload, str): + return payload + return "" + + +def _config() -> Dict[str, Any]: + global _CONFIG_CACHE # noqa: PLW0603 + if _CONFIG_CACHE is not None: + return _CONFIG_CACHE + + _ensure_env_loaded() + + base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com") + project_id = os.getenv("PROJECT_ID") or None + responses_path = os.getenv("AI_RESPONSES_PATH") + if not responses_path and project_id: + responses_path = f"/projects/{project_id}/ai-request" + + _CONFIG_CACHE = { + "base_url": base_url, + "responses_path": responses_path, + "project_id": project_id, + "project_uuid": os.getenv("PROJECT_UUID"), + "project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"), + "default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5-mini"), + "timeout": int(os.getenv("AI_TIMEOUT", "30")), + "verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"}, + } + return _CONFIG_CACHE + + +def _build_url(path: str, base_url: str) -> str: + trimmed = path.strip() + if trimmed.startswith("http://") or trimmed.startswith("https://"): + return trimmed + if trimmed.startswith("/"): + return f"{base_url}{trimmed}" + return f"{base_url}/{trimmed}" + + +def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str: + base_path = (cfg.get("responses_path") or "").rstrip("/") + if not base_path: + return f"/ai-request/{ai_request_id}/status" + if not base_path.endswith("/ai-request"): + base_path = f"{base_path}/ai-request" + return f"{base_path}/{ai_request_id}/status" + + +def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str], + timeout: int, verify_tls: bool) -> Dict[str, Any]: + """ + Shared HTTP helper for GET/POST requests. + """ + req = urlrequest.Request(url, data=body, method=method.upper()) + for name, value in headers.items(): + req.add_header(name, value) + + context = None + if not verify_tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + try: + with urlrequest.urlopen(req, timeout=timeout, context=context) as resp: + status = resp.getcode() + response_body = resp.read().decode("utf-8", errors="replace") + except urlerror.HTTPError as exc: + status = exc.getcode() + response_body = exc.read().decode("utf-8", errors="replace") + except Exception as exc: # pylint: disable=broad-except + return { + "success": False, + "error": "request_failed", + "message": str(exc), + } + + decoded = None + if response_body: + try: + decoded = json.loads(response_body) + except json.JSONDecodeError: + decoded = None + + if 200 <= status < 300: + return { + "success": True, + "status": status, + "data": decoded if decoded is not None else response_body, + } + + error_message = "AI proxy request failed" + if isinstance(decoded, dict): + error_message = decoded.get("error") or decoded.get("message") or error_message + elif response_body: + error_message = response_body + + return { + "success": False, + "status": status, + "error": error_message, + "response": decoded if decoded is not None else response_body, + } + + +def _ensure_env_loaded() -> None: + """Populate os.environ from executor/.env if variables are missing.""" + if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"): + return + + env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env")) + if not os.path.exists(env_path): + return + + try: + with open(env_path, "r", encoding="utf-8") as handle: + for line in handle: + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + key = key.strip() + value = value.strip().strip('\'"') + if key and not os.getenv(key): + os.environ[key] = value + except OSError: + pass diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 3d6501c67fa5c80fdda8a8ee57862699aabd319d..397a328b8ab2202c0de40ecb1dc70e9cb833134c 100644 GIT binary patch delta 20 acmbQwIG>ScIWI340}!0hRLPvkGZ_FcRs_ZX delta 20 acmbQwIG>ScIWI340}vQ{e#)50GZ_Fd1qB`e diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index dadfaa7db630a06a9bc0da9edbc375273aefa0ed..0255491e45cd76b9d0cd3a68d99656a153f1743e 100644 GIT binary patch delta 21 bcmeyQ@JWGZIWI340}!0hRLR`PlO_NFMdSso delta 21 bcmeyQ@JWGZIWI340}wn|@;zfCPnrM#PVNU1 diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 139db1071801d00bd7e1a167305bffd074d952f6..5545dff09a29bef684718076a2b1668e05319cc1 100644 GIT binary patch delta 341 zcmey)(ZQv@oR^o20SNYLsbn@YGcY^`abSQ0%J{ryqPnU=3S$aO4qGmJ6bB;%6GJLX z3TrA;7AH(?7T3fCNn@52wp5NRZkS*S`x=gAKvP!(F$91$vH?lf6wYOgKpDn~31VF7 z%uzfkT)_;Q+#6R+W^!UFNG!>?#h#g$oKu>Ts>yhZwJbBWyts-jF(o%MPyZIfEzXq0 z;*6yH#G(|SEK6QuZfX^0a(+>&UTINIv7aXIs+B^Ym{qRBv|p%WTe7oLK@?1`?Uf#p2H;05lI|M{&vI vWEMNo4{QvqoDD7=5)(|PSWU6N$fA6OMS1dl7AbyKW`3pyZV)Wu1F8oAjS)0< diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 79ce690f602e05f6bfa8a8e253edcb377296b788..33a3e608663bd35fd4bc84e01789a3324240266e 100644 GIT binary patch delta 21 bcmZ3^x}23~IWI340}!0hRLR`PGn)wjHiiWV delta 21 bcmZ3^x}23~IWI340}vQ{e#+R$Gn)wjH;n~g diff --git a/config/urls.py b/config/urls.py index 5093d47..3f3c4b6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,9 +15,12 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import include, path +from django.urls import path, include +from core import views as core_views urlpatterns = [ - path("admin/", admin.site.urls), - path("", include("core.urls")), + path('admin/', admin.site.urls), + path('', core_views.dashboard, name='dashboard'), + path('', include('core.urls')), ] + diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 3b7774ea363dc0bd4dc92284fe89acea2051acd3..8e96518257e865e3f8b93b523a595b46f0169c00 100644 GIT binary patch delta 20 acmbQsIG2%UIWI340}!0hRLPvkGZ6qU2n4_Y delta 20 acmbQsIG2%UIWI340}vQ{e#)50GZ6qUw*?pg diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e41572949c7873f8ad5478be4973869ac513d51..e4a8869223572a5b654dcf00b1f8ca8102c1e984 100644 GIT binary patch delta 280 zcmeBVd&$bXoR^o20SE-NR5C>;@-Agz3C+u#xWgix6G)dhrsQVkX|mp8F9mYqGgEGH zf*B>11*x~#ic1npN{estUqZwE8 zX*PJ@5K(CG1;HYb$s$ai%EBOZA|OH(Nc`fk$<0qG%}KQ@k^*uWfw;JQas!i0vcv@j z6!d_Rzd^J`ZUW0iM$s#bq92%;85KV;U=kBpuLvl71W9}W5g(Zun8YyEgIxdsr}9kI delta 332 zcmaFK+R4VdoR^o20SIoZf6K6*$h(w{EhIBJJ9XkVi+XM_tHd!SH#1L@^%i?^X;M~d za>*^W;*!LY(&Aej1x1y0|Rz)g6b6^rH^3AFJST`GXs+ZR&8Ln008*X BU@-sy diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6435d92a257f85ac41d6fd22a9e528c3da4a1ec5..649e5089f5d2fc21605af2e6c09869666a9e6b19 100644 GIT binary patch delta 20 acmeBS>0#kn&dbZi00bvARWcXyFaZE8j|3$E delta 20 acmeBS>0#kn&dbZi00hRKpE4HmFaZE9J_LOL diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index f6e5c4e3024d0f534a8b93046d15a44a0c8eeba6..6028176d2a2f7e02a6ddb2948b61ca5117b3db57 100644 GIT binary patch literal 1073 zcma)4J8u&~5Z=9)A8|w?A`_5Bfso*k95-|XBqAQ7$Tbl{y5@Ab-2^-L2)la;caE_A&Oi~kgH{>uu7$MG)1pgF0M{=MQ_L= zFg;P+=h&=q*s!gXGd8Nb2EneWA!@Y>I7yA+QJX!X;=KYy=n{jAkLKkn7)2QLoe?%maH%{i($y_(bi zm%n=BBmES9yLE8CYd`E(SB|PHz3R%KU|cQ_kb-srKDMaxv1ua8~U=E0~IW8cx7>dcj6c44jT_NsF}ncr(d+ZDsxE`7lbWFbsId zc>(8(8WorGNj_lL6&1Wu?kO1P7~?)#?4JLpJp-eNRpHanPCvXo0srX*n_2O{0jFU9 AfB*mh delta 483 zcmdnUah{EDIWI340}!OEe9Jh+Fp;lp-m{sDIUzD2+QUe1_P{wB| zAY(d1Dnk@w3PTiADsvP|IztNMBE~4z6sBMXP3B+x2ov;tQ%e#xS#GfxmnLPUCYRje zFG?*eO)V}-Es9UgP0Y->#Zgd{nO~GyQhAFzCAB!Yh%>XGBr`wHPm^u(14fI<)=X~o zazG0k;P8fkSO@D3L9Yv7)WHUVMIt~YMPfk0Pm`sH55(aI5dtvbB4H2MrW2;iVV;>M|sS_vvSfLWjf%{xhSDbhKT zX%b}6kikO+Z{9n&F5)3SqoWLH5I7VCI(cK@OrG+Nw3Jve&`0sz`~JE|{i&*X1fbrW zx1|h1|8Zsr(m8Q@8;E^G5Wx=WU>##5pa+NuuMo*lye$kJ4j{rCcvH|bM69EVbi&yR zj^vIf{)uh=JWl6zP~CT_BhUEkVz;Ai*D47X?J6ZF%TOL?Ru@2axv7D7Bs) zv&quTkuqkZBsRHGoZM0V07DdbQ_%DOYccUz9+6BO6~jA?SL5{(pL zG*gkd;o3n(h;ny3TcKmR7G=E6ig1>H zGqBo}Fy0t9`k6LE>ez0J@w0BV4FzGFT!9O|}$b|id6YE_KVXLOqQHl`aL!$YSU#Ua7Y!y$uV40*>!&>Zg*6-&e0l{mpRQpG!xjrr zXyz=JL|vzW$x2{VtB8?e7_Ql&h7sir0}2TpP8SU0NoYFf8pUITGPsPt5c?AR7%wOi zlQ`Fa{0d&5%etdyaAkIHX;M{rFT4j=b4JW(K(F~wyYNNK=FdCbW?i=}d=Q}jYuQ;9=B)X{Jc$OL1o@+Q}@G0lH zk?aR%5c+JIC+Fp`8yt-qV*Eo+5YOoU1L$Kv|0>@Ml=)c7T$6j^)mV|HyrDJWWuPt>S42tDFxnC?F-s|07?G@LKi);PjT6c9&n%}$CFU=p; zd!@zW(qg}~*!}5pp*p+!xUbG0e)0C#zklheo5$*AU)}7k#3GhvVl+ZBmgYD+j^1(E z`%4sQ>%42e2syHCx-D;kY^4*I*7;vuI+TlpI#ceXi}F3Y3C?`+{B=Na24g%xGrfzy XSe(XC)8}aS*6Ta(xcvPk%A~jtZ^Bz^ literal 1409 zcmZuxy>Ht_6hDg8w@169EsumE? zlf`$Pzf%bPA&e=dC(6Y#C~pu!M6yv&(j^I{&?+LSSBS_|>Ox*Z$1*~vuoBW`qHLo^ z`dpX|iD!Bap|)#I;8pCw%enF6V#5KVV8(?lYlDaR0h3M(N! zdnF=GWs*6|UO7;1CQ2c>nW%gm6}Ls~ehlk0DFa+D#%O8kxPq!NTArdLe^$71(#lLk z@k+!TDZN5^m8j6XGS6t1`ILGMxsl?EGoXF0Yyk(WX!Y@cIc-K=_eF8Nf9v7I04!tL zj!Oy8ti&?TmThOB%ilTs0`E~``8}T5h^5%OW3@Y6Sp$Cl51bzcR_GZA*rxrq*Ezoi zE+!_>kXa1`&zc>_GO5e6gzg!>?J*VH8l;sB-*a%^*~bPUVCSZ`c{wkL6uNjS7jwYg zpqHo%G9kFPv*Wc|@(*sp;slDfloLldkt&Y>9Eq~YVdwz=^P!sC0*1p4@x&y*<-+HvJJ)U})xF652bPRUiqBdb4 z3OnO^hUdFHH(-{-Eblol-0K;Z9f$A=qtATmdX!-*ygaYb124gfgt{iP2A<{gx%!Ra zQO)Yn1S^<~f`eeg;{|^}COVsklD{Ih0D|2RO#I?vCJyKR4lKC*j8jTIGe%PuY>rFH zb>Wz>P`NfPp~}s}y{P=r$;V;2IV?A$ax+*RsSAfYk-BhFd;8?Kolw<=surnQaDN2R zKShZ-yTf`TsyBkIQSJ88CsFP8u(lZ07K3j` z)jLP^sCwt^QdnIcR+ppda`0g4Jn_Qn(y+P|RhNPXV?|2OkI^hk?k%po*-&_%bd7%7 zX_DP!GU*NRhm>^WQ_+^oF1^ovnGF3a_89=;-MJgU#n3W_bVr diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 4e4f113d309e34129cca5a1bb3778fb33b4fe2e4..23f4ce8ac7d5a19406d5e19316efe93bf6baecb4 100644 GIT binary patch literal 665 zcmZvZzi$&U6vv-)zVt33R8WwRD8D3@s8CBtK?GIpQYjltBZe?k*Vt;Vy*r=$gF+TE zcC17t)=p3a6BB>NMY6F{dlQ0z_JFRFAeNOt+E7LgRTPz4BE9bwZKw)Voell>nPEFzPHVj_{>;oL}Y z@r?9%%!uC6iPD69U|p@aAO~FR%QWaR(&fS&o9kE8j~Cur%&Cspy0~S6Z7t+yGmRaD z&^lrssskY1S+fsG9nbHtHSxnXFrNGh?_n>3fu)}lsJGufWja7qhSXS{MFez z9m%ugHZ1~lxtyKdo!y(AnVp&aZ7^7apyWG`Bp3Y%{gZT@Lhm4t=K#5l1SC)?l%}Ss zG(AnHnQ4YX0xd8pHtm`ASU8*VrnzYja3pwAzG>GuF5`IriTm%)H8K=ju)hfw!HkDA5vecDH3bTaLDPBMr_&;t3a2u&rb2&I8Ia89l3l#SV zl~sToqtkWdz8 zVev{tjnvaarlgyk+{! zpT)JXo@h;J2C#yPd|m#i)_mfVQ`*o4t@onNUo!Yh8hhz6Oe5FHIT)}`Zm_mSuTv@^ z+9``=No+d0cc!dXC6e1IcY*d^nI8XL1?zE2HJw1+g}v^T_j?gtr~OFf9a@BP^k&t* zmGQfP-Ya)E3Gcg#o5sx(oUP0sLW@3UaSk->=7~bm#a5)&I}`}14HbNjK7#P8#HPP9 zX&rEJR+dGz+}vlO(3?nNm(f+`O|(qGZGzvfGw`jb2Py0_ZBvmo6#Q~%G2om=U@lvl z$WwSqU|}Zc%q$`Nc8*e3)bBYXAhVEz&%=HGHRa~%I`dqed)>Sdor9Lc&TiGM+FI%5 zn8mUV+~0EdEdTP~NT6z8(BaX_7X{9}^CS{{&q(F79ECpxi+zqQslFX zQRU3F7Rw>;p0>gOCYLxyyOzV|Qc{}7#8<}}OC6h@?s>xz8A&?W`WX5DKCsQf2=3*k16^AjFvB{mAv=NAv zX?aOBy=f7SC}47l%o-&-1EFbFR20*5-rCf&jFYNpGO(e^KvcvK1(4}glJim`Wpb0F zBU8f@uiNp8#l&ggPC`M=f%NRH6td(n6ihq^Zgc?YDSrgHub@Ir?aEM*^WR+l#f`Ub zJc!10uEXFuO33f&D+cQx)c;^p(Sw~vu(O1G{=Tg%C7KKN6{82%zyDF&hi$)V-)R4+ z>%%U+sl#aM(4(D3v@_2<4A-x5>&*Ha?}yexdH8J?8xH=4(<1F9#02{)pb%}!GsVch zwIAL-^}(qJFTAKn28_r+2~l-@#fIqG=!1hjdPA?#(5p4{Zf}=3SiKD`53E@HSl|)i z>QFJ#thJobBRxi>M+@{6f|304T22eLLfv8jlvsbT zxoB(T&c%(N>)~TY_?Q+x_HE^EDJjov7b8t2#0Hxy;NiE_+|CYprsRk9aGq~!P(w>K z+Yjxu=p}99g7&k7HjyYnSf@_Y(3~9EfsYUhoS~ms5XL=cXyvD1$bX=QV$7$5zE?T= zb1%U^=LW+=O`iLWG<5DCpa|Z?5In{X9p~lp%4Yx|22fN1{aAZ`Euj6HyB1Kh=B@=) zr#ZEN{43V4fPyR5?<L(OLlnV47q0WFet%BneO)UUX74-SxEd~j|p3pks FzW}Pqc&Y#Z literal 2006 zcmZ`(&2Q936d!wcz21)`&W4tJ#pJA?V%j1zOmzNm$ozG=l3ywe)Hb% z%?~3Zc?9FF3q=E*-j z479_0?hBiOCEO9YnTVF?30CU9NR#Y?c5QI}H@-|+=}nR529D9No${FX$C{LI!+SPU zn*tq6Z168R;jN4%Z9`N$QDj{EKl`$8?z7~_^8GZONIW6PmX+H~57!dvm~Wk5i3+CD z8de>{vutlw4NQWur#5X`Qyt%cda7VivA1p$-*d4?RSQ$Yc7kfFXw>UXyVcsLn!d~A zi}m`KTw=LPjzJxN)iztXW!zM6(hUc*i5n}|uPiRy(m!3ixv;WyYhaWLKH(DYNaRP^~=u`nh_pqn_KE-I{$i*}l4) zFFl<5w)(KzUg%5c(6R8~%x?K)w>;Y`&+e9Idgb@K67l%h2GSK{&;Ro>LVcS%fBB$#nRS^&g9(F)o$TzuW+`L zKD!4&B9oJ3Bu%z=8Z?g(M*ND?L<|=k&S;V)Xju_q*%G&*sdomAt>vr~Ksn8n_|o|M zZfPBi(nO=1MF&vBofDNaO4Twisve%7U0!$``60Z5tPnIz69)lD(vOH71syrU$$JUmQZNUkJ(yv!)Lsn7 zC)-!T1E<>8!qVZbx$oAyrQ^NQ@%B|dV@KRaWr9p`SMLI0QrruKXD>#g5^JhK%_kIq z6GQ-Hg?%+d6~uuS6Agt{j`4hGUll_OS`HK2tYXWiB+LJn;b#xxJV;Saf+#^zM&kpX z;(-hf{aXGw&g*&d0T_7If)GeQB?v-@a_wk_XsjL0m*{k7c!lU_XLyC^SZ8>J=tyUH tg~Lw3=Swuz8D1eOcZOGAQiKotXh4pqg%f@BZ?e_;m1}?d4^wpVzX0UP4AcMs diff --git a/core/admin.py b/core/admin.py index 639ff3a..4f6163a 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin -from .models import Ticket +from .models import Unit -@admin.register(Ticket) -class TicketAdmin(admin.ModelAdmin): - list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at') - list_filter = ('status', 'priority') - search_fields = ('subject', 'requester_email', 'description') +@admin.register(Unit) +class UnitAdmin(admin.ModelAdmin): + list_display = ('unit_id', 'unit_type', 'status', 'location_lat', 'location_lon') + list_filter = ('unit_type', 'status') + search_fields = ('unit_id',) diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..5de8c8d --- /dev/null +++ b/core/context_processors.py @@ -0,0 +1,10 @@ +import os + +def project_context(request): + """ + Adds project-specific environment variables to the template context globally. + """ + return { + "project_description": os.getenv("PROJECT_DESCRIPTION", ""), + "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + } diff --git a/core/forms.py b/core/forms.py index 7a6b83b..b65f745 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,7 +1,13 @@ from django import forms -from .models import Ticket +from .models import Unit -class TicketForm(forms.ModelForm): +class UnitForm(forms.ModelForm): class Meta: - model = Ticket - fields = ['subject', 'requester_email', 'priority', 'description'] + model = Unit + fields = ('location_lat', 'location_lon', 'status') + widgets = { + 'location_lat': forms.NumberInput(attrs={'class': 'form-control'}), + 'location_lon': forms.NumberInput(attrs={'class': 'form-control'}), + 'status': forms.Select(attrs={'class': 'form-select'}), + } + diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/run_simulation.py b/core/management/commands/run_simulation.py new file mode 100644 index 0000000..aa7cbba --- /dev/null +++ b/core/management/commands/run_simulation.py @@ -0,0 +1,35 @@ + +import random +import time +from django.core.management.base import BaseCommand +from core.models import Unit + +class Command(BaseCommand): + help = 'Starts the data simulation for emergency units.' + + def handle(self, *args, **options): + self.stdout.write(self.style.SUCCESS('Starting simulation...')) + statuses = ['Available', 'Enroute', 'On Scene', 'Returning'] + + while True: + try: + units = Unit.objects.all() + for unit in units: + # Simulate location change + unit.location_lat += random.uniform(-0.01, 0.01) + unit.location_lon += random.uniform(-0.01, 0.01) + + # Simulate status change + if random.random() < 0.1: # 10% chance to change status + unit.status = random.choice(statuses) + + unit.save() + + self.stdout.write(self.style.SUCCESS(f'Updated {len(units)} units.')) + time.sleep(5) + except KeyboardInterrupt: + self.stdout.write(self.style.WARNING('Simulation stopped by user.')) + break + except Exception as e: + self.stderr.write(self.style.ERROR(f'An error occurred: {e}')) + time.sleep(10) # Wait a bit before retrying diff --git a/core/migrations/0002_unit.py b/core/migrations/0002_unit.py new file mode 100644 index 0000000..732922a --- /dev/null +++ b/core/migrations/0002_unit.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.1 on 2024-01-26 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Unit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('unit_id', models.CharField(max_length=100, unique=True)), + ('unit_type', models.CharField(choices=[('Ambulance', 'Ambulance'), ('Police Car', 'Police Car')], max_length=50)), + ('location_lat', models.FloatField(default=0.0)), + ('location_lon', models.FloatField(default=0.0)), + ('status', models.CharField(choices=[('Available', 'Available'), ('Dispatched', 'Dispatched'), ('Refueling', 'Refueling'), ('Out of Service', 'Out of Service')], default='Available', max_length=50)), + ], + options={ + 'ordering': ['unit_id'], + }, + ), + migrations.DeleteModel( + name='Ticket', + ), + ] diff --git a/core/migrations/0003_seed_units.py b/core/migrations/0003_seed_units.py new file mode 100644 index 0000000..2c576f6 --- /dev/null +++ b/core/migrations/0003_seed_units.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0.1 on 2024-01-26 12:01 + +from django.db import migrations +import random + +def create_initial_units(apps, schema_editor): + Unit = apps.get_model('core', 'Unit') + + # 10 Ambulances + for i in range(1, 11): + Unit.objects.create( + unit_id=f'AMB-{i:03}', + unit_type='Ambulance', + location_lat=round(random.uniform(34.0, 35.0), 6), + location_lon=round(random.uniform(-118.0, -119.0), 6), + status='Available' + ) + + # 5 Police Cars + for i in range(1, 6): + Unit.objects.create( + unit_id=f'POL-{i:03}', + unit_type='Police Car', + location_lat=round(random.uniform(34.0, 35.0), 6), + location_lon=round(random.uniform(-118.0, -119.0), 6), + status='Available' + ) + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_unit'), + ] + + operations = [ + migrations.RunPython(create_initial_units), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc index 64d8a5533a01aeb9823459fa0e2a5787563687ab..81369bc15fdba48b8b46e0e6b914f0cb308696a4 100644 GIT binary patch delta 21 bcmeyv^M{9LIWI340}!0hRLR`PQ@{oQM1}>K delta 21 bcmeyv^M{9LIWI340}$N&_cLQ7PXQYMPud4l diff --git a/core/migrations/__pycache__/0002_unit.cpython-311.pyc b/core/migrations/__pycache__/0002_unit.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03289dda7489d85dae7ff379b88057e5f3c6d7bc GIT binary patch literal 1642 zcma)6yHDgs7#~0IuFXPN0xXX`U?YkwcVap6EKmpsBI1&}oDS}yY-1VEtTA})Wo(lp zDMFDV<#nNqQV>P#A3&KhrIEHkQ$&PX<4^`IcyF}2|Dlz`J&uffn{U0L*(Wsh zjxEkN`wt=GBY1DTz5PFQ25v`m5FY$R!b1?VaJ!H-+0pPY&N`ASHAimCz=yge+Q?($ z(dO9gTpYRDjf>ERJ4}DnsXsJBwF636ZgzI|zUlgI&S7q71{d3Cw0vYZu1 zfoW4>MFg|%26gKe-7~kz9@9fY!N~m=4}2v}!xZC4q4GXQ=_`{B-V zUSNxHGCeDDfFn*>Vi`KH4?1l|1vNs8) z<87Ayl+UHlP?P5UJQ5LM#*CmruDA0y zbOAol)qG_$sFO;(5&KbGAuosx9%<#9fNn#nY*OJfRC%@46_}rP+y5dpkUsIF(aMgW<_mx32b03|%iUIC`dIyADyf=@Pm@A;QV3gx zFzJg<^P@@e-oZ+%c<)&GeK{$vo)lMG#nq(m;c32@jL#g@TH`ax$4`^>M)J{4iqQAc zq70vj2lBbVOM$@2FUhC*iKI05^H8facl=&bS~w{!v`Py}->3f}7)i$N9(>puy9+Kp z*x{~U$M5771z2m!GyW+)S0E@L1O)_6{wpQz$qe+S{U|?n@b$sxP=&$Xbg)M^&Jn;R zhqU;rBqN}}WdN5P(uw|lZT}&E=>1NKAH%Ndc+2vgUZo({A6S{nluCnv( w$@+x84_E$QD|`-+Dv~5YW2AC|-hJ~g)rKW4MQ_4Oqt!Qc&IOnMA{BA}2KZ2`*8l(j literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0003_seed_units.cpython-311.pyc b/core/migrations/__pycache__/0003_seed_units.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98c1c49803722502d286a31e7fca0de0a25e1aaa GIT binary patch literal 1832 zcmc%}O=}xRbY^$8+FeOW>bNZ;wq=(Psz5B;DTP37qSS4n#I9-SMTuEfJ7a0p{ZRHJ zt&LN3$RR0)`sP3z@X2j)4>{)OpAZE>#XwIznO-(Hx#ZNDUG4fKJ@`=Q8|~BkocG?m znfFVfpa2S=T)t@h%>(ckV-m;=h=Wflu>}x7&;%B&LC9#%hfpxG=nKuDJ z#O+Xv-PHFU=cmyCKs*wT>*v98;~u}xwNj7mBld?8P-dM+;!nb34rP7$Q10gr>6Vz# zNJ8@QI7m6ppJI<3${+VRxd$rzerAXEV>jsufMBC}qTUm6PhIcXwBB>4$n}4$7Xf;M z-dZGr?zor;U)zSi4{0OzS!dRS>cWjHgqylbHMquNOyy2sBO+2Ya;zx&isM>k zfe2c=?U90~H?gIu7#Y6fx~u{M4UfIFaSE?*RGN;3D?vT5{h)%s!+JpND&IQpH(p!Q zafO{%P4 zw0)_YEBA8c&ipG8oH@Tay;*;Fd*gNpvKb{p4`5%)Z{+`wEZu+)qiK= z&*IqQyFY*bD|#~DEnVxCu05UZ7H50K*+jAOWTji0?UiPq-su*ry<)Y0ek{xZdE^BU zW#uJ9VTxW2#qqU=w>EB_CjNU#nExhz$R{Nq&~pMhLkT3~<={gq*#a2UAOdt~!UkkR z6AP{MIb(5$p71Wa%@I(GY&0-=A}=J9vdp>6fylIfGf{^Y%My`P)z&Pmszg!QMGH(u z=T-GipqT?r9^p2&5w>*$doIh1$X^Eb(yHHd?5N8`aoTt=kbeX27&UQ67#dFyy9?e2 z6m`-Sia;I8>=>lV+%9NbG#hR=gqGi#{(Iwdg}|CQ&^~ z#eJ8Gqa@;B?=v<{;I;HoT-2O + + + + + {% block title %}{{ project_name|default:"Django Project" }}{% endblock %} + + + + + + + + + +
+ {% block content %} + {% endblock %} +
+ +
+
+

© {% now "Y" %} {{ project_name|default:"Project Lifeline" }}. All rights reserved.

+
+
+ + + + diff --git a/core/templates/core/article_detail.html b/core/templates/core/article_detail.html new file mode 100644 index 0000000..8820990 --- /dev/null +++ b/core/templates/core/article_detail.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} + +{% block title %}{{ article.title }}{% endblock %} + +{% block content %} +
+

{{ article.title }}

+

Published on {{ article.created_at|date:"F d, Y" }}

+
+
+ {{ article.content|safe }} +
+
+{% endblock %} diff --git a/core/templates/core/dashboard.html b/core/templates/core/dashboard.html new file mode 100644 index 0000000..ef32336 --- /dev/null +++ b/core/templates/core/dashboard.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} + +{% block title %}Dashboard | {{ project_name }}{% endblock %} + +{% block content %} + + +
+
+
+
+
Live Unit Map
+
+
+
+

Map visualization will be implemented here.

+
+
+
+
+
+
+
+
Key Performance Indicators
+
+
+
+
+
Avg. Response Time
+

-- min

+
+
+
Active Hotspots
+

--

+
+
+
Available Units
+

-- / 15

+
+
+
Dispatches (24h)
+

--

+
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/simulation_control.html b/core/templates/core/simulation_control.html new file mode 100644 index 0000000..6210d1e --- /dev/null +++ b/core/templates/core/simulation_control.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+
+

Data Simulation Control

+
+
+

Use the controls below to start or stop the real-time data simulation. When running, unit locations and statuses will be updated automatically.

+
+ {% csrf_token %} + {% if simulation_running %} + + Simulation is running. + {% else %} + + Simulation is stopped. + {% endif %} +
+
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/unit_form.html b/core/templates/core/unit_form.html new file mode 100644 index 0000000..ca33115 --- /dev/null +++ b/core/templates/core/unit_form.html @@ -0,0 +1,36 @@ +{% extends 'base.html' %} + +{% block title %}Edit Unit {{ unit.unit_id }} - {{ project_name }}{% endblock %} + +{% block content %} + + +
+
+
+
+
+ {% csrf_token %} +
+ + {{ form.location_lat }} +
+
+ + {{ form.location_lon }} +
+
+ + {{ form.status }} +
+ + Cancel +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/unit_list.html b/core/templates/core/unit_list.html new file mode 100644 index 0000000..7298651 --- /dev/null +++ b/core/templates/core/unit_list.html @@ -0,0 +1,47 @@ +{% extends 'base.html' %} + +{% block title %}Unit Management - {{ project_name }}{% endblock %} + +{% block content %} + + +
+
+
+ + + + + + + + + + + + + {% for unit in units %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Unit IDTypeStatusLatitudeLongitude
{{ unit.unit_id }}{{ unit.unit_type }}{{ unit.status }}{{ unit.location_lat }}{{ unit.location_lon }} + Edit +
No units found.
+
+
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..df7ab3a 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,10 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), + path('dashboard/', views.dashboard, name='dashboard'), + path('simulation-control/', views.simulation_control, name='simulation_control'), + path('units/', views.unit_list, name='unit_list'), + path('units//edit/', views.unit_update, name='unit_update'), ] + diff --git a/core/views.py b/core/views.py index c1a6d45..1621d37 100644 --- a/core/views.py +++ b/core/views.py @@ -1,37 +1,65 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from .models import Unit +from .forms import UnitForm +import subprocess import os -import platform +import signal -from django import get_version as django_version -from django.shortcuts import render -from django.urls import reverse_lazy -from django.utils import timezone -from django.views.generic.edit import CreateView +SIMULATION_PID_FILE = 'simulation.pid' -from .forms import TicketForm -from .models import Ticket +def dashboard(request): + return render(request, 'core/dashboard.html', {'project_name': 'Project Lifeline'}) + +def simulation_control(request): + pid = None + if os.path.exists(SIMULATION_PID_FILE): + with open(SIMULATION_PID_FILE, 'r') as f: + try: + pid = int(f.read().strip()) + # Check if the process is still running + os.kill(pid, 0) + except (ValueError, OSError): + pid = None + if os.path.exists(SIMULATION_PID_FILE): + os.remove(SIMULATION_PID_FILE) -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + if request.method == 'POST': + if 'start' in request.POST and not pid: + # Start the simulation + process = subprocess.Popen(['python3', 'manage.py', 'run_simulation']) + with open(SIMULATION_PID_FILE, 'w') as f: + f.write(str(process.pid)) + pid = process.pid + return redirect('simulation_control') + elif 'stop' in request.POST and pid: + # Stop the simulation + try: + os.kill(pid, signal.SIGTERM) + if os.path.exists(SIMULATION_PID_FILE): + os.remove(SIMULATION_PID_FILE) + pid = None + except OSError: + # Process might have already died + if os.path.exists(SIMULATION_PID_FILE): + os.remove(SIMULATION_PID_FILE) + pid = None + return redirect('simulation_control') - context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - } - return render(request, "core/index.html", context) + return render(request, 'core/simulation_control.html', {'simulation_running': pid is not None, 'project_name': 'Project Lifeline'}) -class TicketCreateView(CreateView): - model = Ticket - form_class = TicketForm - template_name = "core/ticket_create.html" - success_url = reverse_lazy("home") +def unit_list(request): + units = Unit.objects.all() + return render(request, 'core/unit_list.html', {'units': units, 'project_name': 'Project Lifeline'}) + +def unit_update(request, pk): + unit = get_object_or_404(Unit, pk=pk) + if request.method == 'POST': + form = UnitForm(request.POST, instance=unit) + if form.is_valid(): + form.save() + return redirect('unit_list') + else: + form = UnitForm(instance=unit) + return render(request, 'core/unit_form.html', {'form': form, 'unit': unit, 'project_name': 'Project Lifeline'}) diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..9860b55 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,100 @@ +:root { + --primary-color: #0A2342; + --secondary-color: #2CA58D; + --background-color: #F0F4F8; + --highlight-color: #FFD700; + --text-color: #333; + --heading-font: 'Poppins', sans-serif; + --body-font: 'Lato', sans-serif; +} + +body { + font-family: var(--body-font); + background-color: var(--background-color); + color: var(--text-color); +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--heading-font); + color: var(--primary-color); +} + +.navbar-brand { + font-family: var(--heading-font); + font-weight: 700; + color: #fff !important; +} + +.nav-link { + color: rgba(255,255,255,0.8); +} + +.nav-link:hover { + color: #fff; +} + +.btn-primary { + background-color: var(--secondary-color); + border-color: var(--secondary-color); +} + +.btn-primary:hover { + background-color: #248b75; + border-color: #248b75; +} + +.table { + background-color: #fff; +} + +.table thead { + background-color: var(--primary-color); + color: #fff; +} + +.page-header { + padding: 2rem 0; + margin-bottom: 2rem; +} + +.container { + max-width: 1200px; +} + +.card { + border: none; + box-shadow: 0 0 20px rgba(0,0,0,0.05); +} + +.form-label { + font-weight: bold; +} + +/* Dashboard specific styles */ +.kpi-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.kpi-card { + background-color: #fff; + padding: 1.5rem; + border-radius: 8px; + text-align: center; + box-shadow: 0 4px 12px rgba(0,0,0,0.08); +} + +.kpi-card h6 { + font-size: 0.9rem; + color: var(--secondary-color); + margin-bottom: 0.5rem; + text-transform: uppercase; +} + +.kpi-value { + font-size: 2rem; + font-weight: 700; + color: var(--primary-color); + margin-bottom: 0; +} diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..6cb6aea 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,63 @@ - :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --primary-color: #0A2342; + --secondary-color: #2CA58D; + --background-color: #F0F4F8; + --highlight-color: #FFD700; + --text-color: #333; + --heading-font: 'Poppins', sans-serif; + --body-font: 'Lato', sans-serif; } + body { - margin: 0; - font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); + font-family: var(--body-font); + background-color: var(--background-color); color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; } + +h1, h2, h3, h4, h5, h6 { + font-family: var(--heading-font); + color: var(--primary-color); +} + +.navbar-brand { + font-family: var(--heading-font); + font-weight: 700; + color: var(--primary-color) !important; +} + +.btn-primary { + background-color: var(--secondary-color); + border-color: var(--secondary-color); +} + +.btn-primary:hover { + background-color: #248b75; + border-color: #248b75; +} + +.table { + background-color: #fff; +} + +.table thead { + background-color: var(--primary-color); + color: #fff; +} + +.page-header { + padding: 2rem 0; + margin-bottom: 2rem; +} + +.container { + max-width: 1200px; +} + +.card { + border: none; + box-shadow: 0 0 20px rgba(0,0,0,0.05); +} + +.form-label { + font-weight: bold; +} \ No newline at end of file