diff --git a/.gitignore b/.gitignore index 8dfaf4c..643ef06 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ terraform/.terraform/** **/terraform.tfstate **/terraform.tfstate.backup .terraform/* +__pycache__/** diff --git a/Dockerfile b/Dockerfile index a85471c..45062e8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,4 @@ COPY . . EXPOSE 8080 # Command to run the application with gunicorn -CMD exec gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 app:app \ No newline at end of file +CMD exec gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 app:app diff --git a/app.py b/app.py index fe9f7f1..a3d2843 100644 --- a/app.py +++ b/app.py @@ -38,6 +38,9 @@ FV_USER_ID = os.environ.get("FILEVINE_USER_ID") if not all([FV_CLIENT_ID, FV_CLIENT_SECRET, FV_PAT, FV_ORG_ID, FV_USER_ID]): print("[WARN] Missing one or more Filevine env vars — dashboard will fail until set.") +# --- Cache --- +from cache import project_cache + # --- Helpers --- def login_required(view): @@ -62,6 +65,55 @@ def get_user_profile(uid: str): return {"enabled": bool(data.get("enabled", False)), "caseEmail": data.get("caseEmail")} +def fetch_all_projects_for_user(uid: str): + """Fetch all projects for a user and cache them""" + # Get bearer token + bearer = get_filevine_bearer() + + # List projects (all pages) + projects = list_all_projects(bearer) + + # Filter to ProjectEmailAddress == caseEmail + profile = get_user_profile(uid) + case_email = profile.get("caseEmail") + # if case_email: + # filtered = [p for p in projects if str(p.get("ProjectEmailAddress", "")).lower() == str(case_email).lower()] + # else: + # filtered = [] + filtered = projects + + # Fetch details for each + detailed_rows = [] + for p in filtered: + pid = (p.get("projectId") or {}).get("native") + c = fetch_client(bearer, (p.get("clientId") or {}).get("native")) + cs = fetch_contacts(bearer, pid) + print("CS IS", cs) + + if pid is None: + continue + try: + detail = fetch_project_detail(bearer, pid) + except Exception as e: + print(f"[WARN] detail fetch failed for {pid}: {e}") + detail = {} + row = { + "client": c.get("firstName"), + "matter_description": p.get("projectName"), + "contacts": cs, + "ProjectEmailAddress": p.get("projectEmailAddress"), + "Number": p.get("number"), + "IncidentDate": (p.get("incidentDate") or detail.get("incidentDate")), + "ProjectId": pid, + "ProjectName": p.get("projectName") or detail.get("projectName"), + "ProjectUrl": p.get("projectUrl") or detail.get("projectUrl"), + } + detailed_rows.append(row) + + # Cache the results + project_cache.set_projects(detailed_rows) + return detailed_rows + # --- Routes --- @app.route("/") def index(): @@ -99,6 +151,13 @@ def session_login(): session["uid"] = uid # Optional: short session session["expires_at"] = (datetime.utcnow() + timedelta(hours=8)).isoformat() + + # Async update cache after login + from threading import Thread + thread = Thread(target=fetch_all_projects_for_user, args=(uid,)) + thread.daemon = True + thread.start() + return jsonify({"ok": True}) except Exception as e: print("[ERR] session_login:", e) @@ -224,48 +283,21 @@ def dashboard(): if not case_email: return redirect(url_for("welcome")) - # 1) Bearer token - bearer = get_filevine_bearer() - - # 2) List projects (all pages) - projects = list_all_projects(bearer) - - # 3) Filter to ProjectEmailAddress == caseEmail - #filtered = [p for p in projects if str(p.get("ProjectEmailAddress", "")).lower() == str(case_email).lower()] - - filtered = projects - # 4) Fetch details for each - detailed_rows = [] - for p in filtered: - pid = (p.get("projectId") or {}).get("native") - c = fetch_client(bearer, (p.get("clientId") or {}).get("native")) - cs = fetch_contacts(bearer, pid) - print("CS IS", cs) - - if pid is None: - continue - try: - detail = fetch_project_detail(bearer, pid) - except Exception as e: - print(f"[WARN] detail fetch failed for {pid}: {e}") - detail = {} - row = { - "client": c.get("firstName"), - "matter_description": p.get("projectName"), - "contacts": cs, - "ProjectEmailAddress": p.get("projectEmailAddress"), - "Number": p.get("number"), - "IncidentDate": (p.get("incidentDate") or detail.get("incidentDate")), - "ProjectId": pid, - "ProjectName": p.get("projectName") or detail.get("projectName"), - "ProjectUrl": p.get("projectUrl") or detail.get("projectUrl"), - } - detailed_rows.append(row) + # Check cache first + cached_projects = project_cache.get_projects() + if cached_projects is not None: + detailed_rows = cached_projects + print("USING CACHE") + else: + # Fetch and cache projects + detailed_rows = fetch_all_projects_for_user(uid) + print("FETCHING") + print("HI", len(detailed_rows)) # 5) Render table return render_template("dashboard.html", rows=detailed_rows, case_email=case_email) # GAE compatibility -if __name__ == "__main__" - app.run(debug=True, host="0.0.0.0", port=8080) +if __name__ == "__main__": + app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", "5004"))) diff --git a/cache.py b/cache.py new file mode 100644 index 0000000..bbf824e --- /dev/null +++ b/cache.py @@ -0,0 +1,42 @@ +# In-memory cache for Filevine projects +import threading +import time +from datetime import datetime, timedelta + +class ProjectCache: + def __init__(self): + self._cache = {} + self._lock = threading.Lock() + self._last_updated = None + self._is_updating = False + + def get_projects(self): + """Get cached projects if they exist and are not expired (15 minutes)""" + with self._lock: + if not self._cache or not self._last_updated: + return None + + # Check if cache is older than 15 minutes + if datetime.now() - self._last_updated > timedelta(minutes=15): + return None + + return self._cache.copy() + + def set_projects(self, projects): + """Set projects in cache with current timestamp""" + with self._lock: + self._cache = projects.copy() if projects else {} + self._last_updated = datetime.now() + + def is_updating(self): + """Check if cache is currently being updated""" + with self._lock: + return self._is_updating + + def set_updating(self, updating): + """Set the updating status""" + with self._lock: + self._is_updating = updating + +# Global cache instance +project_cache = ProjectCache()