Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion backend/admin_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import time
from fastapi import APIRouter, Depends
from server import verify_admin
from users import get_all_users, delete_user_data, reset_user_password, set_require_approval, get_require_approval, approve_user_db, get_ai_config, set_ai_config
from users import get_all_users, delete_user_data, reset_user_password, set_require_approval, get_require_approval, approve_user_db, get_ai_config, set_ai_config, get_default_location, set_default_location
from messages import cleanup_old_messages
from chat import connected_clients # 👈 SOURCE OF TRUTH

Expand Down Expand Up @@ -103,6 +103,20 @@ def admin_cleanup_files(days: int, admin=Depends(verify_admin)):
"message": f"Deleted {deleted_count} files. Freed {freed_mb} MB space."
}



# ================= WEATHER LOCATION SETTINGS =================
@router.get("/weather_location")
def admin_get_weather_location(admin=Depends(verify_admin)):
lat, lon = get_default_location()
return {"success": True, "default_lat": lat, "default_lon": lon}


@router.post("/weather_location")
def admin_set_weather_location(lat: float, lon: float, admin=Depends(verify_admin)):
set_default_location(lat, lon)
return {"success": True, "message": "Weather location updated", "default_lat": lat, "default_lon": lon}

# ================= AI SETTINGS =================
@router.get("/ai_settings")
def admin_get_ai_settings(admin=Depends(verify_admin)):
Expand Down
4 changes: 3 additions & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ pytesseract
requests
PyPDF2
chromadb
psutil
psutil
apscheduler
httpx
27 changes: 25 additions & 2 deletions backend/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
# lan_server/server.py
import os
import secrets
from contextlib import asynccontextmanager
from fastapi import FastAPI
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from fastapi import Header, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
Expand All @@ -12,13 +14,13 @@
from jose import jwt
from datetime import datetime, timedelta, timezone
import chat, files, calls, messages
from tasks import run_proactive_weather_sentinel
import profiles # 👈 1. Import profiles module
from users import init_db, register_user, verify_user, get_admin_key_db, set_admin_key_db
from messages import init_msg_db
from users import get_all_users
from messages import get_recent_messages
from users import delete_user_data # 👈 Import the new function
from fastapi.staticfiles import StaticFiles

# ================= JWT CONFIG =================
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "CHANGE_THIS_TO_SOMETHING_RANDOM_AND_LONG")
Expand Down Expand Up @@ -69,8 +71,29 @@ def verify_admin(x_admin_key: str = Header(None)):
print(f"🚫 Admin Access Denied: Key mismatch. Received length: {len(clean_key)} (Expected: {len(clean_secret)}), Masked input: {masked_key}")
print("💡 TIP: Copy the EXACT random secret from the server logs above.")
raise HTTPException(status_code=403, detail="Admin access denied")
# ================= BACKGROUND TASKS & SCHEDULER =================
@asynccontextmanager
async def lifespan(app: FastAPI):
scheduler = AsyncIOScheduler(timezone="Asia/Kolkata")
scheduler.add_job(
run_proactive_weather_sentinel,
trigger="cron",
hour=7,
minute=0,
id="proactive_weather_sentinel",
replace_existing=True,
)
scheduler.start()
print("🌅 Weather Sentinel scheduler started (Runs at 07:00 AM IST).")
try:
yield
finally:
scheduler.shutdown(wait=False)
print("🌅 Weather Sentinel scheduler stopped.")


# ================= FASTAPI APP =================
app = FastAPI(title="LAN Chat Server (modular)")
app = FastAPI(title="LAN Chat Server (modular)", lifespan=lifespan)

