@@ -40,12 +40,10 @@ def load_db():
4040 if "mod_library" not in data : data ["mod_library" ] = []
4141 if "server_configs" not in data : data ["server_configs" ] = []
4242 if "projects" not in data : data ["projects" ] = []
43-
4443 for m in data .get ("mods" , []):
4544 if "read" not in m : m ["read" ] = True
4645 for p in data .get ("projects" , []):
4746 if "read" not in p : p ["read" ] = True
48-
4947 return data
5048 except json .JSONDecodeError : return {}
5149
@@ -82,6 +80,48 @@ def fetch_mod_details(mod_input):
8280 except Exception as e :
8381 return mod_id , None , None , str (e )
8482
83+ # --- SMART JSON INSERTER (FIXES BRACKET ISSUES) ---
84+ def inject_mod (current_text , mod_obj ):
85+ """
86+ Intelligently inserts a mod object into a JSON list string.
87+ Handles trailing whitespace and malformed JSON better.
88+ """
89+ # 1. Clean input
90+ s = current_text .strip ()
91+
92+ # 2. Try Strict Parse (Best Case)
93+ try :
94+ data = json .loads (s )
95+ if isinstance (data , list ):
96+ data .append (mod_obj )
97+ return json .dumps (data , indent = 4 )
98+ except :
99+ pass # Fallback to string manipulation if JSON is currently invalid (common while editing)
100+
101+ # 3. String Manipulation Fallback
102+ snippet = json .dumps (mod_obj , indent = 4 )
103+
104+ if s .endswith ("]" ):
105+ # We found the end of the list.
106+ # Check if list is effectively empty "[]"
107+ if len (s ) < 3 :
108+ return f"[\n { snippet } \n ]"
109+
110+ # Peel off the last ']'
111+ # rstrip() removes whitespace/newlines before the bracket to prevent weird gaps
112+ content = s [:- 1 ].rstrip ()
113+
114+ # Add comma, new object, and close bracket
115+ return f"{ content } ,\n { snippet } \n ]"
116+
117+ elif not s :
118+ # Empty editor -> Start new list
119+ return f"[\n { snippet } \n ]"
120+
121+ else :
122+ # No closing bracket found? Just append (Safe fallback)
123+ return s + ",\n " + snippet
124+
85125# --- LOCAL SESSION STATE ---
86126if "logged_in" not in st .session_state : st .session_state .logged_in = False
87127if "current_user" not in st .session_state : st .session_state .current_user = None
@@ -90,7 +130,6 @@ def fetch_mod_details(mod_input):
90130if "selected_project_id" not in st .session_state : st .session_state .selected_project_id = None
91131if "editor_content" not in st .session_state : st .session_state .editor_content = "[\n \n ]"
92132if "fetched_mod" not in st .session_state : st .session_state .fetched_mod = None
93- # KEY FIX: Refresh counter to force text area update
94133if "editor_key" not in st .session_state : st .session_state .editor_key = 0
95134
96135# --- CALLBACK TO SYNC EDITOR ---
@@ -205,18 +244,14 @@ def get_mod_status():
205244
206245st .sidebar .button ("📢 Announcements" , on_click = navigate_to , args = ("view_announcements" ,))
207246
208- # MOVED: Mod Studio to Top (Super Admin Only)
209247if user_role == "SUPER_ADMIN" :
210248 st .sidebar .button ("📝 Mod Studio" , on_click = navigate_to , args = ("json_editor" ,))
211249
212- # --- ADMIN SECTION ---
213250if user_role in ["admin" , "SUPER_ADMIN" ]:
214251 st .sidebar .subheader ("Server Admin" )
215252 st .sidebar .button (f"{ get_mod_status ()} Report Broken Mod" , on_click = navigate_to , args = ("report_broken_mod" , None ))
216- # NEW JOB BUTTON (ADMIN ONLY)
217253 st .sidebar .button ("🚀 Submit New Job" , on_click = navigate_to , args = ("create_project" ,))
218254
219- # --- CLP SECTION ---
220255if user_role in ["CLPLEAD" , "SUPER_ADMIN" , "CLP" ]:
221256 st .sidebar .subheader ("CLP Management" )
222257 if user_role in ["CLPLEAD" , "SUPER_ADMIN" ]:
@@ -259,70 +294,48 @@ def get_mod_status():
259294 st .caption (f"{ a ['date' ]} by { a ['author' ]} " )
260295 st .markdown (a ['content' ], unsafe_allow_html = True )
261296
262- # --- CREATE PROJECT PAGE (Saves to PROJECTS) ---
263297elif st .session_state .page == "create_project" :
264298 st .title ("🚀 Submit New Job / Project" )
265299 st .caption ("This will create a task in the 'New Work' tab." )
266-
267300 with st .container (border = True ):
268301 p_name = st .text_input ("Project Title" )
269302 p_assign = st .text_input ("Lead Developer/Assignee" )
270303 p_sev = st .slider ("Severity / Priority" , 1 , 10 , 5 )
271304 st .write ("Project Brief:" )
272305 p_desc = st_quill (key = "proj_desc_page" , html = True )
273-
274306 if st .button ("Create Project" , type = "primary" ):
275- # SAVE TO 'projects' ONLY
276307 DB ['projects' ].append ({
277- "id" : len (DB ['projects' ]),
278- "name" : p_name ,
279- "assigned" : p_assign ,
280- "severity" : p_sev ,
281- "description" : p_desc ,
282- "complete" : False ,
283- "discussion" : [],
284- "read" : False
308+ "id" : len (DB ['projects' ]), "name" : p_name , "assigned" : p_assign , "severity" : p_sev ,
309+ "description" : p_desc , "complete" : False , "discussion" : [], "read" : False
285310 })
286311 save_db (DB )
287312 st .success ("Project Created! Saved to New Work." )
288313 st .session_state .page = "view_projects"
289314 st .rerun ()
290315
291- # --- REPORT BROKEN MOD PAGE (Saves to MODS) ---
292316elif st .session_state .page == "report_broken_mod" :
293317 st .title ("Report Broken Mod" )
294318 st .caption ("This will create a ticket in the 'Broken Mods' tab." )
295-
296319 name = st .text_input ("Mod Name" )
297320 json_code = st .text_area ("JSON Code" , height = 100 )
298321 sev = st .slider ("Severity" , 1 , 10 )
299322 assign = st .text_input ("Assign To" )
300323 st .write ("Description:" )
301324 desc = st_quill (key = "mod_desc" , html = True )
302325 if st .button ("Submit Report" ):
303- # SAVE TO 'mods' ONLY
304326 DB ['mods' ].append ({
305- "id" : len (DB ['mods' ]),
306- "name" : name ,
307- "json_data" : json_code ,
308- "severity" : sev ,
309- "assignment" : assign ,
310- "description" : desc ,
311- "complete" : False ,
312- "discussion" : [],
313- "read" : False
327+ "id" : len (DB ['mods' ]), "name" : name , "json_data" : json_code , "severity" : sev ,
328+ "assignment" : assign , "description" : desc , "complete" : False , "discussion" : [], "read" : False
314329 })
315330 save_db (DB )
316331 st .success ("Submitted! Saved to Broken Mods." )
317332 st .session_state .page = "view_broken_mods"
318333 st .rerun ()
319334
320- # --- VIEW BROKEN MODS (Reads from MODS) ---
321335elif st .session_state .page == "view_broken_mods" :
322336 if user_role not in ["admin" , "SUPER_ADMIN" ]: st .error ("Access Denied." )
323337 else :
324338 st .title ("Active Broken Mods" )
325- # READ FROM 'mods' ONLY
326339 active = [m for m in DB ['mods' ] if not m ['complete' ]]
327340 if not active : st .success ("No active issues." )
328341 for m in active :
@@ -333,17 +346,12 @@ def get_mod_status():
333346 st .subheader (f"{ prefix } { m ['name' ]} " )
334347 st .caption (f"Severity: { m ['severity' ]} | Assigned: { m ['assignment' ]} " )
335348 with c2 :
336- if st .button ("Details" , key = f"d_{ m ['id' ]} " , on_click = navigate_to , args = ("mod_detail" , m ['id' ], None )):
337- pass
349+ if st .button ("Details" , key = f"d_{ m ['id' ]} " , on_click = navigate_to , args = ("mod_detail" , m ['id' ], None )): pass
338350
339- # --- VIEW PROJECTS (Reads from PROJECTS) ---
340351elif st .session_state .page == "view_projects" :
341352 st .title ("New Work / Active Projects" )
342- # READ FROM 'projects' ONLY
343353 active_projs = [p for p in DB ['projects' ] if not p ['complete' ]]
344-
345- if not active_projs :
346- st .info ("No active projects." )
354+ if not active_projs : st .info ("No active projects." )
347355 else :
348356 for p in active_projs :
349357 with st .container (border = True ):
@@ -354,10 +362,8 @@ def get_mod_status():
354362 sev = p .get ('severity' , 1 )
355363 st .caption (f"Lead: { p ['assigned' ]} | Severity: { sev } /10" )
356364 with c2 :
357- if st .button ("Open" , key = f"p_{ p ['id' ]} " , on_click = navigate_to , args = ("project_detail" , None , p ['id' ])):
358- pass
365+ if st .button ("Open" , key = f"p_{ p ['id' ]} " , on_click = navigate_to , args = ("project_detail" , None , p ['id' ])): pass
359366
360- # --- VIEW FIXED MODS ---
361367elif st .session_state .page == "view_fixed_mods" :
362368 if user_role not in ["admin" , "SUPER_ADMIN" ]: st .error ("Access Denied." )
363369 else :
@@ -370,15 +376,12 @@ def get_mod_status():
370376 with c1 : st .subheader (f"✅ { m ['name' ]} " )
371377 with c2 : st .button ("Archive View" , key = f"a_{ m ['id' ]} " , on_click = navigate_to , args = ("mod_detail" , m ['id' ], None ))
372378
373- # --- MOD DETAIL ---
374379elif st .session_state .page == "mod_detail" :
375380 m = next ((x for x in DB ['mods' ] if x ['id' ] == st .session_state .selected_mod_id ), None )
376-
377381 if m and not m .get ('read' , True ) and user_role in ["admin" , "SUPER_ADMIN" ]:
378382 m ['read' ] = True
379383 save_db (DB )
380384 st .rerun ()
381-
382385 if m :
383386 st .title (f"Issue: { m ['name' ]} " )
384387 c1 , c2 = st .columns ([2 ,1 ])
@@ -404,25 +407,20 @@ def get_mod_status():
404407 with c2 :
405408 st .subheader ("Discussion" )
406409 chat = st .container (height = 400 , border = True )
407- for msg in m .get ('discussion' , []):
408- chat .markdown (f"**{ msg ['user' ]} **: { msg ['text' ]} " )
409-
410+ for msg in m .get ('discussion' , []): chat .markdown (f"**{ msg ['user' ]} **: { msg ['text' ]} " )
410411 with st .form ("chat" ):
411412 txt = st .text_input ("Message" )
412413 if st .form_submit_button ("Send" ) and txt :
413414 m .setdefault ('discussion' , []).append ({"user" : USER_NAME , "text" : txt , "time" : str (datetime .now ())})
414415 save_db (DB )
415416 st .rerun ()
416417
417- # --- PROJECT DETAIL ---
418418elif st .session_state .page == "project_detail" :
419419 p = next ((x for x in DB ['projects' ] if x ['id' ] == st .session_state .selected_project_id ), None )
420-
421420 if p and not p .get ('read' , True ) and user_role in ["admin" , "SUPER_ADMIN" ]:
422421 p ['read' ] = True
423422 save_db (DB )
424423 st .rerun ()
425-
426424 if p :
427425 st .title (f"Project: { p ['name' ]} " )
428426 c1 , c2 = st .columns ([2 ,1 ])
@@ -438,15 +436,11 @@ def get_mod_status():
438436 st .success ("Completed!" )
439437 st .session_state .page = "view_projects"
440438 st .rerun ()
441- else :
442- st .success ("Project Completed." )
439+ else : st .success ("Project Completed." )
443440 with c2 :
444441 st .subheader ("Discussion" )
445- # Unified Chat Style (No Dividers)
446442 chat = st .container (height = 400 , border = True )
447- for msg in p .get ('discussion' , []):
448- chat .markdown (f"**{ msg ['user' ]} **: { msg ['text' ]} " )
449-
443+ for msg in p .get ('discussion' , []): chat .markdown (f"**{ msg ['user' ]} **: { msg ['text' ]} " )
450444 with st .form ("p_chat" ):
451445 txt = st .text_input ("Message" )
452446 if st .form_submit_button ("Send" ) and txt :
@@ -554,7 +548,7 @@ def get_mod_status():
554548 found = next ((c for c in DB ['server_configs' ] if c ['name' ] == selected_conf ), None )
555549 if found :
556550 st .session_state .editor_content = found ['content' ]
557- st .session_state .editor_key += 1 # INCREMENT KEY
551+ st .session_state .editor_key += 1
558552 st .success (f"Loaded '{ selected_conf } '!" )
559553 st .rerun ()
560554 with c_save :
@@ -576,48 +570,41 @@ def get_mod_status():
576570 st .subheader ("Active JSON Editor" )
577571 st .caption ("Press 'Ctrl+A' then 'Ctrl+C' inside the box to copy everything." )
578572
579- # --- FIX: DYNAMIC KEY TO FORCE UPDATE ---
580573 json_text = st .text_area (
581574 "JSON Output" ,
582575 value = st .session_state .editor_content ,
583576 height = 600 ,
584- key = f"json_area_{ st .session_state .editor_key } " , # DYNAMIC KEY
577+ key = f"json_area_{ st .session_state .editor_key } " ,
585578 on_change = sync_editor
586579 )
587580
588581 with col_tools :
582+ # FIX: Place Tabs OUTSIDE the scrollable container
589583 tab_search , tab_saved , tab_import = st .tabs (["🌐 Search" , "💾 Library" , "📥 Import" ])
590584
591585 with tab_search :
592586 st .info ("💡 **Tip:** Type a name to find the link, then Paste the URL to fetch data." )
593587 search_term = st .text_input ("1. Search Term" , placeholder = "e.g. RHS Status Quo" )
594588 if search_term :
595589 st .link_button (f"🌐 Open Search: '{ search_term } '" , f"https://reforger.armaplatform.com/workshop?search={ search_term } " )
596-
597590 st .divider ()
598591 st .write ("**2. Paste Workshop URL**" )
599592 fetch_url = st .text_input ("Paste URL here to auto-fetch" , placeholder = "https://reforger.armaplatform.com/workshop/..." )
600-
601593 if st .button ("🚀 Fetch Details" ):
602594 if fetch_url :
603595 mid , mname , mimg , mver = fetch_mod_details (fetch_url )
604596 if mname :
605- st .session_state .fetched_mod = {
606- "modId" : mid , "name" : mname , "version" : mver , "image_url" : mimg
607- }
597+ st .session_state .fetched_mod = {"modId" : mid , "name" : mname , "version" : mver , "image_url" : mimg }
608598 st .success ("Found!" )
609599 else : st .error ("Could not find mod. Check URL." )
610600
611- # Scrollable results for search
612601 with st .container (height = 500 , border = True ):
613602 if st .session_state .fetched_mod :
614603 mod = st .session_state .fetched_mod
615604 if mod ['image_url' ]: st .image (mod ['image_url' ])
616605 st .subheader (mod ['name' ])
617-
618606 clean_mod = {"modId" : mod ['modId' ], "name" : mod ['name' ], "version" : "" }
619607 st .code (json .dumps (clean_mod , indent = 4 ), language = 'json' )
620-
621608 c1 , c2 = st .columns (2 )
622609 with c1 :
623610 if st .button ("💾 Save to Library" ):
@@ -626,54 +613,36 @@ def get_mod_status():
626613 st .success ("Saved!" )
627614 with c2 :
628615 if st .button ("➕ Add to Editor" ):
629- snippet = json .dumps (clean_mod , indent = 4 )
630- cur = st .session_state .editor_content .strip ()
631- if not cur : cur = "[]"
632- if cur .endswith ("]" ):
633- if len (cur ) > 2 : new_s = cur [:- 1 ] + ",\n " + snippet + "\n ]"
634- else : new_s = "[\n " + snippet + "\n ]"
635- else : new_s = cur + ",\n " + snippet
636- st .session_state .editor_content = new_s
637- st .session_state .editor_key += 1 # TRIGGER REFRESH
616+ new_text = inject_mod (st .session_state .editor_content , clean_mod )
617+ st .session_state .editor_content = new_text
618+ st .session_state .editor_key += 1
638619 st .rerun ()
639620
640621 with tab_saved :
641622 lib_search = st .text_input ("Filter Library" , placeholder = "Filter by name..." )
642-
643623 filtered = sorted (
644624 [m for m in DB ['mod_library' ] if lib_search .lower () in m .get ('name' ,'' ).lower ()],
645625 key = lambda x : x .get ('name' , '' ).lower ()
646626 )
647-
648627 with st .container (height = 600 , border = True ):
649628 if not filtered : st .info ("No saved mods." )
650629 for mod in filtered :
651630 with st .container (border = True ):
652631 c_info , c_add , c_copy , c_del = st .columns ([3 , 1 , 1 , 1 ], vertical_alignment = "center" )
653-
654632 with c_info :
655633 st .write (f"**{ mod ['name' ]} **" )
656634 mini_json = {"modId" : mod ['modId' ], "name" : mod ['name' ], "version" : "" }
657635 json_str = json .dumps (mini_json , indent = 4 )
658-
659636 with c_add :
660637 if st .button ("➕" , key = f"ins_{ mod ['modId' ]} " , help = "Insert into Editor" , use_container_width = True ):
661- snippet = json_str
662- cur = st .session_state .editor_content .strip ()
663- if not cur : cur = "[]"
664- if cur .endswith ("]" ):
665- if len (cur ) > 2 : new_s = cur [:- 1 ] + ",\n " + snippet + "\n ]"
666- else : new_s = "[\n " + snippet + "\n ]"
667- else : new_s = cur + ",\n " + snippet
668- st .session_state .editor_content = new_s
669- st .session_state .editor_key += 1 # TRIGGER REFRESH
638+ new_text = inject_mod (st .session_state .editor_content , mini_json )
639+ st .session_state .editor_content = new_text
640+ st .session_state .editor_key += 1
670641 st .rerun ()
671-
672642 with c_copy :
673643 with st .popover ("📋" , use_container_width = True ):
674644 st .code (json_str , language = 'json' )
675645 st .caption ("Click the icon in the corner to copy." )
676-
677646 with c_del :
678647 if st .button ("🗑️" , key = f"rm_{ mod ['modId' ]} " , help = "Delete from Library" , use_container_width = True ):
679648 idx = DB ['mod_library' ].index (mod )
@@ -685,7 +654,6 @@ def get_mod_status():
685654 st .subheader ("Batch Importer" )
686655 st .caption ("Paste a full JSON file or a list of mods. We will extract every mod block and save it to your library." )
687656 import_text = st .text_area ("Paste JSON Here" , height = 300 )
688-
689657 if st .button ("Process & Import Mods" , type = "primary" ):
690658 try :
691659 pattern = r'\{[^{}]*"modId"[^{}]*\}'
0 commit comments