Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

cosmaticdev/MedalIntel

Repository files navigation

HEADS UP: REPO IS NOW ARCHIVED

Around March 2026, Medal fixed the issue that this project leveraged. You are now only able to track the public releases, and as such development and internal environments are now actually hidden. You can still see flag / user metadata changes and you can track the public environments correctly, at least as of 3/23/2026, but that is subject to change.

I will be taking down my personal server hosting this project and will be leaving this project as an archive if anyone wishes to use it still. Documentation suggesting that my personal server is online can be ignored as it is no longer true. If you want historical data you can shoot me a message and I'll help if I can.

README starts:

After I saw that accessing internal environments for Medal.tv were not authenticated I decided to spin up a tool to track changes in them. This works similarly to a git commit tracker, albeit with no comments or individual commits, only each release. Being able to see changes in testing environments is usually enough to track new features as they are being developed, though, so feel free to poke around and see what you can find!

This project is currently live at medalintel.cosmatic.dev if you would like to try it out.

Issues? DM Me on discord @cosmatic_ or open an issue on GitHub. Note that I am limited, so if you have the time to make a fix yourself, I would be more than happy to accept a PR.

Commit history prior to 2/2/2026 is not available for privacy reasons as passwords, auth tokens, and other secrets were stored in the git history and so I migrated the project to a new repo. Commit messages, timestamps, and authors are still available but content has been scrubbed.

Tracks the following Medal data:

  • Environment Updates: Whenever a new release for an environment is pushed, including public and internal environments
  • New Environments: Whenever the team spins up a new testing / development environment
  • Feature Flags: Changes in flag versions, and new flags
  • User Profile Metadata: New fields loaded in the profile api
  • User Capabilities: New fields loaded in the capabilities api

Installation

1. Prerequisites

  • Python 3.10+ - Install Python
    • I HIGHLY recommend using the latest version of CPython, PyPy has not been tested
  • Rust (for building the core) - Install Rust
  • The .NET SDK - Download from Microsoft
  • ilspycmd (decompiling .NET binaries) (ilspycmd --version)
    • Download with .NET CLI: dotnet tool install --global ilspycmd
  • Git (for version tracking on dashboard) - Install Git
    • Verify installation: git --version
    • Git version will show as Unknown if not installed
  • fast-cli (for network bandwidth monitoring) (fast --version)
    • fast-cli on npm npm install --global fast-cli
    • App works without it, dashboard just won't display bandwidth
  • Docker if you wish to run in a container (not recommended) - Download Docker
  • A Cloudflare account and connected website if you plan to host on a domain
    • If you are planning to run the server through the Python script, install Cloudflared (Cloudflare Tunneling) with winget install Cloudflare.cloudflared

2. Setup

Clone the repo and navigate into it:

git clone https://github.com/cosmaticdev/MedalIntel.git
cd MedalIntel

Install the python dependencies:

pip install -r requirements.txt

3. Environment Variables

You need to set up your secrets. Copy the example file:

copy .env.example .env

Open .env and fill it out:

  • MEDAL_XAUTH: (If you want capability or profile metadata tracking to work) You need to grab this from a network request on medal.tv (look for the x-authentication header).
  • MEDAL_LD_JWT: (Optional, for feature flag tracking) LaunchDarkly JWT token. To obtain this:
    1. Open Chrome DevTools (Control+Shift+C) and go to the Network tab
    2. Jump over to medal.tv while logged in
    3. Search the network requests tab for launchdarkly
    4. Find the request to https://app.launchdarkly.com/sdk/evalx/...
    5. Copy the JWT from the URL path (everything after /contexts/, starting with ey)
  • SECRET_KEY: Required. A long random string. Generate using python -c 'import secrets; print(secrets.token_hex(32))'
  • CLOUDFLARE_TUNNEL_TOKEN: Optional. Required if you want to access the dashboard on your domain

4. Cloud Backups (optional)

