From f0b30e2097c3fb0bc9961ad5d3d9d60bf2a62ca6 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 20 Apr 2026 08:56:37 +1000 Subject: [PATCH] feat: network settings, update banner and changelog in settings page view_settings.py: - Network Access section: LAN vs localhost toggle, port input - _get_lan_ip: detects first 192.168.x.x address, ignores PPPoE/WAN - _write_server_config: writes [server] block to config.toml - _write_config: now preserves existing [server] block when saving theme - Custom hostname input with step-by-step hosts file instructions for Windows (C:\Windows\System32\drivers\etc\hosts) and macOS (/etc/hosts) - iOS/Android note directing to IP address or static DHCP reservation - _get_local_sha: reads .git/HEAD directly, no git CLI required - _get_remote_info: fetches latest commit SHA from GitHub API, 1hr cache - _get_recent_commits: fetches last 10 commits for changelog, 1hr cache - check_for_update_banner: call from app.py to show top-of-page info banner when local SHA differs from latest GitHub commit - Version and Changelog section in Settings: local vs remote SHA, update warning with git pull instructions, Check again button, last 10 commits as collapsible expanders with you-are-here marker --- .streamlit/config.toml | 4 + __pycache__/view_settings.cpython-314.pyc | Bin 15006 -> 32690 bytes view_settings.py | 336 ++++++++++++++++++++++ 3 files changed, 340 insertions(+) diff --git a/.streamlit/config.toml b/.streamlit/config.toml index e9e9e5e..76ca72a 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -5,3 +5,7 @@ backgroundColor = "#ffffff" secondaryBackgroundColor = "#f0f2f6" textColor = "#1a1a1a" font = "sans serif" + +[server] +address = "192.168.2.115" +port = 8501 diff --git a/__pycache__/view_settings.cpython-314.pyc b/__pycache__/view_settings.cpython-314.pyc index 340374ff284ee69bad3f603b2fbd48e1c3e788dc..37a8c0bd17e39302f307b16302991c801eaf454a 100644 GIT binary patch literal 32690 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 EKh(Kei~s-t delta 2129 zcmaJ?TWl0n7@jk??xkI}_ubO#wx!*cUQ#HR6bfx=X=!O;D81OrFx{QDL$^E2nOSHI z0YajUZ{i`*M5BfW6HG|d@@R~Zc!}{69*p!vV~mDGV+1T}VodNqXImdVoB7V!^Zn=c zop1hm?X_yj0M`H$A^=4*kFIESv)|9M$cG*!#eJepVYX?`tR%h3YbHZ9nq zYPrDW0hbS40dR%D6=k><(aqM1iv=xIEF^+%juwV?32z6q?a(ge?QL3FvET-^a==^- zRSVII^f+w7ciu8D=#J^SWJFvbk3#14dePS`k_O-0+Ga>{TQscJd%R>GwIo{{0cfunlgz)&wPVnfx@ z{@&xgr>JbF%%nRKRK)CvRB;#0WfK=dJBn~IR1N}&U^WeAa&Ty4#Fi#1Dt#;fyjH*~Tmoc_ zjoK>g=uC)>RzzCsI@M;BLSw>96$RwQTu&e<^py(21&|O|k0J6{DcCb6W(%4$CIQNA z^6t&KDulsuLGTLY!ZeeRm+9=SmZp$1(e08vCXXqv70Wf39jY2B z5Iv3~5Q!^UxbFf6Dq$r}C#v9P@@_*x2fOthl~A~LG)Lz#TX)a_ELSlBjVr>pvbs8W zb@00H&C##r2C~@LhJGS%H#Yn4)aP}}%3aytT}TF6hxhSYl4A19Ab=wSz(%j&5P8tnggQyVuJ%sm52~Iw5^2++l95Q@ z-OvN;u^|+>4zv9H=GlC@<>;}raLTp+WM{=QWbv%*cuqFS)iN#XZMDK z406}r*t_D1bTLwNT$&Ei_3!JF9jURew%7Syi>TEl7+?%aenVxurJe}`3B^ld5 z&gfMtPdh%cs#hN3$%lzgvmnqlS6nLh*+~}%SUAs0jF^Wmn8V_8uxPp4a7-+d2m0~g z;#O~lw@J^Lhw2H|IZXB$FTn2Z8c&1Ap#!G)FL>?y<@XWKl$alIKEpE~@s6m(_W%bl zu{BS<%XZRsQbx=K;us4WWH7`=m17MmNLH2$$*1x+wsoLiti2O{PS43|QEOmD~lkmhj?xqa@aha%Hq zJ|=f?D4aMozCQ@-?e$rByIz3T43WRvpEA^$|sdq3YlQRBT|=bzZ;z2D)VY>?K~@MNvD zUaPQnlYg?)8_lCxx*oS+AbC5(*z zTws<4xGek}HwFGx$Q;mfVA+nR6J{sA1wjZN=VO69e`!p7q>|>#(_#3Q{E?@i;P1f$ OsOiZi)GeZRKK@_Yj1imw diff --git a/view_settings.py b/view_settings.py index b5342cf..74f8860 100644 --- a/view_settings.py +++ b/view_settings.py @@ -10,6 +10,104 @@ import os, re CONFIG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".streamlit") CONFIG_FILE = os.path.join(CONFIG_DIR, "config.toml") +GITHUB_REPO = "alphapapi-ctrl/mt5-tools" +GITHUB_BRANCH = "master" + + +# ───────────────────────────────────────────────────────────────────────────── +# Version / update check +# ───────────────────────────────────────────────────────────────────────────── +def _get_local_sha() -> str: + """Return the current local git HEAD SHA, or '' if not a git repo.""" + try: + git_head = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".git", "HEAD") + if not os.path.isfile(git_head): + return "" + ref = open(git_head).read().strip() + if ref.startswith("ref:"): + # Resolve the ref to a SHA + ref_path = ref.split(" ", 1)[1] # e.g. refs/heads/master + sha_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), ".git", ref_path + ) + if os.path.isfile(sha_file): + return open(sha_file).read().strip() + else: + return ref # detached HEAD — ref is the SHA directly + except Exception: + pass + return "" + + +@st.cache_data(ttl=3600) # cache for 1 hour so we don't hammer the API +def _get_remote_info() -> dict: + """ + Fetch the latest commit SHA and message from GitHub API. + Returns dict with keys: sha, message, author, date, error + """ + import urllib.request, json + url = f"https://api.github.com/repos/{GITHUB_REPO}/commits/{GITHUB_BRANCH}" + try: + req = urllib.request.Request(url, headers={"User-Agent": "MT5Tools"}) + with urllib.request.urlopen(req, timeout=5) as r: + data = json.loads(r.read().decode()) + return { + "sha": data["sha"], + "short": data["sha"][:7], + "message": data["commit"]["message"].split("\n")[0], + "author": data["commit"]["author"]["name"], + "date": data["commit"]["author"]["date"][:10], + "error": None, + } + except Exception as e: + return {"sha": "", "short": "", "message": "", "author": "", "date": "", "error": str(e)} + + +@st.cache_data(ttl=3600) +def _get_recent_commits(n=10) -> list: + """Fetch the last n commits for the changelog.""" + import urllib.request, json + url = f"https://api.github.com/repos/{GITHUB_REPO}/commits?sha={GITHUB_BRANCH}&per_page={n}" + try: + req = urllib.request.Request(url, headers={"User-Agent": "MT5Tools"}) + with urllib.request.urlopen(req, timeout=5) as r: + data = json.loads(r.read().decode()) + return [ + { + "sha": c["sha"][:7], + "message": c["commit"]["message"], + "date": c["commit"]["author"]["date"][:10], + "author": c["commit"]["author"]["name"], + } + for c in data + ] + except Exception: + return [] + + +def check_for_update_banner(): + """ + Call from app.py on every page render. + Shows a top-of-page banner if a newer version is available on GitHub. + Silently does nothing if offline or not a git repo. + """ + local_sha = _get_local_sha() + if not local_sha: + return # not a git repo — skip silently + + remote = _get_remote_info() + if remote["error"] or not remote["sha"]: + return # offline or API error — skip silently + + if not remote["sha"].startswith(local_sha) and local_sha != remote["sha"]: + st.info( + f"🔄 **Update available** — " + f"latest commit `{remote['short']}` on {remote['date']}: " + f"*{remote['message']}* · " + f"Run `git pull` then restart to update.", + icon="🔄", + ) + THEMES = { "Dark": { "base": "dark", @@ -47,15 +145,77 @@ def _read_config() -> dict: return dict(THEMES["Dark"]) +def _get_lan_ip() -> str: + """Return the first 192.168.x.x address found on this machine, or '' if none.""" + import socket + try: + # Get all addresses for this hostname + hostname = socket.gethostname() + addrs = socket.getaddrinfo(hostname, None) + for addr in addrs: + ip = addr[4][0] + if ip.startswith("192.168."): + return ip + except Exception: + pass + # Fallback: connect to a dummy address to get the outbound LAN interface + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("192.168.1.1", 80)) + ip = s.getsockname()[0] + s.close() + if ip.startswith("192.168."): + return ip + except Exception: + pass + return "" + + +def _read_server_config() -> dict: + """Read [server] section from config.toml.""" + if not os.path.isfile(CONFIG_FILE): + return {} + try: + text = open(CONFIG_FILE).read() + found = {} + m = re.search(r'address\s*=\s*"([^"]*)"', text) + if m: found["address"] = m.group(1) + m = re.search(r'port\s*=\s*(\d+)', text) + if m: found["port"] = int(m.group(1)) + return found + except Exception: + return {} + + def _write_config(theme: dict): os.makedirs(CONFIG_DIR, exist_ok=True) + # Preserve existing [server] section if present + server_block = "" + if os.path.isfile(CONFIG_FILE): + existing = open(CONFIG_FILE).read() + m = re.search(r'\[server\].*?(?=\[|\Z)', existing, re.DOTALL) + if m: + server_block = "\n" + m.group(0).strip() + "\n" lines = ["[theme]\n"] for k, v in theme.items(): lines.append(f'{k} = "{v}"\n') + if server_block: + lines.append(server_block) with open(CONFIG_FILE, "w") as f: f.writelines(lines) +def _write_server_config(address: str, port: int): + """Write or update the [server] section in config.toml.""" + os.makedirs(CONFIG_DIR, exist_ok=True) + # Read existing content, strip any existing [server] block + existing = open(CONFIG_FILE).read() if os.path.isfile(CONFIG_FILE) else "" + existing = re.sub(r'\[server\].*?(?=\[|\Z)', '', existing, flags=re.DOTALL).strip() + server_block = f'\n\n[server]\naddress = "{address}"\nport = {port}\n' + with open(CONFIG_FILE, "w") as f: + f.write(existing + server_block) + + def _is_custom(cfg: dict) -> bool: """Return True if the saved config differs from both standard themes.""" for t in THEMES.values(): @@ -244,6 +404,182 @@ def render(): horizontal=True, key="st_font_size") inject_theme_css() + # ── Network ─────────────────────────────────────────────────────────────── + st.divider() + st.subheader("Network Access") + st.caption("Controls which network interface Streamlit binds to. " + "Restart the app after changing.") + + server_cfg = _read_server_config() + lan_ip = _get_lan_ip() + + nc1, nc2 = st.columns(2) + access_mode = nc1.radio( + "Access", + ["Localhost only", "Local network (LAN)"], + horizontal=True, + key="st_net_mode", + index=1 if server_cfg.get("address","") not in ("","127.0.0.1","localhost") else 0, + help="Localhost = this PC only. LAN = all devices on your 192.168.x.x network.", + ) + port = nc2.number_input( + "Port", min_value=1024, max_value=65535, + value=server_cfg.get("port", 8501), + step=1, key="st_net_port", + ) + + if access_mode == "Local network (LAN)": + if lan_ip: + st.info(f"Detected LAN address: **{lan_ip}** → " + f"other devices can reach the app at `http://{lan_ip}:{int(port)}`") + else: + st.warning("No 192.168.x.x address found — are you connected to your router?") + + # Hostname instructions + import socket as _socket, platform as _platform + hostname = _socket.gethostname().lower() + os_name = _platform.system() + lan_ip_str = lan_ip or "192.168.x.x" + + st.markdown("**Use a custom hostname instead of an IP address**") + st.caption( + "Add an entry to the hosts file on each device that needs to access the app. " + "No extra software required — works on Windows, macOS, iOS, and Android." + ) + + custom_name = st.text_input( + "Hostname to use", value="mt5tools", + key="st_custom_hostname", + help="e.g. mt5tools → http://mt5tools:8501" + ) + st.markdown( + f"Once set, devices on your LAN can access the app at: " + f"`http://{custom_name}:{int(port)}`" + ) + + if os_name == "Windows": + st.markdown(f""" +
+Windows — edit the hosts file on each client device + +1. Open **Notepad as Administrator** (right-click Notepad → Run as administrator) +2. Open the file: `C:\\Windows\\System32\\drivers\\etc\\hosts` +3. Add this line at the bottom: + +``` +{lan_ip_str} {custom_name} +``` + +4. Save the file +5. Open a browser and go to `http://{custom_name}:{int(port)}` + +**Note:** You need to do this on every Windows device that will access the app. +No restart required — the change takes effect immediately. +
+""", unsafe_allow_html=True) + + elif os_name == "Darwin": + st.markdown(f""" +
+macOS — edit /etc/hosts on each client device + +1. Open **Terminal** +2. Run: + +``` +sudo nano /etc/hosts +``` + +3. Add this line at the bottom: + +``` +{lan_ip_str} {custom_name} +``` + +4. Press `Ctrl+X`, then `Y`, then `Enter` to save +5. Flush the DNS cache: + +``` +sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder +``` + +6. Open a browser and go to `http://{custom_name}:{int(port)}` +
+""", unsafe_allow_html=True) + + st.markdown(f""" +
+iOS / Android — not supported via hosts file + +Mobile devices do not allow editing the hosts file without rooting/jailbreaking. +Use the IP address directly instead: + +`http://{lan_ip_str}:{int(port)}` + +Alternatively, set a **static DHCP reservation** on your router so the IP never +changes — then bookmark the IP address URL. +
+""", unsafe_allow_html=True) + + if st.button("Apply Network Settings", key="st_net_apply"): + if access_mode == "Localhost only": + _write_server_config("127.0.0.1", int(port)) + else: + addr = lan_ip if lan_ip else "192.168.1.1" + _write_server_config(addr, int(port)) + st.success("Saved to config.toml — restart the app for changes to take effect.") + + # ── Version & Changelog ─────────────────────────────────────────────────── + st.divider() + st.subheader("Version & Changelog") + + local_sha = _get_local_sha() + remote = _get_remote_info() + + vc1, vc2 = st.columns(2) + vc1.markdown( + f"**Local version:** `{local_sha[:7] if local_sha else 'unknown'}`" + if local_sha else "**Local version:** not a git repo" + ) + if remote["error"]: + vc2.markdown(f"**Remote:** unable to reach GitHub *(offline?)*") + else: + vc2.markdown(f"**Latest on GitHub:** `{remote['short']}` — {remote['date']}") + + if local_sha and not remote["error"]: + if remote["sha"].startswith(local_sha) or local_sha == remote["sha"]: + st.success("✅ You are on the latest version.") + else: + st.warning( + f"🔄 **Update available.** Latest: `{remote['short']}` — *{remote['message']}*\n\n" + f"Run the following to update:\n```\ngit pull\n```\nThen restart the app." + ) + + if st.button("🔁 Check again", key="st_ver_refresh"): + st.cache_data.clear() + st.rerun() + + st.markdown("##### Recent Changes") + commits = _get_recent_commits(10) + if commits: + for c in commits: + is_local = local_sha and c["sha"] == local_sha[:7] + lines = c["message"].strip().split("\n") + title = lines[0] + body = [l for l in lines[1:] if l.strip()] + marker = " ← **you are here**" if is_local else "" + with st.expander(f"`{c['sha']}` · {c['date']} · {title}{marker}"): + if body: + for l in body: + if l.strip().startswith("-"): + st.markdown(l.strip()) + elif l.strip(): + st.markdown(l.strip()) + else: + st.caption("No extended description.") + else: + st.caption("Changelog unavailable — check your internet connection.") + # ── About ───────────────────────────────────────────────────────────────── st.divider() st.subheader("About")