# CORS: allow origins (disabled credentials for security)
app.add_middleware(
Expand Down
107 changes: 107 additions & 0 deletions backend/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import asyncio
import json
import logging
from datetime import datetime, timedelta, timezone

import httpx

from chat import connected_clients, send_to_user
from lumir.ai_engine import ask_ai
from messages import create_delivery_entries, save_message
from users import get_ai_config, get_all_users, get_default_location

LOGGER = logging.getLogger(__name__)

IST = timezone(timedelta(hours=5, minutes=30))
DEFAULT_LAT = 26.2183
DEFAULT_LON = 78.1828



def _get_default_coordinates() -> tuple[float, float]:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid Python 3.9-only tuple annotation in backend task

This repository declares Python 3.8+ support, but tuple[float, float] requires Python 3.9+ (unless postponed evaluation is enabled). On Python 3.8, importing this module raises TypeError: 'type' object is not subscriptable, which prevents server.py from importing tasks.py and can stop the backend from starting at all.

Useful? React with 👍 / 👎.

try:
return get_default_location()
except Exception:
LOGGER.debug("Weather sentinel could not read configured coordinates.", exc_info=True)
return DEFAULT_LAT, DEFAULT_LON


async def _broadcast_family_warning(message_text: str) -> None:
timestamp = int(datetime.now(IST).timestamp() * 1000)
payload = {
"type": "text",
"text": message_text,
"sender": "Lumir",
"receiver": "Family Group",
"timestamp": timestamp,
}

msg_id = save_message(
text=message_text,
sender="Lumir",
receiver="Family Group",
msg_type="text",
)

recipients = [u["username"] for u in get_all_users() if u.get("username") != "Lumir"]
if recipients:
create_delivery_entries(msg_id, recipients)

payload_json = json.dumps(payload)
for username in list(connected_clients.keys()):
await send_to_user(username, payload_json)
Comment on lines +51 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark weather alerts delivered after websocket send

This path creates delivery_status rows for all recipients, but the websocket send loop does not mark recipients as delivered when send_to_user succeeds. In this codebase, pending rows are replayed on reconnect via get_undelivered_messages, so users who already received the live weather alert will get a duplicate after reconnecting.

Useful? React with 👍 / 👎.



async def run_proactive_weather_sentinel() -> None:
try:
lat, lon = _get_default_coordinates()
location = f"{lat:.4f}, {lon:.4f}"

async with httpx.AsyncClient(timeout=15.0) as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": lat,
"longitude": lon,
"daily": "precipitation_sum,temperature_2m_max",
"timezone": "Asia/Kolkata",
"forecast_days": 1,
},
)
response.raise_for_status()
forecast = response.json().get("daily", {})

precipitation_values = forecast.get("precipitation_sum") or []
max_temp_values = forecast.get("temperature_2m_max") or []

precipitation_sum = float(precipitation_values[0]) if precipitation_values else 0.0
temperature_2m_max = float(max_temp_values[0]) if max_temp_values else 0.0

alert_reason = None
if precipitation_sum > 2.0:
alert_reason = f"{precipitation_sum:.1f}mm rain"
elif temperature_2m_max > 40.0:
alert_reason = f"{temperature_2m_max:.1f}°C heat"

if not alert_reason:
return

hidden_prompt = (
"System Context: Aaj {location} mein {alert_reason} hone wali hai. "
"Act like a caring family member and write a warning message for the Family Group. "
"Keep it under 15 words. Do NOT use markdown. Say good morning."
).format(location=location, alert_reason=alert_reason)

ai_config = get_ai_config()
generated_message = await asyncio.to_thread(
ask_ai,
prompt=hidden_prompt,
config=ai_config,
history=[],
sender="Family Group",
)

await _broadcast_family_warning((generated_message or "Good morning. Please stay safe today.").strip())

except Exception:
LOGGER.debug("Weather sentinel failed during scheduled execution.", exc_info=True)
43 changes: 43 additions & 0 deletions backend/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,49 @@ def init_db():


# --- Config Helpers ---


# --- Weather Location Config Helpers ---
def get_default_location():
"""
Fetch weather default location from config table.
Falls back to Maheshpura coordinates when missing/invalid.
"""
lat = 26.2183
lon = 78.1828

conn = sqlite3.connect(DATABASE_NAME)
cursor = conn.cursor()
cursor.execute("SELECT key, value FROM config WHERE key IN ('default_lat', 'default_lon')")
rows = cursor.fetchall()
conn.close()

for key, value in rows:
try:
if key == "default_lat":
lat = float(value)
elif key == "default_lon":
lon = float(value)
except (TypeError, ValueError):
continue

return lat, lon


def set_default_location(lat: float, lon: float):
conn = sqlite3.connect(DATABASE_NAME)
cursor = conn.cursor()
cursor.execute(
"INSERT OR REPLACE INTO config (key, value) VALUES ('default_lat', ?)",
(str(lat),),
)
cursor.execute(
"INSERT OR REPLACE INTO config (key, value) VALUES ('default_lon', ?)",
(str(lon),),
)
conn.commit()
conn.close()

def set_require_approval(enabled: bool):
conn = sqlite3.connect(DATABASE_NAME)
cursor = conn.cursor()
Expand Down
75 changes: 75 additions & 0 deletions intra_admin/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,8 +488,36 @@ def create_settings_page(self):
card_layout.addWidget(self.settings_status)
card_layout.addWidget(save_btn)

# Weather Location Card
weather_card = QFrame()
weather_card.setStyleSheet("background-color: #181825; border: 1px solid #313244; border-radius: 12px;")
weather_layout = QFormLayout(weather_card)
weather_layout.setContentsMargins(30, 30, 30, 30)
weather_layout.setSpacing(15)

self.weather_lat_input = QLineEdit()
self.weather_lat_input.setPlaceholderText("e.g. 26.2183")
self.weather_lon_input = QLineEdit()
self.weather_lon_input.setPlaceholderText("e.g. 78.1828")

self.weather_status = QLabel("")
self.weather_status.setStyleSheet("font-weight: bold; font-size: 14px;")

weather_save_btn = QPushButton("💾 SAVE WEATHER LOCATION")
weather_save_btn.setObjectName("SaveBtn")
weather_save_btn.setMinimumHeight(50)
weather_save_btn.setCursor(Qt.PointingHandCursor)
weather_save_btn.clicked.connect(self.save_weather_location)

weather_layout.addRow("Default Latitude:", self.weather_lat_input)
weather_layout.addRow("Default Longitude:", self.weather_lon_input)
weather_layout.addRow(self.weather_status)
weather_layout.addRow(weather_save_btn)

layout.addWidget(lbl)
layout.addWidget(card)
layout.addSpacing(15)
layout.addWidget(weather_card)
layout.addStretch()
return page

Expand All @@ -502,6 +530,7 @@ def load_settings(self):
self.chk_approval.blockSignals(True)
self.chk_approval.setChecked(bool(data["require_approval"]))
self.chk_approval.blockSignals(False)
self.load_weather_location()
except Exception as e:
print("load_settings error:", e)

Expand All @@ -525,6 +554,52 @@ def save_settings(self):
self.settings_status.setText(f"❌ Error: {str(e)}")
self.settings_status.setStyleSheet("color: #f38ba8;")

def load_weather_location(self):
try:
self.weather_status.setText("")
res = requests.get(f"{self.server_url}/admin/weather_location", headers=self.headers, timeout=3)
data = res.json()
if data.get("success"):
self.weather_lat_input.setText(str(data.get("default_lat", "")))
self.weather_lon_input.setText(str(data.get("default_lon", "")))
except Exception as e:
self.weather_status.setText(f"❌ Load failed: {str(e)}")
self.weather_status.setStyleSheet("color: #f38ba8;")

def save_weather_location(self):
try:
lat = float(self.weather_lat_input.text().strip())
lon = float(self.weather_lon_input.text().strip())
except ValueError:
QMessageBox.warning(self, "Invalid Input", "Latitude/Longitude must be valid numbers.")
return

if not (-90 <= lat <= 90):
QMessageBox.warning(self, "Invalid Latitude", "Latitude must be between -90 and 90.")
return

if not (-180 <= lon <= 180):
QMessageBox.warning(self, "Invalid Longitude", "Longitude must be between -180 and 180.")
return

try:
self.weather_status.setText("Saving weather location...")
self.weather_status.setStyleSheet("color: #89b4fa;")
res = requests.post(
f"{self.server_url}/admin/weather_location",
headers=self.headers,
params={"lat": lat, "lon": lon},
timeout=3,
)
if res.status_code == 200:
self.weather_status.setText("✅ Weather location saved successfully!")
self.weather_status.setStyleSheet("color: #a6e3a1;")
else:
raise Exception(f"Server error: {res.status_code}")
except Exception as e:
self.weather_status.setText(f"❌ Error: {str(e)}")
self.weather_status.setStyleSheet("color: #f38ba8;")


# ================= PAGE: AI SETTINGS =================
def create_ai_page(self):
Expand Down