If you do not plan to use cloud backups, you can skip this step.

Planning to host via Python:

The Python project is built with the idea that any cloud storage you may have is mounted as a drive. Theoretically, this should make it easy to use a NAS, Google Drive, or any other cloud storage service. If you need to use storage system that is not mounted you will need to make modifications to the code yourself to get it to work (feel free to send patches if you want to support a different storage system). I recommend using either a NAS or Google Drive (mounted via the Google Drive desktop app) if you plan on using cloud backups.

If you are using Google Drive your route will look something like this: G:\My Drive\MedalIntel_Backups. Make sure the Google Drive app is in stream mode and not mirror mode.

Planning to host via Docker:

Docker has difficulties mounting a remote drive as a volume, so you will need to use a different service to get it working. I found that rclone works well for this purpose. The image and plugin are installed automatically when you use the docker setup script (explained later). You will need to set up rclone and create a remote for your cloud storage. Rclone has a really good guide for setting up, so feel free to just follow it.

Feel free to follow any guide, but here is a quick one for Google Drive on Windows:

winget install rclone
rclone config

From there, follow the prompts:

  • n (New remote)
  • name>: gdrive
  • Storage>: drive (Google Drive)
  • client_id>: Press Enter (leave blank)
  • client_secret>: Press Enter (leave blank)
  • scope>: Choose 1 (Full access)
  • service_account_file>: Press Enter (leave blank)
  • Edit advanced config?: n (No)
  • Use web browser to authenticate?: y (Yes) Your browser will open. Log in to Google.
  • Configure this as a team drive?: n (No)
  • y (Yes, keep this remote)
  • q (Quit)

From there you should be ready to go. Make sure that you have sufficient storage and that you set your config to export diffs to the cloud.

5. Configuration (config.json)

There are lots of settings that you might want to change, but be cautious as changing these values can really slam your initial startup time, or general performance if you choose them wrong.

  • I recommend only focusing on the following for the first run or initial tests:
  • performance/max_workers - I have this at a default of -2. This will utilize almost your entire CPU while making sure you have enough overhead to actually still use the machine
  • performance/module_check_workers - Easiest to have this setting mirror max_workers, but that's up to you
  • performance/diff_pre_check_workers - Same logic as above applies
  • storage/cloud_root - This project is set up assuming your cloud storage is mounted as a drive. Give the path to the drive and to the backup folder within the drive in this field. If you aren't using cloud backups, ignore this field.
  • environment_filter/enabled_environments - If you want to only track a couple environments, this is the field for you.
  • environment_filter/max_age_days - There's a ton of years-old junk environments, not a bad idea to ignore everything older than a couple weeks old.
  • cloud/enabled - Just let the system know if you intend to use cloud backups or not
  • cloud/max_local_diffs - How many diffs you want to cache locally. I just leave this at 0 and offload everything to the cloud, but that's up to you
  • cloud/max_diffs_per_environment - If you just want to cache one or two of the newest diffs and offload the rest (overridden by max_local_diffs)
  • server/enabled - If you want to host the web viewer or if you just want to be grabbing the changes yourself
  • cloudflare/enabled - If you plan to host on a domain via Cloudflare Tunneling

Depending on how you decide to configure your setup your first run can get incredibly long. Specifically, if your max_age_days is set to a high number or is unlimited, it will go through over a hundred old environments, or if enable_full_history_diff is true, it will go through multiple versions of every environment that it's tracking. If you do a initial run of every historically available environment with full history diffs you can expect several hours (or even days depending on your system and worker count) for a first run to finish. If your first run only tracks environments from the past month or so and only looks at the last three historical versions of each environment, it should only take a hour or so. On my 9800x3d it takes ~45s-2m+ per environment while running workers at -2 (all but 2 cores). If you have a weaker system, expect much longer intitial runs. Heavily CPU bound, but you still need a decent amount of free ram, 5-10gigs of excess should be plenty.

Here's a brief rundown on every option you can find in config.json:

