chore: archive 27 diagnostic scripts used during the missing-end investigation
These scripts were created during the search for the "Missing End()" imgui error that the user reported on 2026-06-29. They are throwaway diagnostic tools; their purpose was to find the orphan imgui.end_child() call in render_tier_stream_panel (commitc2155593) and verify the fix worked. No production code depends on these. They are kept for archival purposes only so future debugging of similar imbalanced-begin/end issues has a reference. Scripts included: - apply_fix.py : the actual applied fix to src/gui_2.py - fix_orphan.py/fix_orphan2.py : iterative attempts at removing the orphan - fix_indent.py : was used to attempt an indent fix; superseded - remove_orphan.py : rejected because pattern didn't match - find_imbalance.py : the canonical begin/end imbalance detector - find_extras.py : finds orphan imgui.end() (window-level) - find_ends.py : dumps all imgui.end() lines with context - peek*.py (8 files) : various context-dump helpers used during investigation - check_dynamic.py : dynamic-control-flow imbalanced tracker - check_indents.py : indent diagnostic for L7086 - diag_install_heuristic.py : earlier diagnostic for install heuristic - inspect_imgui_apis.py : dumps imgui-bundle API surface - search_indent*.py (3) : indent search helpers - window_balance.py : dedicated imgui.begin/imgui.end balance check - apply_fix.py/remove_orphan2.py : final iterations that succeeded None of these are imported by src/ or tests/. The fix commitc2155593is the actual production change; these scripts are just the trail of breadcrumbs left during the investigation.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
"""Fix the orphan end_child and the indent issue. Single commit."""
|
||||
from pathlib import Path
|
||||
|
||||
f = Path("src/gui_2.py")
|
||||
data = f.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# Fix: change 4 spaces to 5 spaces before the render_selectable_label
|
||||
# (was incorrectly dedented in earlier commit, broke the begin_child body scope)
|
||||
old_line = " render_selectable_label(app, f'stream_t3_"
|
||||
new_line = " render_selectable_label(app, f'stream_t3_"
|
||||
if old_line in data:
|
||||
new_data = data.replace(old_line, new_line, 1)
|
||||
f.write_text(new_data, encoding="utf-8")
|
||||
print("fix applied: render_selectable_label inflated to 5 spaces")
|
||||
else:
|
||||
print("ERROR: pattern not found")
|
||||
for i, line in enumerate(data.split("\n")):
|
||||
if "render_selectable_label" in line and "stream_t3" in line:
|
||||
print(f" candidate L{i+1}: {line!r}")
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Check imgui begin/end balance dynamically (simulate runtime paths)."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
text = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = text.split("\n")
|
||||
|
||||
# Track all begin/end pairs across all types
|
||||
patterns = {
|
||||
"begin": re.compile(r"\bimgui\.begin(?:_(\w+))?\("),
|
||||
"end": re.compile(r"\bimgui\.end(?:_(\w+))?\("),
|
||||
}
|
||||
|
||||
# Walk line by line, but track conditional branches
|
||||
# For imgui's static check, we just need to count begin/end pairs
|
||||
# in sequence. The "Missing End" comes from extra ends, not missing ends.
|
||||
|
||||
balance = 0
|
||||
events = []
|
||||
for i, line in enumerate(lines, 1):
|
||||
for match in patterns["begin"].finditer(line):
|
||||
subtype = match.group(1) or "default"
|
||||
events.append((i, "begin", subtype))
|
||||
for match in patterns["end"].finditer(line):
|
||||
subtype = match.group(1) or "default"
|
||||
events.append((i, "end", subtype))
|
||||
|
||||
# Walk events and track stack
|
||||
stack = []
|
||||
extras = []
|
||||
for ev in events:
|
||||
line, kind, subtype = ev
|
||||
if kind == "begin":
|
||||
stack.append(ev)
|
||||
else:
|
||||
if stack:
|
||||
stack.pop()
|
||||
else:
|
||||
extras.append(ev)
|
||||
|
||||
# Check which begin has no matching end
|
||||
print(f"=== LEFTOVER begins (count: {len(stack)}) ===")
|
||||
for ev in stack:
|
||||
print(f" L{ev[0]}: {ev[1]} {ev[2]}")
|
||||
print(f"=== EXTRAS ends (count: {len(extras)}) ===")
|
||||
for ev in extras:
|
||||
print(f" L{ev[0]}: {ev[1]} {ev[2]}")
|
||||
@@ -0,0 +1,19 @@
|
||||
from pathlib import Path
|
||||
data = Path("src/gui_2.py").read_bytes()
|
||||
print(f"File size: {len(data)} bytes")
|
||||
search = b"render_selectable_label"
|
||||
positions = []
|
||||
i = 0
|
||||
while True:
|
||||
j = data.find(search, i)
|
||||
if j == -1: break
|
||||
positions.append(j)
|
||||
i = j + 1
|
||||
print(f"Found {len(positions)} occurrences: {positions[:5]}")
|
||||
# Show 60 bytes around each
|
||||
for p in positions:
|
||||
line_start = data.rfind(b"\n", 0, p) + 1
|
||||
line_end = data.find(b"\n", p)
|
||||
line = data[line_start:line_end]
|
||||
leading = len(line) - len(line.lstrip(b" "))
|
||||
print(f" At offset {p}: leading={leading}, line={line!r}")
|
||||
@@ -0,0 +1,11 @@
|
||||
from pathlib import Path
|
||||
dst = Path("manualslop_layout.ini")
|
||||
dst_text = dst.read_text(encoding="utf-8", errors="replace") if dst.exists() else ""
|
||||
print("size:", len(dst_text), "bytes")
|
||||
print("has '[Window][' header:", "[Window][" in dst_text)
|
||||
is_empty = len(dst_text) < 1000 or "[Window][" not in dst_text
|
||||
print("is_dst_empty heuristic:", is_empty)
|
||||
print("would-install:", is_empty)
|
||||
print()
|
||||
print("=== full content ===")
|
||||
print(dst_text)
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Find ALL imgui.end() lines in the file with context."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
text = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = text.split("\n")
|
||||
|
||||
# ALL imgui.end() (not end_X)
|
||||
for i, line in enumerate(lines, 1):
|
||||
if re.search(r"\bimgui\.end\(\)", line):
|
||||
print(f"L{i}: {line.strip()[:80]}")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Find imgui.begin matching L3757 and L8809 (they're EXTRA ends)."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
text = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = text.split("\n")
|
||||
|
||||
# Find the 4 imgui.begin calls with their context
|
||||
print("=== ALL imgui.begin calls ===")
|
||||
for i, line in enumerate(lines, 1):
|
||||
if re.search(r"\bimgui\.begin\(", line):
|
||||
print(f" L{i}: {line.strip()[:80]}")
|
||||
|
||||
print()
|
||||
print("=== imgui.end at L3757 (context ±15 lines) ===")
|
||||
for i in range(3735, 3770):
|
||||
line = lines[i-1] if i-1 < len(lines) else ""
|
||||
print(f" L{i}: {line.strip()[:80]}")
|
||||
|
||||
print()
|
||||
print("=== imgui.end at L8809 (context ±15 lines) ===")
|
||||
for i in range(8795, 8825):
|
||||
line = lines[i-1] if i-1 < len(lines) else ""
|
||||
print(f" L{i}: {line.strip()[:80]}")
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Find imgui.begin_child without matching imgui.end_child in src/gui_2.py (corrected)."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
text = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = text.splitlines()
|
||||
|
||||
stack = []
|
||||
extras = []
|
||||
for i, line in enumerate(lines, 1):
|
||||
n_begin = len(re.findall(r"\bimgui\.begin_child\(", line))
|
||||
n_end = len(re.findall(r"\bimgui\.end_child\(\)", line))
|
||||
for _ in range(n_begin):
|
||||
stack.append((i, line.strip()))
|
||||
for _ in range(n_end):
|
||||
if stack:
|
||||
stack.pop()
|
||||
else:
|
||||
extras.append((i, line.strip()))
|
||||
|
||||
print(f"=== Leftover begin_child (no matching end_child) ===")
|
||||
print(f"count: {len(stack)}")
|
||||
for ln, txt in stack:
|
||||
print(f" L{ln}: {txt}")
|
||||
print()
|
||||
print(f"=== Extra end_child (no matching begin_child) ===")
|
||||
print(f"count: {len(extras)}")
|
||||
for ln, txt in extras:
|
||||
print(f" L{ln}: {txt}")
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Fix the pre-existing indent at L7086."""
|
||||
from pathlib import Path
|
||||
|
||||
f = Path("src/gui_2.py")
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# Find the bad line and re-indent it
|
||||
old = ' render_selectable_label(app, f\'stream_t3_{ticket_id}\', app.mma_streams[key], width=-1, multiline=True, height=0)\n'
|
||||
new = ' render_selectable_label(app, f\'stream_t3_{ticket_id}\', app.mma_streams[key], width=-1, multiline=True, height=0)\n'
|
||||
|
||||
if old in text:
|
||||
text = text.replace(old, new)
|
||||
f.write_text(text, encoding="utf-8")
|
||||
print("replaced ok")
|
||||
else:
|
||||
print("ERROR: pattern not found")
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Fix the orphan end_child in src/gui_2.py at the tier3 section."""
|
||||
from pathlib import Path
|
||||
|
||||
f = Path("src/gui_2.py")
|
||||
text = f.read_text(encoding="utf-8", errors="replace")
|
||||
lines = text.splitlines(keepends=True)
|
||||
|
||||
# Find the bad section and replace
|
||||
old = ''' #NOTE(Ed): Exception(Thirdparty)
|
||||
try:
|
||||
if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1):
|
||||
imgui.set_scroll_here_y(1.0)
|
||||
app._tier_stream_last_len[key] = len(app.mma_streams[key])
|
||||
imgui.end_child()
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
imgui.end_child()'''
|
||||
new = ''' #NOTE(Ed): Exception(Thirdparty)
|
||||
try:
|
||||
if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1):
|
||||
imgui.set_scroll_here_y(1.0)
|
||||
app._tier_stream_last_len[key] = len(app.mma_streams[key])
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
finally:
|
||||
imgui.end_child()'''
|
||||
|
||||
if old in text:
|
||||
new_text = text.replace(old, new)
|
||||
f.write_text(new_text, encoding="utf-8")
|
||||
print("replaced ok")
|
||||
else:
|
||||
print("ERROR: old block not found verbatim")
|
||||
print("---looking for nearby text---")
|
||||
if "tier3_" in text:
|
||||
idx = text.find("tier3_")
|
||||
print(text[idx:idx+800])
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Inspect imgui-bundle INI APIs to figure out which function applies settings properly."""
|
||||
from imgui_bundle import imgui, hello_imgui
|
||||
|
||||
print("=== imgui-bundle hello_imgui INI APIs ===")
|
||||
for x in sorted(dir(hello_imgui)):
|
||||
if "ini" in x.lower() or "settings" in x.lower() or "prefs" in x.lower() or "user_pref" in x.lower():
|
||||
print(f" hello_imgui.{x}")
|
||||
|
||||
print()
|
||||
print("=== imgui core INI APIs ===")
|
||||
for x in sorted(dir(imgui)):
|
||||
if "ini" in x.lower():
|
||||
print(f" imgui.{x}")
|
||||
|
||||
print()
|
||||
print("=== Probe functions ===")
|
||||
try:
|
||||
fn = imgui.load_ini_settings_from_memory
|
||||
print(f"imgui.load_ini_settings_from_memory: {fn!r}")
|
||||
print(f" type: {type(fn)}")
|
||||
if fn.__doc__:
|
||||
print(f" doc[:300]: {fn.__doc__[:300]}")
|
||||
except Exception as e:
|
||||
print(f" err: {e}")
|
||||
|
||||
# Check if load_ini_settings_from_disk and load_ini_settings_from_memory differ in behavior
|
||||
import os, sys
|
||||
# Try to call load_ini_settings_from_memory with empty text
|
||||
print()
|
||||
print("=== Inspect hello_imgui default settings location ===")
|
||||
try:
|
||||
folder = hello_imgui.ini_folder_location()
|
||||
print(f" ini_folder_location(): {folder!r}")
|
||||
except Exception as e:
|
||||
print(f" err: {e}")
|
||||
try:
|
||||
settings = hello_imgui.ini_settings_location('Default')
|
||||
print(f" ini_settings_location('Default'): {settings!r}")
|
||||
except Exception as e:
|
||||
print(f" err: {e}")
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Get the original 71028dad content of the tier3_ section to compare."""
|
||||
import subprocess
|
||||
out = subprocess.run(["git", "show", "71028dad:src/gui_2.py"], cwd=".", capture_output=True, errors="ignore", text=True, encoding="utf-8")
|
||||
text = out.stdout
|
||||
# Find tier3_
|
||||
idx = text.find("tier3_")
|
||||
print(text[idx:idx+2000])
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Show the actual file lines around 7088."""
|
||||
from pathlib import Path
|
||||
lines = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
print(f"Total lines: {len(lines)}")
|
||||
print()
|
||||
for i in range(7080, 7098):
|
||||
print(f" L{i+1}: {lines[i]!r}")
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Show exact bytes at L7085-L7090."""
|
||||
from pathlib import Path
|
||||
data = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = data.splitlines()
|
||||
for i in range(7084, 7090):
|
||||
line = lines[i]
|
||||
leading = len(line) - len(line.lstrip(" "))
|
||||
print(f"L{i+1}: leading={leading!r} rest={line.strip()!r}")
|
||||
# Show first 10 chars as hex
|
||||
first = line[:leading+5]
|
||||
print(f" first_chars={first!r}")
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Show L7080-L7100 from current file with exact indentation."""
|
||||
from pathlib import Path
|
||||
data = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = data.splitlines()
|
||||
for i in range(7080, 7100):
|
||||
line = lines[i]
|
||||
leading = len(line) - len(line.lstrip(" "))
|
||||
print(f" L{i+1} [{leading} spaces]: {line.strip()[:80]}")
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Show L7080-L7095 exact content."""
|
||||
from pathlib import Path
|
||||
lines = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
for i in range(7080, 7098):
|
||||
line = lines[i]
|
||||
leading = len(line) - len(line.lstrip(" "))
|
||||
print(f"L{i+1} [{leading}]: {line}")
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Show full context of the indent error."""
|
||||
from pathlib import Path
|
||||
lines = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
for i in range(7050, 7100):
|
||||
line = lines[i]
|
||||
leading = len(line) - len(line.lstrip(" "))
|
||||
print(f"L{i+1} [{leading}]: {line}")
|
||||
@@ -0,0 +1,7 @@
|
||||
from pathlib import Path
|
||||
data = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = data.split("\n")
|
||||
for i in range(7080, 7100):
|
||||
line = lines[i]
|
||||
leading = len(line) - len(line.lstrip(" "))
|
||||
print(f"L{i+1} [{leading}]: {line[:80]!r}")
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Look at the conditional structure around L3757 and L8809."""
|
||||
from pathlib import Path
|
||||
|
||||
text = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = text.split("\n")
|
||||
|
||||
# Look at the structure around L3757 (Persona Editor)
|
||||
print("=== Persona Editor area: L3574-L3762 ===")
|
||||
for i in range(3574, 3762):
|
||||
line = lines[i-1]
|
||||
print(f" L{i}: {line.strip()[:80]}")
|
||||
print()
|
||||
print("=== Command Palette area: L8770-L8812 ===")
|
||||
for i in range(8770, 8812):
|
||||
line = lines[i-1]
|
||||
print(f" L{i}: {line.strip()[:80]}")
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Re-apply ONLY the orphan fix: remove the end_child in the except block.
|
||||
The original code (71028dad) had:
|
||||
try:
|
||||
...
|
||||
imgui.end_child() <-- in try
|
||||
except (...):
|
||||
imgui.end_child() <-- in except (the orphan)
|
||||
pass
|
||||
Both fire on different paths. If try succeeds, only the first fires (closes begin).
|
||||
If try fails, only the second fires (closes begin). But find_imbalance.py
|
||||
showed 1 extra end_child at L7094. That's the except's. Removing it should fix
|
||||
the imbalance without breaking the try-success path.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
f = Path("src/gui_2.py")
|
||||
data = f.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# The orphan line: " imgui.end_child()\n pass\n if app.perf_profiling_enabled"
|
||||
# Remove the orphan end_child but keep the rest intact.
|
||||
# Use the unique surrounding context.
|
||||
old = " imgui.end_child()\n pass\n if app.perf_profiling_enabled"
|
||||
new = " pass\n if app.perf_profiling_enabled"
|
||||
|
||||
if old in data:
|
||||
data = data.replace(old, new, 1)
|
||||
f.write_text(data, encoding="utf-8")
|
||||
print("orphan removed successfully")
|
||||
else:
|
||||
print("ERROR: pattern not found")
|
||||
# Search for the surrounding context
|
||||
idx = data.find("pass\n if app.perf_profiling_enabled")
|
||||
if idx == -1:
|
||||
# Try without leading newline
|
||||
idx = data.find("pass\n if app.perf_profiling_enabled")
|
||||
if idx >= 0:
|
||||
print(f" Context: {data[max(0,idx-100):idx+100]!r}")
|
||||
else:
|
||||
# Find imgui.end_child lines
|
||||
for i, line in enumerate(data.split("\n")):
|
||||
if "imgui.end_child()" in line:
|
||||
print(f" L{i+1}: {line!r}")
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Remove the orphan end_child in the except block."""
|
||||
from pathlib import Path
|
||||
|
||||
f = Path("src/gui_2.py")
|
||||
data = f.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
# Use the specific pattern that the search returned:
|
||||
# " except (TypeError, AttributeError):\n imgui.end_child()\n pass\n"
|
||||
# Remove the imgui.end_child line.
|
||||
old = """ except (TypeError, AttributeError):
|
||||
imgui.end_child()
|
||||
pass
|
||||
"""
|
||||
new = """ except (TypeError, AttributeError):
|
||||
pass
|
||||
"""
|
||||
|
||||
if old in data:
|
||||
data = data.replace(old, new, 1)
|
||||
f.write_text(data, encoding="utf-8")
|
||||
print("orphan removed")
|
||||
else:
|
||||
print("ERROR: pattern not found")
|
||||
@@ -0,0 +1,9 @@
|
||||
from pathlib import Path
|
||||
data = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
pattern = " render_selectable_label(app, f'stream_t3_{ticket_id}'\n"
|
||||
count = data.count(pattern)
|
||||
print(f"pattern occurrences: {count}")
|
||||
idx = 0
|
||||
for i in range(count):
|
||||
idx = data.find(pattern, idx if i == 0 else idx + 1)
|
||||
print(f" match {i}: at offset {idx}")
|
||||
@@ -0,0 +1,6 @@
|
||||
from pathlib import Path
|
||||
data = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
idx = data.find("render_selectable_label")
|
||||
print(f"Match at offset {idx}")
|
||||
print("Context:")
|
||||
print(repr(data[max(0, idx-50):idx+200]))
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
data = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
idx = data.find("render_selectable_label(app, f'stream_t3")
|
||||
print(f"Found at offset {idx}")
|
||||
print(repr(data[max(0, idx-50):idx+250]))
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Find imgui.begin/end (window-level) and imgui.end_child (child-level) only.
|
||||
Skip tree_node (end_group) and other unrelated types.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
text = Path("src/gui_2.py").read_text(encoding="utf-8", errors="replace")
|
||||
lines = text.split("\n")
|
||||
|
||||
# Only count imgui.begin( and imgui.end( (window-level)
|
||||
# These are the actual MainDockSpace begin/end
|
||||
# Also count imgui.begin_child / end_child
|
||||
balance = {"begin": [], "end": [], "begin_child": [], "end_child": []}
|
||||
for i, line in enumerate(lines, 1):
|
||||
if re.search(r"\bimgui\.begin\(", line):
|
||||
balance["begin"].append(i)
|
||||
if re.search(r"\bimgui\.end\(", line):
|
||||
balance["end"].append(i)
|
||||
if re.search(r"\bimgui\.begin_child\(", line):
|
||||
balance["begin_child"].append(i)
|
||||
if re.search(r"\bimgui\.end_child\(", line):
|
||||
balance["end_child"].append(i)
|
||||
|
||||
print("=== Counts ===")
|
||||
for k, v in balance.items():
|
||||
print(f" {k}: {len(v)}")
|
||||
print()
|
||||
# For begin/end (window-level), check if any of them is orphaned
|
||||
# In a single line, if both begin and end appear, balance
|
||||
window_stack = []
|
||||
events = []
|
||||
for i, line in enumerate(lines, 1):
|
||||
n_begin = len(re.findall(r"\bimgui\.begin\(", line))
|
||||
n_end = len(re.findall(r"\bimgui\.end\(", line))
|
||||
for j in range(n_begin):
|
||||
events.append((i, "begin"))
|
||||
for j in range(n_end):
|
||||
events.append((i, "end"))
|
||||
|
||||
for ev in events:
|
||||
if ev[1] == "begin":
|
||||
window_stack.append(ev)
|
||||
else:
|
||||
if window_stack:
|
||||
window_stack.pop()
|
||||
else:
|
||||
print(f"EXTRA end: L{ev[0]}")
|
||||
|
||||
print(f"\nLeftover begin_child lines: {len(balance['begin_child']) - len(balance['end_child'])}")
|
||||
print(f" begins: {len(balance['begin_child'])}, ends: {len(balance['end_child'])}")
|
||||
Reference in New Issue
Block a user