@@ -68,23 +68,32 @@ def _build_config_json(app: FastAPI, request: Request) -> str:
6868 radio_config_dict = None
6969 if radio_config :
7070 radio_config_dict = {
71+ "profile" : radio_config .profile ,
7172 "frequency" : radio_config .frequency ,
7273 "bandwidth" : radio_config .bandwidth ,
7374 "spreading_factor" : radio_config .spreading_factor ,
7475 "coding_rate" : radio_config .coding_rate ,
76+ "tx_power" : radio_config .tx_power ,
7577 }
7678
77- # Get custom pages for navigation
79+ # Get feature flags
80+ features = app .state .features
81+
82+ # Get custom pages for navigation (empty when pages feature is disabled)
7883 page_loader = app .state .page_loader
79- custom_pages = [
80- {
81- "slug" : p .slug ,
82- "title" : p .title ,
83- "url" : p .url ,
84- "menu_order" : p .menu_order ,
85- }
86- for p in page_loader .get_menu_pages ()
87- ]
84+ custom_pages = (
85+ [
86+ {
87+ "slug" : p .slug ,
88+ "title" : p .title ,
89+ "url" : p .url ,
90+ "menu_order" : p .menu_order ,
91+ }
92+ for p in page_loader .get_menu_pages ()
93+ ]
94+ if features .get ("pages" , True )
95+ else []
96+ )
8897
8998 config = {
9099 "network_name" : app .state .network_name ,
@@ -97,6 +106,7 @@ def _build_config_json(app: FastAPI, request: Request) -> str:
97106 "network_contact_youtube" : app .state .network_contact_youtube ,
98107 "network_welcome_text" : app .state .network_welcome_text ,
99108 "admin_enabled" : app .state .admin_enabled ,
109+ "features" : features ,
100110 "custom_pages" : custom_pages ,
101111 "logo_url" : app .state .logo_url ,
102112 "version" : __version__ ,
@@ -121,6 +131,7 @@ def create_app(
121131 network_contact_github : str | None = None ,
122132 network_contact_youtube : str | None = None ,
123133 network_welcome_text : str | None = None ,
134+ features : dict [str , bool ] | None = None ,
124135) -> FastAPI :
125136 """Create and configure the web dashboard application.
126137
@@ -140,6 +151,7 @@ def create_app(
140151 network_contact_github: GitHub repository URL
141152 network_contact_youtube: YouTube channel URL
142153 network_welcome_text: Welcome text for homepage
154+ features: Feature flags dict (default: all enabled from settings)
143155
144156 Returns:
145157 Configured FastAPI application
@@ -189,6 +201,24 @@ def create_app(
189201 network_welcome_text or settings .network_welcome_text
190202 )
191203
204+ # Store feature flags with automatic dependencies:
205+ # - Dashboard requires at least one of nodes/advertisements/messages
206+ # - Map requires nodes (map displays node locations)
207+ effective_features = features if features is not None else settings .features
208+ overrides : dict [str , bool ] = {}
209+ has_dashboard_content = (
210+ effective_features .get ("nodes" , True )
211+ or effective_features .get ("advertisements" , True )
212+ or effective_features .get ("messages" , True )
213+ )
214+ if not has_dashboard_content :
215+ overrides ["dashboard" ] = False
216+ if not effective_features .get ("nodes" , True ):
217+ overrides ["map" ] = False
218+ if overrides :
219+ effective_features = {** effective_features , ** overrides }
220+ app .state .features = effective_features
221+
192222 # Set up templates (for SPA shell only)
193223 templates = Jinja2Templates (directory = str (TEMPLATES_DIR ))
194224 templates .env .trim_blocks = True
@@ -309,6 +339,8 @@ async def api_proxy(request: Request, path: str) -> Response:
309339 @app .get ("/map/data" , tags = ["Map" ])
310340 async def map_data (request : Request ) -> JSONResponse :
311341 """Return node location data as JSON for the map."""
342+ if not request .app .state .features .get ("map" , True ):
343+ return JSONResponse ({"detail" : "Map feature is disabled" }, status_code = 404 )
312344 nodes_with_location : list [dict [str , Any ]] = []
313345 members_list : list [dict [str , Any ]] = []
314346 members_by_id : dict [str , dict [str , Any ]] = {}
@@ -448,6 +480,10 @@ async def map_data(request: Request) -> JSONResponse:
448480 @app .get ("/spa/pages/{slug}" , tags = ["SPA" ])
449481 async def get_custom_page (request : Request , slug : str ) -> JSONResponse :
450482 """Get a custom page by slug."""
483+ if not request .app .state .features .get ("pages" , True ):
484+ return JSONResponse (
485+ {"detail" : "Pages feature is disabled" }, status_code = 404
486+ )
451487 page_loader = request .app .state .page_loader
452488 page = page_loader .get_page (slug )
453489 if not page :
@@ -489,21 +525,57 @@ def _get_https_base_url(request: Request) -> str:
489525 async def robots_txt (request : Request ) -> str :
490526 """Serve robots.txt."""
491527 base_url = _get_https_base_url (request )
492- return f"User-agent: *\n Disallow:\n \n Sitemap: { base_url } /sitemap.xml\n "
528+ features = request .app .state .features
529+
530+ # Always disallow message and node detail pages
531+ disallow_lines = [
532+ "Disallow: /messages" ,
533+ "Disallow: /nodes/" ,
534+ ]
535+
536+ # Add disallow for disabled features
537+ feature_paths = {
538+ "dashboard" : "/dashboard" ,
539+ "nodes" : "/nodes" ,
540+ "advertisements" : "/advertisements" ,
541+ "map" : "/map" ,
542+ "members" : "/members" ,
543+ "pages" : "/pages" ,
544+ }
545+ for feature , path in feature_paths .items ():
546+ if not features .get (feature , True ):
547+ line = f"Disallow: { path } "
548+ if line not in disallow_lines :
549+ disallow_lines .append (line )
550+
551+ disallow_block = "\n " .join (disallow_lines )
552+ return (
553+ f"User-agent: *\n "
554+ f"{ disallow_block } \n "
555+ f"\n "
556+ f"Sitemap: { base_url } /sitemap.xml\n "
557+ )
493558
494559 @app .get ("/sitemap.xml" )
495560 async def sitemap_xml (request : Request ) -> Response :
496- """Generate dynamic sitemap including all node pages ."""
561+ """Generate dynamic sitemap."""
497562 base_url = _get_https_base_url (request )
563+ features = request .app .state .features
564+
565+ # Home is always included; other pages depend on feature flags
566+ all_static_pages = [
567+ ("" , "daily" , "1.0" , None ),
568+ ("/dashboard" , "hourly" , "0.9" , "dashboard" ),
569+ ("/nodes" , "hourly" , "0.9" , "nodes" ),
570+ ("/advertisements" , "hourly" , "0.8" , "advertisements" ),
571+ ("/map" , "daily" , "0.7" , "map" ),
572+ ("/members" , "weekly" , "0.6" , "members" ),
573+ ]
498574
499575 static_pages = [
500- ("" , "daily" , "1.0" ),
501- ("/dashboard" , "hourly" , "0.9" ),
502- ("/nodes" , "hourly" , "0.9" ),
503- ("/advertisements" , "hourly" , "0.8" ),
504- ("/messages" , "hourly" , "0.8" ),
505- ("/map" , "daily" , "0.7" ),
506- ("/members" , "weekly" , "0.6" ),
576+ (path , freq , prio )
577+ for path , freq , prio , feature in all_static_pages
578+ if feature is None or features .get (feature , True )
507579 ]
508580
509581 urls = []
@@ -516,34 +588,16 @@ async def sitemap_xml(request: Request) -> Response:
516588 f" </url>"
517589 )
518590
519- try :
520- response = await request .app .state .http_client .get (
521- "/api/v1/nodes" , params = {"limit" : 500 , "role" : "infra" }
522- )
523- if response .status_code == 200 :
524- nodes = response .json ().get ("items" , [])
525- for node in nodes :
526- public_key = node .get ("public_key" )
527- if public_key :
528- urls .append (
529- f" <url>\n "
530- f" <loc>{ base_url } /nodes/{ public_key [:8 ]} </loc>\n "
531- f" <changefreq>daily</changefreq>\n "
532- f" <priority>0.5</priority>\n "
533- f" </url>"
534- )
535- except Exception as e :
536- logger .warning (f"Failed to fetch nodes for sitemap: { e } " )
537-
538- page_loader = request .app .state .page_loader
539- for page in page_loader .get_menu_pages ():
540- urls .append (
541- f" <url>\n "
542- f" <loc>{ base_url } { page .url } </loc>\n "
543- f" <changefreq>weekly</changefreq>\n "
544- f" <priority>0.6</priority>\n "
545- f" </url>"
546- )
591+ if features .get ("pages" , True ):
592+ page_loader = request .app .state .page_loader
593+ for page in page_loader .get_menu_pages ():
594+ urls .append (
595+ f" <url>\n "
596+ f" <loc>{ base_url } { page .url } </loc>\n "
597+ f" <changefreq>weekly</changefreq>\n "
598+ f" <priority>0.6</priority>\n "
599+ f" </url>"
600+ )
547601
548602 xml = (
549603 '<?xml version="1.0" encoding="UTF-8"?>\n '
@@ -559,8 +613,11 @@ async def sitemap_xml(request: Request) -> Response:
559613 async def spa_catchall (request : Request , path : str = "" ) -> HTMLResponse :
560614 """Serve the SPA shell for all non-API routes."""
561615 templates_inst : Jinja2Templates = request .app .state .templates
616+ features = request .app .state .features
562617 page_loader = request .app .state .page_loader
563- custom_pages = page_loader .get_menu_pages ()
618+ custom_pages = (
619+ page_loader .get_menu_pages () if features .get ("pages" , True ) else []
620+ )
564621
565622 config_json = _build_config_json (request .app , request )
566623
@@ -577,6 +634,7 @@ async def spa_catchall(request: Request, path: str = "") -> HTMLResponse:
577634 "network_contact_youtube" : request .app .state .network_contact_youtube ,
578635 "network_welcome_text" : request .app .state .network_welcome_text ,
579636 "admin_enabled" : request .app .state .admin_enabled ,
637+ "features" : features ,
580638 "custom_pages" : custom_pages ,
581639 "logo_url" : request .app .state .logo_url ,
582640 "version" : __version__ ,
0 commit comments