This guide provides step-by-step instructions for deploying the Medical Automation Suite, which includes the Medical Phrase Client and the Snippet Expander.
Looking for the streamlined production bundle? The
clean_suite/directory contains only the files that are actively used in the current deployment (server script, SQL data, and Windows clients). Copy that folder to start from a minimal, distraction-free snapshot of the project.
To keep the deployment resilient, follow these six guardrails:
- Isolate the services. Run them under a dedicated Unix account with no shell access and ownership restricted to the project directory and database files.
- Pin & patch dependencies. Python requirements are now version-pinned. Schedule a recurring (e.g., monthly) reminder to review
pip list --outdatedand update the pinned versions after testing. - Add telemetry. Both services write structured logs to
logs/medical_automation.logby default (override viaMEDICAL_AUTOMATION_LOG_DIR). Review this log for crashes or suspicious traffic. - Terminate HTTPS at the edge. Place the HTTP services behind an HTTPS reverse proxy (e.g., Nginx, Caddy, or Traefik) and redirect all plain HTTP traffic to HTTPS. This prevents credentials and medical content from traversing the network in cleartext.
- Threat-model the simplest abuse. The snippet management API now requires an explicit opt-in (
SNIPPET_ADMIN_ENABLED=1) and a strongSNIPPET_ADMIN_TOKENheader to prevent drive-by edits. - Minimize scope. Keep only the endpoints you need. Leave the snippet admin API disabled unless you are actively managing snippets, and keep the Flask development server bound to
127.0.0.1when reverse-proxying.
This project uses two separate files to manage dependencies for the server and the client. All dependencies are pinned to known-good versions so deployments are reproducible:
server_requirements.txt: Contains packages needed only for the server (e.g.,Flask).requirements.txt: Contains packages needed for the client applications on Windows (e.g.,pynput,pywebview).
Monthly patch routine: With your virtual environment activated, run
pip list --outdatedon both the server and client machines. Test upgrades in a staging environment, then update the pinned versions in these files so you can deploy safely.
The server component runs on a Raspberry Pi or any Linux machine on your network. It hosts the REST APIs for both the medical phrases and the text snippets.
- A Raspberry Pi with Raspberry Pi OS (or any Debian-based Linux).
- Python 3 installed.
- Network connectivity between the server and client machines.
To isolate the automation services, create a system user with no interactive shell and a locked-down home. The remainder of the guide assumes the files live in /opt/medical_automation and are owned by this user.
sudo adduser --system --group --home /opt/medical_automation --shell /usr/sbin/nologin medauto
sudo mkdir -p /opt/medical_automation
sudo chown medauto:medauto /opt/medical_automationCopy the necessary server files to the Raspberry Pi, then move them into /opt/medical_automation so they are owned by the medauto account. This includes the server script, requirements, and the SQL data files.
# Example using scp from your local machine (copy to a staging folder)
scp server.py server_requirements.txt SQL2.sql snippets_data.sql pi@<raspberry_pi_ip>:/tmp/medical_automation/
# On the Raspberry Pi
ssh pi@<raspberry_pi_ip>
sudo rsync -av /tmp/medical_automation/ /opt/medical_automation/
sudo chown -R medauto:medauto /opt/medical_automationUsing a virtual environment is highly recommended to manage dependencies.
-
Connect to your Raspberry Pi (e.g., via SSH), elevate to the
medautoaccount, and navigate to the project directory:sudo -u medauto -H bash cd /opt/medical_automation -
Create a virtual environment:
python3 -m venv venv
This creates a
venvfolder in your project directory owned bymedauto. -
Activate the virtual environment:
source venv/bin/activateYour terminal prompt should now be prefixed with
(venv).
With the virtual environment active, install the required packages from server_requirements.txt.
pip install -r server_requirements.txtThe server uses two separate databases.
-
Medical Phrases Database (
automation.db): This database is created and populated automatically fromSQL2.sqlthe first time the server runs. No manual action is needed for this part, as long asSQL2.sqlwas copied to the server. -
Snippets Database (
snippets.db): This database contains the text expansion snippets. After the server runs for the first time (which creates the empty database file), you must populate it using thesnippets_data.sqlscript.Run the following command on your Raspberry Pi:
sudo -u medauto sqlite3 /opt/medical_automation/snippets.db < /opt/medical_automation/snippets_data.sqlThis command will delete any existing snippets and populate the table with a fresh set from your SQL file.
To ensure the unified server runs automatically on boot, we will create a systemd service.
-
Create a new service file:
sudo nano /etc/systemd/system/medical-server.service
-
Add the following content. Note that
ExecStartnow points to theserver.pyscript.[Unit] Description=Medical Automation Unified Server After=network.target [Service] Type=simple User=medauto Group=medauto WorkingDirectory=/opt/medical_automation EnvironmentFile=/etc/medical_automation.env ExecStart=/opt/medical_automation/venv/bin/python /opt/medical_automation/server.py Restart=always NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes ReadWritePaths=/opt/medical_automation /var/log/medical_automation [Install] WantedBy=multi-user.target
-
Create an environment file to store secrets and runtime settings referenced above:
sudo install -o root -g root -m 640 /dev/null /etc/medical_automation.env sudo nano /etc/medical_automation.env
Recommended contents:
# Disable snippet admin endpoints unless you explicitly opt in SNIPPET_ADMIN_ENABLED=0 # Generate a strong random token if/when you enable admin access SNIPPET_ADMIN_TOKEN= # Optional: store logs outside the app directory MEDICAL_AUTOMATION_LOG_DIR=/var/log/medical_automation
If you set
MEDICAL_AUTOMATION_LOG_DIR, create the directory and give ownership tomedauto:sudo mkdir -p /var/log/medical_automation sudo chown medauto:medauto /var/log/medical_automation
- Reload the systemd daemon to recognize the new service:
sudo systemctl daemon-reload
- Enable the service to start on boot:
sudo systemctl enable medical-server.service - Start the service immediately:
sudo systemctl start medical-server.service
- Check the status to ensure it's running without errors:
sudo systemctl status medical-server.service
Running the Flask development server directly on the network leaves traffic unencrypted. Terminate TLS in front of the service so clients always connect over HTTPS:
- Install a reverse proxy (e.g., Nginx) and obtain certificates via Let's Encrypt (
certbot) or another trusted CA. - Bind Flask only to the loopback interface (
127.0.0.1) and expose port 443 on the proxy. Example Nginx server block:server { listen 443 ssl http2; server_name medical-automation.example.com; ssl_certificate /etc/letsencrypt/live/medical-automation.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/medical-automation.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; } } server { listen 80; server_name medical-automation.example.com; return 301 https://$host$request_uri; }
- Reload Nginx and confirm you can reach the service at
https://medical-automation.example.comfrom client machines. - If you enable the snippet admin API, require the same HTTPS endpoint so the admin token is never sent in plaintext.
- Application logs: By default both services log to
logs/medical_automation.loginside the project directory. To inspect recent activity:sudo -u medauto tail -f /opt/medical_automation/logs/medical_automation.log
- Systemd journal: For aggregated service output, continue using
journalctl -u medical-server.service. - Crash investigations: Look for lines tagged
ERRORorWARNING—they will include stack traces or the client's IP when admin API requests fail.
- The
/snippetsPOST/PUT/DELETE endpoints are disabled by default. To enable them temporarily, edit/etc/medical_automation.envand set:Restart the service afterwards.SNIPPET_ADMIN_ENABLED=1 SNIPPET_ADMIN_TOKEN=replace-with-a-long-random-string
- Clients must send the token in the
X-Admin-Tokenheader. Example withcurl:curl -X POST http://127.0.0.1:5000/snippets \ -H "Content-Type: application/json" \ -H "X-Admin-Token: $SNIPPET_ADMIN_TOKEN" \ -d '{"abbreviation": "bp", "phrase": "Blood pressure"}'
- When administration tasks are complete, reset
SNIPPET_ADMIN_ENABLED=0and restart the service to reduce the exposed attack surface.
The client applications run on your primary Windows machine.
- Windows 10 or 11.
- Python 3 installed.
- Copy all project files to a folder on your Windows machine.
- Open a Command Prompt or PowerShell and navigate to the project directory.
- Create a virtual environment:
python -m venv venv - Activate the virtual environment:
Your terminal prompt should now be prefixed with
.\venv\Scripts\activate
(venv).
With the virtual environment active, install all required Python packages using requirements.txt:
pip install -r requirements.txtNote: This will install requests, pynput, pyperclip, pyautogui, and pywebview.
Before running the client applications, you must configure them to point to your server's IP address.
- Find your Raspberry Pi's IP address (e.g.,
192.168.1.100). - Open the following files in a text editor:
ClienteWindowssnippet_expander.pysnippet_manager_gui.py
- In each file, find the URL variables and replace
pi.localwith your server's IP address.pi_url = "http://192.168.1.100:8080"SERVER_URL = "http://192.168.1.100:5000"MEDICAL_SERVER_URL = "http://192.168.1.100:8080"
Make sure your virtual environment is active before running the applications.
- Medical Phrase Client:
python ClienteWindows
- Snippet Manager GUI:
python snippet_manager_gui.py
- Snippet Expander (requires Administrator rights):
- Open Command Prompt as Administrator.
- Navigate to the project directory and activate the virtual environment (
.\venv\Scripts\activate). - Run the script:
python snippet_expander.py
For easier daily use, you can package the Python scripts into standalone .exe files using PyInstaller.
-
Install PyInstaller (in your activated virtual environment):
pip install pyinstaller
-
Create the Executables: Run these commands from your project directory.
-
For GUI Applications (this hides the console window):
pyinstaller --onefile --windowed ClienteWindows pyinstaller --onefile --windowed snippet_manager_gui.py
-
For the Background Snippet Expander:
pyinstaller --onefile snippet_expander.py
-
-
Find the Executables: The
.exefiles will be located in thedistfolder inside your project directory. You can create shortcuts to these files on your Desktop or Start Menu for easy access. Remember to run thesnippet_expander.exeas Administrator.
If you see an error like ConnectionRefusedError or target machine actively refused it, it means the server is not responding. Follow these steps on your Raspberry Pi to diagnose the issue.
Use systemctl to check the status of the service you created.
sudo systemctl status snippet-server.service- If the status is
active (running), the server is running. Proceed to Step 2. - If the status is
inactive (dead)orfailed, the service is not running. Try restarting it:If it fails again, check the logs for errors:sudo systemctl restart snippet-server.service
This will show the last 50 log entries, which usually contain the reason for the failure.journalctl -u snippet-server.service -n 50 --no-pager
The server must listen on 0.0.0.0 to be accessible from other devices, not 127.0.0.1 (localhost).
-
Use the
netstatcommand to see which ports are open:sudo netstat -tulpn | grep 5000 -
Analyze the output:
- Correct:
tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN ...This means the server is correctly listening on all network interfaces. - Incorrect:
tcp 0 0 127.0.0.1:5000 0.0.0.0:* LISTEN ...This means the server is only listening for local connections. Ensureserver.pycontainsapp.run(host='0.0.0.0', port=5000). - No Output: If you see no output, the server application is not running on port 5000. Go back to Step 1.
- Correct:
If the server is running and listening correctly, a firewall might be blocking the connection.
-
Check the status of the Uncomplicated Firewall (
ufw):sudo ufw status
-
If the status is
active, you must explicitly allow traffic on port 5000:sudo ufw allow 5000/tcp sudo ufw reload
This command opens the port for incoming TCP connections.
You may encounter a situation where systemctl status shows the service is active (running), but netstat -tulpn shows nothing listening on the expected port. This usually means the Python script started but failed internally before the web server could launch.
To diagnose this, you need to view the detailed logs from your service.
Use the journalctl command to see the full output from your service, including any Python error messages.
# View the last 100 log entries for the service
journalctl -u snippet-server.service -n 100 --no-pagerLook for a Traceback (Python error) in the output. This will tell you exactly what went wrong inside the server.py script.
-
Database Errors: The script might be failing to connect to or initialize the
snippets.dbdatabase. Ensure thesnippets.dbfile and the directory containing it are owned by themedautouser and have the correct permissions.# To fix permissions in your project directory sudo chown -R medauto:medauto /opt/medical_automation -
Syntax Errors: A simple typo in
server.pycan cause it to fail on startup. Thejournalctllog will show the exact line number of the error. -
Dependency Issues: If a required package (like
Flask) was not installed correctly in the virtual environment, the script will fail. The log will show anImportError.
After fixing the issue found in the logs, restart the service to apply the changes:
sudo systemctl restart snippet-server.serviceIf the journalctl log shows an error like can't open file ... [Errno 2] No such file or directory, it means the systemd service cannot find your Python script.
First, confirm the exact name and location of your server script on the Raspberry Pi.
-
Connect to your Raspberry Pi and navigate to the project directory:
cd /opt/medical_automation -
List the files in the directory:
ls -l
-
Look for your server script in the output. Is it named
server.py,ServidorCode2, or something else? Make sure it's actually there.
Next, ensure the ExecStart path in your service file matches the actual path and filename from Step 1.
-
Display the contents of your service file:
cat /etc/systemd/system/snippet-server.service
-
Check the
ExecStartline. For example:ExecStart=/opt/medical_automation/venv/bin/python /opt/medical_automation/server.py -
If the path or filename is incorrect, edit the service file:
sudo nano /etc/systemd/system/snippet-server.service
Correct the filename to match what you found in Step 1.
After correcting the service file, you must reload systemd and restart the service.
sudo systemctl daemon-reload
sudo systemctl restart snippet-server.service
sudo systemctl status snippet-server.serviceThis should resolve the "No such file or directory" error.
If you see an error like OSError: [Errno 98] Address already in use when starting a server, it means another process is already using the port (e.g., 5000 or 8080). This often happens if a previous server instance did not shut down correctly.
Use the lsof (list open files) command to find the Process ID (PID) that is occupying the port. Replace <port_number> with the port from the error message (e.g., 5000).
# Example for port 5000
sudo lsof -i :5000This command will show you a list of processes. Look for the one with a state of LISTEN and note its PID.
Once you have the PID, you can stop the process using the kill command.
# Replace <PID> with the process ID you found
kill <PID>If the process does not stop, you can force it to stop with kill -9:
kill -9 <PID>If you configured the server to run as a systemd service, that service might be the one holding the port. In that case, you should manage it with systemctl.
# Stop the service if it's running
sudo systemctl stop snippet-server.service
# Then try running your script manually for testing
source venv/bin/activate
python server.pyAfter stopping the conflicting process, you should be able to start your server script without the "Address already in use" error.