api

  • check_interval_minutes: How often the script checks for environment/flag/metadata changes (default: 5)
  • api_endpoints: Enpoint urls to connect to the Medal api (defaults probably don't need to be changed)
  • max_download_retries: How many times to retry downloading a file (default: 3)
  • retry_delay_seconds: How long to wait between retries (default: 2)

performance

  • max_workers: How many worker threads to use. Positive integer for a fixed number, negative for "all but x", zero to utilize all available cores (default: -2)
  • cache_ttl_seconds: How long to cache API responses (default: 60)
  • download_chunk_size: How many bytes to download at a time (default: 4194304)
  • module_check_workers: How many worker threads to use for module checking. Same worker logic as max_workers (default: -2)
  • diff_pre_check_workers: How many worker threads to use for diff pre-checking. Same worker logic as max_workers (default: -2)

storage

  • local_saves: Where to save local files (default: "Saves")
  • local_versions: Where to save local versions (default: "Saves/Versions")
  • cloud_root: Where to save cloud files. This will probably need to be changed (default: "G:\My Drive\MedalIntel_Backups")

environment_filter

  • enabled_environments: List of environments to track. Leave empty to track all environments (default: [])
  • max_age_days: Maximum age of environments to track. Older environments are ignored. -1 means no limit (default: 21)

cloud

  • enabled: Whether to enable cloud backup features (default: true)
  • max_local_diffs: Maximum number of local diffs to keep. -1 means unlimited (default: 0)
  • max_diffs_per_environment: Maximum number of diffs per environment to keep. -1 means unlimited (default: 0)
  • verify_uploads: Whether to verify uploads to cloud (default: true)
  • verify_downloads: Whether to verify downloads from cloud (default: true)

security

  • rate_limits
    • global_per_day: Maximum number of requests per day (default: 10000)
    • global_per_hour: Maximum number of requests per hour (default: 500)
    • cloud_diff_per_minute: Maximum number of cloud diff requests per minute (default: 10)
  • cors
    • use_dynamic_origin: Whether to use dynamic origin (default: true)
    • custom_origins: List of custom origins accepted by CORS (non-localhost) (default: [])
  • input_validation
    • max_param_length: Maximum length of parameters in requests (default: 100)
    • allowed_chars_pattern: Regex pattern for allowed characters in requests (default: "^[a-zA-Z0-9._-]+$")

server

  • enabled: Whether to enable the server (default: true)
  • port: Port to run the server on (default: 5000)
  • host: Host to run the server on (default: "0.0.0.0")
  • debug: Whether to enable debug mode (default: false)
  • use_reloader: Whether to use the flask reloader (default: false)

cloudflare

  • enabled: Whether to enable Cloudflared tunneling (default: true)

dashboard

  • max_feed_items: Maximum number of feed items to render (default: 50)
  • code_stats_enabled: Whether to enable repository code statistics (default: true)

startup

  • scanner_restart_delay: Delay in seconds before restarting the scanner (default: 1.0)
  • server_restart_delay: Delay in seconds before restarting the server (default: 1.0)
  • error_restart_delay: Delay in seconds before restarting on error (default: 5.0)

traffic

  • enabled: Whether to enable traffic data collection (default: true)
  • flush_interval_seconds: How often to flush traffic data (default: 5.0)

timeouts

  • ilspycmd_seconds: Timeout in seconds for ilspycmd (default: 180)
  • download_seconds: Timeout in seconds for downloads (default: 300)
  • powershell_seconds: Timeout in seconds for PowerShell (default: 20)

rust

  • file_comparison_buffer_size: Buffer size in bytes for file comparison (default: 65536)
  • minification_line_threshold: Line threshold for minification detection (default: 500)
  • text_extensions: List of file extensions to treat as text files (default: [....])
  • media_extensions: List of file extensions to treat as media files (default: [....])
  • scan_exclude_patterns: List of patterns to exclude from scanning (default: [....])

diffing

  • enable_full_history_diff: Whether to enable full history diff (default: true)
  • max_versions_to_process: Maximum number of versions to process from an environment (default: 5)

theme

  • accent_color: Accent color for the theme (default: "#5e6ad2")
  • background_color: Background color for the theme (default: "#0d0d0d")
  • card_background: Card background color for the theme (default: "#161616")
  • enable_animations: Whether to enable animations (default: true)
  • font_family: Font family for the theme (default: "Inter")

Here are some cool themes to try:

Midnight Blue (Default)

"theme": {
    "accent_color": "#5e6ad2",
    "background_color": "#0d0d0d",
    "card_background": "#161616"
}

Emerald Dark

"theme": {
    "accent_color": "#10b981",
    "background_color": "#022c22",
    "card_background": "#064e3b"
}

5.5. Importing History

Environment and Flag history are blank by default when cloning this project. This will mean that you are missing a historical log of environments and of last-updated dates for flags. If you would like, there is a mirror of my history files (accurate back to 1/28/2026) that you can download and import to overwrite your blank History and FlagHistory files to kickstart yourself. You can also get this info from people running mirrors of this project and who have the api/serve_saves config option set to true.

These files are available on my servers at medalintel.cosmatic.dev/saves/history.json and medalintel.cosmatic.dev/saves/flagshistory.json

6. Setting up Cloudflare Tunnels (if hosting, otherwise skip)

If you want to view your dashboard from anywhere without exposing your IP or opening ports, you can use a Cloudflare Tunnel. If you want to host your site in another way, you will need to configure that yourself.

  1. Go to the Cloudflare Zero Trust Dashboard.
  2. Click Networks -> Manage Tunnels -> Create New Cloudflared Tunnel.
  3. Name it whatever, then choose your OS.
  4. Copy the token part of the install command (it's the long string right at the end of the command).
  5. Paste that token into your .env file as CLOUDFLARE_TUNNEL_TOKEN.
  6. Set the public subdomain, select your domain from the dropdown, and add a path if you really want to
  7. Under service, set the type to HTTP and the URL to localhost:<port> where <port> is the port your server is running on (default: 5000).
  8. Click Complete Setup.

If you are planning to run the server through the Python script, install Cloudflared (Cloudflare Tunneling) with winget install Cloudflare.cloudflared or check your installation with cloudflared --version

7. Running the server

The server setup you choose here must be the same as your decision as step 4 when you chose your cloud storage setup.

Via Python:

Running via python is the most bare-bones way to run the server. Deploying via python will best utilize the CPU is and most optimized on RAM usage. Unlike Docker, you will not have the option to easily run the server on startup; you will need to either run it manually or set up a task scheduler to run it on startup, or something similar.

python startup.py

Via Docker:

Running via docker is not recommended. Docker uses far more RAM, and has poorer multi-core performance compared to running via python. Keep that in mind when making a decision on where to deploy. The server will still be accessible from the cloudflared tunnel and back up to the cloud. Running via either of these deploy scripts will automatically create the docker composition files for the container and deploy them automatically using the existing config.json without more modifications. Windows:

./deploy.bat

Linux / macOS:

chmod +x deploy.sh
./deploy.sh

Credits

  • Medal.tv: For the environments and metadata that we are tracking.
  • Maturin: For letting us use Rust builds in our Python.
  • Flask: For being the backbone of the server
  • SocketIO: For handling real-time updates and running the flask server
  • Cloudflare: For handling tunneling to the internet (for free!)
  • And many other open source libraries referenced in requirements.txt

If you would like to contact me for any reason at all, I am available nearly every day on discord @cosmatic_ or via email at cosmatic@cael.me.


This is a personal project and is not affiliated with Medal.tv. This could break at any time as Medal makes back-end changes. If Medal tells you to stop using this, stop using this.

About

Listening for Medal environment and flag updates, logging what changed and when.

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors