2# +==== BEGIN CatFeeder =================+
5# ...............)..(.')
7# ...............\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
14# CREATION DATE: 24-01-2026
15# LAST Modified: 15:47:54 18-02-2026
17# This is the project in charge of making the connected cat feeder project work.
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: This is the python file in charge of loading and providing the ressources required for loading a barebones front-end.
22# +==== END CatFeeder =================+
26from typing
import TYPE_CHECKING, Dict, Union
27from datetime
import datetime, timedelta
28from dataclasses
import dataclass
31from pathlib
import Path
32from fastapi
import Response, Request
33from display_tty
import Disp, initialise_logger
35from ...utils
import CONST
36from ...core
import RuntimeManager, RI
37from ...http_codes
import HCI, HttpDataTypes
38from ...path_manager
import PathManager, decorators
41 from ...sql
import SQL
42 from ...server_header
import ServerHeaders
43 from ...boilerplates
import BoilerplateIncoming, BoilerplateResponses, BoilerplateNonHTTP
45CACHE_LIFETIME: timedelta = timedelta(seconds=CONST.FRONT_END_ASSETS_REFRESH)
50 """The cache node class
53 timestamp: datetime = datetime.now() + CACHE_LIFETIME
60 DASHBOARD =
"dashboard"
66 Front-end manager for serving static assets and HTML pages.
69 disp: Disp = initialise_logger(__qualname__,
False)
71 def __init__(self, error: int = 84, success: int = 0, debug: bool =
False) ->
None:
75 self.
disp.update_disp_debug(debug)
76 self.
disp.log_debug(
"Initialising...")
84 self.
year: str = str(datetime.now().year)
127 CONST.STYLE_DIRECTORY /
"pico.classless.min.css"
130 CONST.STYLE_DIRECTORY /
"style.css"
133 CONST.STYLE_DIRECTORY /
"Noto_Emoji" /
"noto-emoji.css"
137 CONST.STYLE_DIRECTORY /
"Noto_Emoji" /
"static" /
"NotoEmoji-Regular.ttf"
140 CONST.STYLE_DIRECTORY /
"Noto_Emoji" /
"static" /
"NotoEmoji-Light.ttf"
143 CONST.STYLE_DIRECTORY /
"Noto_Emoji" /
"static" /
"NotoEmoji-Medium.ttf"
146 CONST.STYLE_DIRECTORY /
"Noto_Emoji" /
"static" /
"NotoEmoji-SemiBold.ttf"
149 CONST.STYLE_DIRECTORY /
"Noto_Emoji" /
"static" /
"NotoEmoji-Bold.ttf"
172 CONST.JS_DIRECTORY /
"dashboard.js"
175 CONST.JS_DIRECTORY /
"login.js"
178 CONST.JS_DIRECTORY /
"logout.js"
182 CONST.HTML_DIRECTORY /
"login.html"
185 CONST.HTML_DIRECTORY /
"dashboard.html"
188 CONST.HTML_DIRECTORY /
"logout.html"
209 "BoilerplateIncoming")
211 "BoilerplateResponses")
213 "BoilerplateNonHTTP")
224 self.
disp.log_debug(
"Initialised")
229 """Get the client's host from the request.
232 request: FastAPI Request object.
234 str: Client's host as a string.
242 """Read the content of a file.
245 file_path: Path to the file.
247 str: Content of the file as a string.
249 with open(file_path,
"r", encoding=
"utf-8")
as file:
250 content = file.read()
253 def _load_cache(self, expiration: timedelta = CACHE_LIFETIME) ->
None:
254 """Load the file cache for static assets.
256 self.
disp.log_debug(
"Loading front-end file cache...")
261 f
"Caching front-end file: {path} for endpoint: {endpoint}"
263 expiration_date = datetime.now() + expiration
266 except (OSError, UnicodeDecodeError)
as e:
267 self.
disp.log_error(f
"Failed to load file {path}: {e}")
271 timestamp=expiration_date,
276 f
"Cached front-end file: {path} for endpoint: {endpoint} until {expiration_date}"
278 self.
disp.log_debug(f
"{total_files} Front-end file cache loaded.")
281 """Refresh the file cache for static assets.
283 self.
disp.log_debug(
"Refreshing front-end file cache...")
286 for endpoint, cache_entry
in self.
file_cache.items():
287 if datetime.now() >= cache_entry.timestamp:
289 f
"Refreshing cache for front-end file: {cache_entry.source} at endpoint: {endpoint}"
294 except (OSError, UnicodeDecodeError)
as e:
296 f
"Failed to load file {cache_entry.source}: {e}"
301 timestamp=expiration_date,
302 source=cache_entry.source
306 f
"Refreshed cache for front-end file: {cache_entry.source} at endpoint: {endpoint} until {expiration_date}"
311 f
"Refreshed {cache_refreshed} front-end file caches."
313 self.
disp.log_debug(f
"Skipped {skipped} front-end file caches.")
316 """Generate a 404 response for missing resources.
319 path: Path to the missing resource.
321 Response: FastAPI Response with 404 error.
325 message=f
"The requested resource at path {path} was not found.",
333 """Refresh the file cache for static assets.
335 self.
disp.log_debug(f
"Retrieving front-end file from cache: {path}")
338 self.
disp.log_debug(f
"Front-end file not found in cache: {path}")
342 self.
disp.log_debug(f
"Front-end file cache empty for: {path}")
344 path,
"Resource Not Found"
346 self.
disp.log_debug(f
"Retrieved front-end file from cache: {path}")
352 """Generate HTML headers for front-end pages.
355 page_title: Title of the HTML page.
357 str: HTML headers as a string.
360 verification: str = CONST.GOOGLE_SITE_VERIFICATION_CODE
361 headers = f
"""<!DOCTYPE html>
365 <meta charset="UTF-8">
366 <meta name="Language" CONTENT="en,fr" />
367 <meta name="publisher" content="{self.author}" />
368 <meta http-equiv="pragma" content="cache" />
369 <meta http-equiv="Cache-control" content="public" />
370 <meta name="googlebot" content="index,follow,snippet" />
371 <meta name="google" content="translate,sitelinkssearchbox" />
372 <link rel="stylesheet" href="{self.front_end_assets_css_pico}">
373 <link rel="stylesheet" href="{self.front_end_assets_css_custom}">
374 <link rel="stylesheet" href="{self.front_end_assets_css_emoji_font}">
375 <meta name="google-site-verification" content="{verification}" />
376 <meta name="copyright" content=\"© {self.author} {self.year}"/>
377 <meta name="viewport" content="width=device-width, initial-scale=1.0">
378 <meta name="robots" content="index,follow,max-image-preview:standard" />
379 <link rel="icon" type="image/png" href="{self.front_end_assets_image_favicon_png}" />
380 <meta name="Index" content="This is the page named: {page_title} of the server {app_name}." />
381 <link rel="shortcut icon" type="image/x-icon" href="{self.front_end_assets_images_favicon}" />
387 """Generate HTML theme toggler for front-end pages.
390 str: HTML theme toggler as a string.
392 toggler =
"""<div class="theme-toggler">
393 <label for="theme-select">Theme:</label>
394 <select id="theme-select" aria-label="Theme selector">
395 <option value="system">System</option>
396 <option value="light">Light</option>
397 <option value="dark">Dark</option>
403 def _get_heading(self, page_title: str, show_logout: bool =
False) -> str:
404 """Generate HTML heading for front-end pages.
407 page_title: Title of the page.
408 show_logout: Whether to show the logout button.
410 str: HTML heading as a string.
415 logout_button =
'<div class="logout-container"><button onclick="logout()">Logout</button></div>'
416 heading = f
"""<header class="page-header">
417 <h1 class="page-title">{self.server_headers_initialised.app_name} - {page_title}</h1>
418 <div class="theme-container">{theme_toggle}</div>
424 def _get_footers(self, page_type: Pages = Pages.DEFAULT, client_host: str =
"") -> str:
425 """Generate HTML footers for front-end pages.
428 page_type: Type of the page (Pages enum).
430 str: HTML footers as a string.
435 host: str = client_host
436 if host ==
"0.0.0.0":
437 host =
"http://127.0.0.1"
438 if not host.startswith(
"http"):
439 host = f
"http://{host}"
445 if page_type == Pages.LOGIN:
446 page_scripts += f
'<script type="text/JavaScript" src="{self.front_end_assets_js_login}" data-dashboard-url="{self.front_end_dashboard}"></script>'
447 if page_type == Pages.DASHBOARD:
448 page_scripts += f
'<script type="text/JavaScript" src="{self.front_end_assets_js_dashboard}" data-login-url="{self.front_end_login}" data-logout-url="{self.front_end_logout}"></script>'
449 if page_type == Pages.LOGOUT:
450 page_scripts += f
'<script type="text/JavaScript" src="{self.front_end_assets_js_logout}" data-login-url="{self.front_end_login}"></script>'
451 footers = f
"""<div class="footer">
453 <p>© {self.author} {self.year}</p>
454 <script type="module" src="{self.front_end_assets_js_module_cookies}"></script>
455 <script type="module" src="{self.front_end_assets_js_module_indexdb}" data-db-name="{name}" data-store-name="keyValueStore"></script>
456 <script type="module" src="{self.front_end_assets_js_module_querier}" data-api-url="{host}" data-api-port="{port}"></script>
457 <script id="general-module" type="module" src="{self.front_end_assets_js_module_general}" data-theme-cookie-name="theme" data-cookie-expires-days="365"></script>
458 <script type="text/JavaScript" src="{self.front_end_assets_js_theme_pico}"></script>
460 <script type="module">
461 const moduleScript = document.getElementById('general-module');
462 const opts = moduleScript ? moduleScript.dataset : {"{}"};
463 window.general_scripts.initThemeToggler(opts);
471 """Serve the login page.
474 Response: FastAPI Response with login HTML content.
481 page_footer = self.
_get_footers(Pages.LOGIN, client_host)
482 page_content = f
"""{page_header}
493 """Serve the dashboard page.
496 Response: FastAPI Response with dashboard HTML content.
499 page_title =
"Dashboard"
501 page_heading = self.
_get_heading(page_title, show_logout=
True)
503 page_footer = self.
_get_footers(Pages.DASHBOARD, client_host)
504 page_content = f
"""{page_header}
515 """Serve the logout page.
518 Response: FastAPI Response with logout HTML content.
521 page_title =
"Logout"
525 page_footer = self.
_get_footers(Pages.LOGOUT, client_host)
526 page_content = f
"""{page_header}
539 """Get the Pico CSS content.
542 Response: FastAPI Response with Pico CSS content.
545 if isinstance(css_content, Response):
550 """Get the Pico CSS content.
553 Response: FastAPI Response with Pico CSS content.
556 if isinstance(css_content, Response):
561 """Get the Emoji font CSS content.
564 Response: FastAPI Response with Emoji font CSS content.
567 if isinstance(css_content, Response):
572 """Get the Emoji regular font file
575 Response: FastAPI Response with the Emoji font file
579 if isinstance(file_content, Response):
586 """Get the Emoji light font file
589 Response: FastAPI Response with the Emoji font file
594 if isinstance(file_content, Response):
601 """Get the Emoji medium font file
604 Response: FastAPI Response with the Emoji font file
608 if isinstance(file_content, Response):
615 """Get the Emoji regular font file
618 Response: FastAPI Response with the Emoji font file
623 if isinstance(file_content, Response):
630 """Get the Emoji regular font file
633 Response: FastAPI Response with the Emoji font file
637 if isinstance(file_content, Response):
644 """Get the cookies JavaScript module content.
647 Response: FastAPI Response with cookies JS module content.
650 if isinstance(js_content, Response):
652 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
655 """Get the IndexedDB JavaScript module content.
658 Response: FastAPI Response with IndexedDB JS module content.
661 if isinstance(js_content, Response):
663 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
666 """Get the Querier JavaScript module content.
669 Response: FastAPI Response with Querier JS module content.
672 if isinstance(js_content, Response):
674 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
677 """Get the General JavaScript module content.
679 Response: FastAPI Response with General JS module content.
682 if isinstance(js_content, Response):
684 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
687 """Get the Pico theme JavaScript content.
690 Response: FastAPI Response with Pico theme JS content.
693 if isinstance(js_content, Response):
695 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
698 """Get the dashboard JavaScript content.
701 Response: FastAPI Response with dashboard JS content.
704 if isinstance(js_content, Response):
706 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
709 """Get the login JavaScript content.
712 Response: FastAPI Response with login JS content.
715 if isinstance(js_content, Response):
717 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
720 """Get the logout JavaScript content.
723 Response: FastAPI Response with logout JS content.
726 if isinstance(js_content, Response):
728 return HCI.success(js_content, content_type=HttpDataTypes.JAVASCRIPT, headers=self.
server_headers_initialised.for_javascript())
731 """Get the favicon ICO content.
734 Response: FastAPI Response with favicon ICO content.
736 icon = CONST.ICON_PATH
737 self.
disp.log_debug(f
"Favicon path: {icon}")
738 if os.path.isfile(icon):
739 return HCI.success(icon, content_type=HttpDataTypes.XICON)
740 return HCI.not_found(
"Icon not found in the expected directory", content_type=HttpDataTypes.TEXT)
743 """Get the favicon PNG content.
746 Response: FastAPI Response with favicon PNG content.
748 icon = CONST.PNG_ICON_PATH
749 self.
disp.log_debug(f
"Static logo path: {icon}")
750 if os.path.isfile(icon):
752 return HCI.not_found(
"Icon not found in the expected directory", content_type=HttpDataTypes.TEXT)
757 """Inject front-end asset paths into the PathManager.
760 path_manager: Instance of PathManager to inject paths into.
783 for endpooint, handler
in assets.items():
784 path_manager.add_path_if_not_exists(
785 endpooint, handler,
"GET",
786 decorators=[decorators.public_endpoint(),
787 decorators.front_end_assets_endpoint]
791 """Inject front-end paths into the PathManager.
794 path_manager: Instance of PathManager to inject paths into.
796 path_manager.add_path_if_not_exists(
798 decorators=[decorators.public_endpoint(),
799 decorators.front_end_endpoint]
801 path_manager.add_path_if_not_exists(
803 decorators=[decorators.auth_endpoint(),
804 decorators.front_end_endpoint]
806 path_manager.add_path_if_not_exists(
808 decorators=[decorators.public_endpoint(),
809 decorators.front_end_endpoint]
Response serve_dashboard(self, Request request)
str front_end_assets_css_pico
Response serve_logout(self, Request request)
str front_end_assets_js_theme
str _get_headers(self, str page_title)
Response get_css_emoji_medium(self)
str _get_client_host(self, Request request)
str source_css_emoji_font_regular
Response get_cookies_module(self)
Union[str, Response] _get_cache(self, str path)
str front_end_assets_image_favicon_png
str source_css_emoji_font
str source_html_dashboard
str front_end_assets_css_emoji_font_static_bold
str front_end_assets_js_module_cookies
Response get_logout_js(self)
RuntimeManager database_link
None _load_cache(self, timedelta expiration=CACHE_LIFETIME)
str front_end_assets_css_emoji_font_static_medium
str front_end_assets_css_emoji_font_static_regular
str source_js_module_cookies
str _get_heading(self, str page_title, bool show_logout=False)
RuntimeManager boilerplate_non_http_initialised
Response get_css_emoji_light(self)
str front_end_assets_js_dashboard
str source_css_emoji_font_bold
None inject_paths(self, "PathManager" path_manager)
str front_end_assets_js_modules
Response get_favicon_ico(self)
str source_js_module_querier
Response get_css_emoji_bold(self)
Response get_css_emoji_regular(self)
Response get_custom_css(self)
Response get_general_module(self)
str front_end_assets_js_login
str front_end_assets_images_logos
str front_end_assets_js_module_general
Response _ressource_not_found_response(self, str path, str title)
str _get_footers(self, Pages page_type=Pages.DEFAULT, str client_host="")
str source_css_emoji_font_medium
Response get_querier_module(self)
str front_end_assets_html_logout
str front_end_assets_html_dashboard
None _inject_assets(self, "PathManager" path_manager)
timedelta cache_expiration
str source_css_emoji_font_semi_bold
str front_end_assets_images_favicon
str front_end_assets_js_theme_pico
str front_end_assets_html_login
str source_js_module_general
str front_end_assets_js_module_indexdb
RuntimeManager boilerplate_responses_initialised
Response get_favicon_png(self)
str source_js_module_indexdb
str _get_file_content(self, str file_path)
Response get_dashboard_js(self)
str front_end_assets_css_emoji_font
RuntimeManager boilerplate_incoming_initialised
Response get_login_js(self)
None _refresh_cache(self)
str front_end_assets_js_logout
str front_end_assets_js_module_querier
str front_end_assets_images
Response serve_login(self, Request request)
RuntimeManager server_headers_initialised
str source_css_emoji_font_light
str front_end_assets_css_emoji_font_static_light
Response get_pico_theme(self)
str front_end_assets_css_custom
Response get_css_emoji_font(self)
RuntimeManager runtime_manager
Response get_css_emoji_semi_bold(self)
None __init__(self, int error=84, int success=0, bool debug=False)
str front_end_assets_css_emoji_font_static_semi_bold
Response get_indexeddb_module(self)
str front_end_assets_css_emoji_font_static
str _get_theme_toggler(self)