Add bachelor thesis work tracker system - core implementation

Co-authored-by: kuhyx <147418882+kuhyx@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-01-12 21:34:26 +00:00
parent e07be0a873
commit 02efc737a3
3 changed files with 800 additions and 0 deletions

View File

@ -0,0 +1,335 @@
#!/bin/bash
# Bachelor Thesis Work Tracker - One-Shot Installer
#
# This script installs a system that:
# 1. Monitors active windows for thesis-related work (Unreal Engine, Unity, Nvidia Omniverse, VS Code with specific repo)
# 2. Tracks accumulated work time with protection against tampering
# 3. Blocks Steam and other distractions via /etc/hosts until work quota is met
# 4. Provides psychological friction against circumvention
#
# The system is designed to be as hard to circumvent as possible:
# - State files are immutable (chattr +i)
# - Runs as a systemd service that auto-restarts
# - Integrated with hosts guard system
# - Protected against easy time manipulation
#
# Usage:
# sudo ./setup_thesis_work_tracker.sh [options]
#
# Options:
# --work-quota MINUTES Set required work time in minutes (default: 120 = 2 hours)
# --decay-rate MINUTES Set decay rate per hour of distraction usage (default: 30)
# --vscode-repo NAME Set required VS Code repository name (default: praca_magisterska)
# --dry-run Show what would be done without making changes
# --uninstall Remove the thesis work tracker system
# -h|--help Show this help
#
# Exit codes:
# 0 = success
# 1 = general failure
# 2 = argument error
set -euo pipefail
######################################################################
# Configuration Defaults
######################################################################
WORK_QUOTA_MINUTES=120 # 2 hours of work required
DECAY_RATE_MINUTES=30 # Lose 30 minutes per hour of Steam usage
VSCODE_REPO="praca_magisterska"
DRY_RUN=0
UNINSTALL=0
######################################################################
# Paths
######################################################################
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
TRACKER_SCRIPT="$SCRIPT_DIR/thesis_work_tracker.sh"
SERVICE_FILE="$SCRIPT_DIR/systemd/thesis-work-tracker@.service"
INSTALL_BIN="/usr/local/bin/thesis_work_tracker.sh"
INSTALL_SERVICE="/etc/systemd/system/thesis-work-tracker@.service"
STATE_DIR="/var/lib/thesis-work-tracker"
LOG_DIR="/var/log/thesis-work-tracker"
######################################################################
# Colors and Logging
######################################################################
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
msg() { printf "${GREEN}[+]${NC} %s\n" "$*"; }
note() { printf "${BLUE}[i]${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}[!]${NC} %s\n" "$*"; }
err() { printf "${RED}[x]${NC} %s\n" "$*" >&2; }
run() {
if [[ $DRY_RUN -eq 1 ]]; then
printf '%s[DRY-RUN]%s ' "${CYAN}" "${NC}"
printf '%q ' "$@"
printf '\n'
else
"$@"
fi
}
######################################################################
# Helpers
######################################################################
require_root() {
if [[ $EUID -ne 0 ]]; then
exec sudo -E bash "$0" "$@"
fi
}
usage() {
sed -n '2,/^set -euo pipefail/p' "$0" | sed 's/^# \{0,1\}//'
}
check_dependencies() {
local missing=()
for cmd in xdotool systemctl; do
if ! command -v "$cmd" &> /dev/null; then
missing+=("$cmd")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
err "Missing required dependencies: ${missing[*]}"
note "Install them with: sudo pacman -S ${missing[*]}"
return 1
fi
}
get_current_user() {
# Get the user who invoked sudo, or current user if not using sudo
if [[ -n ${SUDO_USER:-} ]]; then
echo "$SUDO_USER"
else
whoami
fi
}
######################################################################
# Parse Arguments
######################################################################
while [[ $# -gt 0 ]]; do
case "$1" in
--work-quota)
WORK_QUOTA_MINUTES="${2:-}"
[[ -z $WORK_QUOTA_MINUTES ]] && {
err "--work-quota requires a value"
exit 2
}
shift 2
;;
--decay-rate)
DECAY_RATE_MINUTES="${2:-}"
[[ -z $DECAY_RATE_MINUTES ]] && {
err "--decay-rate requires a value"
exit 2
}
shift 2
;;
--vscode-repo)
VSCODE_REPO="${2:-}"
[[ -z $VSCODE_REPO ]] && {
err "--vscode-repo requires a value"
exit 2
}
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--uninstall)
UNINSTALL=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "Unknown option: $1"
usage
exit 2
;;
esac
done
######################################################################
# Main Functions
######################################################################
uninstall_tracker() {
msg "Uninstalling thesis work tracker..."
# Get current user for service name
local user
user=$(get_current_user)
# Stop and disable service
if systemctl is-active --quiet "thesis-work-tracker@$user.service" 2>/dev/null; then
run systemctl stop "thesis-work-tracker@$user.service"
fi
if systemctl is-enabled --quiet "thesis-work-tracker@$user.service" 2>/dev/null; then
run systemctl disable "thesis-work-tracker@$user.service"
fi
# Remove service file
if [[ -f $INSTALL_SERVICE ]]; then
run rm -f "$INSTALL_SERVICE"
run systemctl daemon-reload
fi
# Remove tracker script
if [[ -f $INSTALL_BIN ]]; then
run rm -f "$INSTALL_BIN"
fi
# Remove state directory (with immutable flags removed)
if [[ -d $STATE_DIR ]]; then
run chattr -i -R "$STATE_DIR" 2>/dev/null || true
note "State directory preserved at: $STATE_DIR"
note "To completely remove state: sudo rm -rf $STATE_DIR"
fi
msg "Thesis work tracker uninstalled successfully"
note "Log files preserved at: $LOG_DIR"
}
install_tracker() {
msg "Installing thesis work tracker..."
# Check dependencies
check_dependencies || exit 1
# Verify source files exist
if [[ ! -f $TRACKER_SCRIPT ]]; then
err "Tracker script not found: $TRACKER_SCRIPT"
exit 1
fi
if [[ ! -f $SERVICE_FILE ]]; then
err "Service file not found: $SERVICE_FILE"
exit 1
fi
# Create directories
msg "Creating directories..."
run mkdir -p "$LOG_DIR"
run chmod 755 "$LOG_DIR"
# Install tracker script with configuration
msg "Installing tracker script to $INSTALL_BIN..."
# Copy script and update configuration values
run cp "$TRACKER_SCRIPT" "$INSTALL_BIN"
# Update configuration in the installed script
local work_quota_seconds=$((WORK_QUOTA_MINUTES * 60))
local decay_rate_seconds=$((DECAY_RATE_MINUTES * 60))
run sed -i "s/^WORK_QUOTA_REQUIRED=.*/WORK_QUOTA_REQUIRED=$work_quota_seconds # $WORK_QUOTA_MINUTES minutes/" "$INSTALL_BIN"
run sed -i "s/^WORK_DECAY_PER_HOUR=.*/WORK_DECAY_PER_HOUR=$decay_rate_seconds # $DECAY_RATE_MINUTES minutes/" "$INSTALL_BIN"
run sed -i "s/^VSCODE_REQUIRED_REPO=.*/VSCODE_REQUIRED_REPO=\"$VSCODE_REPO\"/" "$INSTALL_BIN"
run chmod 755 "$INSTALL_BIN"
# Install systemd service
msg "Installing systemd service..."
run cp "$SERVICE_FILE" "$INSTALL_SERVICE"
run chmod 644 "$INSTALL_SERVICE"
run systemctl daemon-reload
# Get current user for service enablement
local user
user=$(get_current_user)
# Enable and start service
msg "Enabling and starting service for user: $user..."
run systemctl enable "thesis-work-tracker@$user.service"
run systemctl restart "thesis-work-tracker@$user.service"
# Wait a moment for service to start
sleep 2
# Check service status
if systemctl is-active --quiet "thesis-work-tracker@$user.service"; then
msg "Service started successfully!"
else
warn "Service may not have started properly. Check status with:"
warn " systemctl status thesis-work-tracker@$user.service"
fi
# Display configuration summary
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ Bachelor Thesis Work Tracker - Installation ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
echo "Configuration:"
echo " • Work quota required: ${BOLD}${WORK_QUOTA_MINUTES} minutes${NC}"
echo " • Decay rate (per hour of Steam): ${BOLD}${DECAY_RATE_MINUTES} minutes${NC}"
echo " • VS Code repository: ${BOLD}${VSCODE_REPO}${NC}"
echo ""
echo "Tracked Applications:"
echo " ✓ Unreal Engine (all versions)"
echo " ✓ Unity Editor"
echo " ✓ Nvidia Omniverse"
echo " ✓ Visual Studio Code (only when working on '$VSCODE_REPO')"
echo ""
echo "Blocked Sites (until quota met):"
echo " ⛔ Steam (all domains)"
echo " ⛔ Social media (Reddit, Twitter, Facebook, Instagram)"
echo " ⛔ Video sites (YouTube, Twitch)"
echo " ⛔ Other distractions (9gag, Imgur)"
echo ""
echo "System Protection Features:"
echo " 🔒 State files protected with immutable flags"
echo " 🔒 Auto-restart on failure"
echo " 🔒 Integrated with hosts guard system"
echo " 🔒 Continuous monitoring every 5 seconds"
echo ""
echo "How it works:"
echo " 1. Work on your thesis using the approved applications"
echo " 2. Time accumulates in the background"
echo " 3. After ${WORK_QUOTA_MINUTES} minutes of work, Steam is unblocked"
echo " 4. Steam usage decays your work time at ${DECAY_RATE_MINUTES} min/hour"
echo " 5. When work time drops below quota, Steam is blocked again"
echo ""
echo "Useful Commands:"
echo " • Check status: systemctl status thesis-work-tracker@$user.service"
echo " • View logs: tail -f $LOG_DIR/tracker.log"
echo " • View state: sudo cat $STATE_DIR/work-time.state"
echo " • Restart: sudo systemctl restart thesis-work-tracker@$user.service"
echo " • Uninstall: sudo $0 --uninstall"
echo ""
echo "⚠️ IMPORTANT: This system is designed to be hard to circumvent!"
echo " State files are immutable and the service auto-restarts."
echo " To legitimately modify settings, uninstall and reinstall."
echo ""
echo "Good luck with your bachelor thesis! 🎓"
echo ""
}
######################################################################
# Main
######################################################################
require_root "$@"
if [[ $UNINSTALL -eq 1 ]]; then
uninstall_tracker
else
install_tracker
fi
exit 0

View File

@ -0,0 +1,29 @@
[Unit]
Description=Bachelor Thesis Work Tracker
Documentation=man:systemd.service(5)
After=graphical.target
Wants=graphical.target
[Service]
Type=simple
ExecStart=/usr/local/bin/thesis_work_tracker.sh
Restart=always
RestartSec=10
# Run as the user who is logged in (for X11/window detection)
User=%i
Environment="DISPLAY=:0"
Environment="XAUTHORITY=/home/%i/.Xauthority"
# Logging
StandardOutput=append:/var/log/thesis-work-tracker/tracker.log
StandardError=append:/var/log/thesis-work-tracker/tracker.log
# Security hardening
NoNewPrivileges=false
PrivateTmp=true
ProtectSystem=false
ProtectHome=false
[Install]
WantedBy=default.target

View File

@ -0,0 +1,436 @@
#!/bin/bash
# Bachelor Thesis Work Tracker
# Monitors active windows for thesis-related work (Unreal Engine, Unity, Nvidia Omniverse, VS Code with specific repo)
# Unlocks Steam and other distractions only after sufficient work time is accumulated
#
# This daemon runs continuously and:
# 1. Tracks active window time for approved thesis work applications
# 2. Maintains a protected state file with accumulated work time
# 3. Manages hosts file blocking/unblocking based on work quota
# 4. Provides psychological friction against circumvention
set -euo pipefail
# Configuration
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
STATE_DIR="/var/lib/thesis-work-tracker"
STATE_FILE="$STATE_DIR/work-time.state"
LOCK_FILE="$STATE_DIR/tracker.lock"
LOG_DIR="/var/log/thesis-work-tracker"
LOG_FILE="$LOG_DIR/tracker.log"
CHECK_INTERVAL=5 # Check every 5 seconds
# Work requirements (in seconds)
# 2 hours of work = 7200 seconds required before Steam access
WORK_QUOTA_REQUIRED=7200 # 2 hours
WORK_DECAY_PER_HOUR=1800 # Lose 30 minutes per hour of Steam usage
# Thesis work applications - process names and window patterns
# These are the applications that count as "thesis work"
declare -A THESIS_APPS=(
["UnrealEditor"]="Unreal Engine"
["UE4Editor"]="Unreal Engine 4"
["UE5Editor"]="Unreal Engine 5"
["Unity"]="Unity Editor"
["UnityHub"]="Unity Hub"
["Code"]="Visual Studio Code" # Special handling for repo check
["code"]="Visual Studio Code" # lowercase variant
["omniverse"]="Nvidia Omniverse"
["kit"]="Nvidia Omniverse Kit"
)
# VS Code specific repo to track
VSCODE_REQUIRED_REPO="praca_magisterska"
# Steam and distraction patterns for hosts blocking
STEAM_DOMAINS=(
"steampowered.com"
"steamcommunity.com"
"steamgames.com"
"store.steampowered.com"
"steamcdn-a.akamaihd.net"
"steamstatic.com"
"steamusercontent.com"
)
# Additional distraction sites that should be blocked
DISTRACTION_DOMAINS=(
"reddit.com"
"twitter.com"
"x.com"
"facebook.com"
"instagram.com"
"youtube.com"
"twitch.tv"
"9gag.com"
"imgur.com"
)
# Colors for logging
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Logging function
log_message() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${timestamp}] [${level}] ${message}" | tee -a "$LOG_FILE"
}
log_info() { log_message "INFO" "$@"; }
log_warn() { log_message "WARN" "$@"; }
log_error() { log_message "ERROR" "$@"; }
log_debug() {
if [[ ${DEBUG:-0} -eq 1 ]]; then
log_message "DEBUG" "$@"
fi
}
# Initialize directories and state file
init_state() {
# Create directories with proper permissions
if [[ ! -d $STATE_DIR ]]; then
sudo mkdir -p "$STATE_DIR"
sudo chmod 700 "$STATE_DIR"
fi
if [[ ! -d $LOG_DIR ]]; then
sudo mkdir -p "$LOG_DIR"
sudo chmod 755 "$LOG_DIR"
fi
# Initialize state file if it doesn't exist
if [[ ! -f $STATE_FILE ]]; then
cat <<EOF | sudo tee "$STATE_FILE" > /dev/null
# Thesis Work Tracker State File
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
# Last updated: $(date)
TOTAL_WORK_SECONDS=0
LAST_UPDATE_TIMESTAMP=$(date +%s)
STEAM_ACCESS_GRANTED=0
LAST_WORK_SESSION_START=0
CURRENT_SESSION_SECONDS=0
EOF
sudo chmod 600 "$STATE_FILE"
sudo chattr +i "$STATE_FILE" 2>/dev/null || true
fi
}
# Load current state from file
load_state() {
if [[ ! -f $STATE_FILE ]]; then
log_error "State file not found: $STATE_FILE"
return 1
fi
# Temporarily remove immutable flag to read
sudo chattr -i "$STATE_FILE" 2>/dev/null || true
# Source the state file
# shellcheck source=/dev/null
source "$STATE_FILE"
# Re-apply immutable flag
sudo chattr +i "$STATE_FILE" 2>/dev/null || true
}
# Save current state to file
save_state() {
local total_work="$1"
local steam_access="$2"
local current_session="$3"
local session_start="$4"
# Remove immutable flag
sudo chattr -i "$STATE_FILE" 2>/dev/null || true
# Write new state
cat <<EOF | sudo tee "$STATE_FILE" > /dev/null
# Thesis Work Tracker State File
# DO NOT EDIT MANUALLY - Managed by thesis_work_tracker daemon
# Last updated: $(date)
TOTAL_WORK_SECONDS=$total_work
LAST_UPDATE_TIMESTAMP=$(date +%s)
STEAM_ACCESS_GRANTED=$steam_access
LAST_WORK_SESSION_START=$session_start
CURRENT_SESSION_SECONDS=$current_session
EOF
sudo chmod 600 "$STATE_FILE"
# Re-apply immutable flag
sudo chattr +i "$STATE_FILE" 2>/dev/null || true
}
# Check if a process is running
is_process_running() {
local process_name="$1"
pgrep -x "$process_name" > /dev/null 2>&1
}
# Get active window title and process name
get_active_window_info() {
if ! command -v xdotool &> /dev/null; then
log_error "xdotool not installed, cannot detect active window"
return 1
fi
local active_window_id
active_window_id=$(xdotool getactivewindow 2>/dev/null || echo "")
if [[ -z $active_window_id ]]; then
return 1
fi
local window_name
window_name=$(xdotool getwindowname "$active_window_id" 2>/dev/null || echo "")
local window_pid
window_pid=$(xdotool getwindowpid "$active_window_id" 2>/dev/null || echo "")
local process_name=""
if [[ -n $window_pid ]]; then
process_name=$(ps -p "$window_pid" -o comm= 2>/dev/null || echo "")
fi
echo "${process_name}|${window_name}"
}
# Check if VS Code is working on the required repository
is_vscode_on_thesis_repo() {
local window_title="$1"
# VS Code window titles typically contain the folder/workspace name
# Look for the repo name in the window title
if [[ $window_title == *"$VSCODE_REQUIRED_REPO"* ]]; then
return 0
fi
# Also check if VS Code has the repo open by checking recent workspaces
# VS Code stores workspace info in ~/.config/Code/User/workspaceStorage
if [[ -d "$HOME/.config/Code/User/workspaceStorage" ]]; then
if find "$HOME/.config/Code/User/workspaceStorage" -type f -name "workspace.json" -exec grep -l "$VSCODE_REQUIRED_REPO" {} \; 2>/dev/null | grep -q .; then
# Additional check: is this window actually VS Code?
if [[ $window_title == *"Visual Studio Code"* ]]; then
return 0
fi
fi
fi
return 1
}
# Check if current active window is thesis work
is_thesis_work_active() {
local window_info
window_info=$(get_active_window_info)
if [[ -z $window_info ]]; then
return 1
fi
local process_name
local window_title
IFS='|' read -r process_name window_title <<< "$window_info"
log_debug "Active window: process='$process_name' title='$window_title'"
# Check each thesis application
for proc_pattern in "${!THESIS_APPS[@]}"; do
if [[ $process_name == *"$proc_pattern"* ]] || [[ $window_title == *"${THESIS_APPS[$proc_pattern]}"* ]]; then
# Special handling for VS Code - must be on thesis repo
if [[ $proc_pattern == "Code" ]] || [[ $proc_pattern == "code" ]]; then
if is_vscode_on_thesis_repo "$window_title"; then
log_debug "Thesis work detected: VS Code on $VSCODE_REQUIRED_REPO"
return 0
else
log_debug "VS Code detected but not on thesis repo"
continue
fi
fi
log_debug "Thesis work detected: ${THESIS_APPS[$proc_pattern]}"
return 0
fi
done
return 1
}
# Block Steam and distractions in /etc/hosts
block_distractions() {
log_info "Blocking Steam and distractions in /etc/hosts"
# Remove immutable flag temporarily
sudo chattr -i /etc/hosts 2>/dev/null || true
# Add blocking entries if not already present
local hosts_modified=0
for domain in "${STEAM_DOMAINS[@]}" "${DISTRACTION_DOMAINS[@]}"; do
if ! grep -q "^0.0.0.0[[:space:]]*$domain" /etc/hosts 2>/dev/null; then
echo "0.0.0.0 $domain" | sudo tee -a /etc/hosts > /dev/null
hosts_modified=1
fi
done
# Re-apply immutable flag
sudo chattr +i /etc/hosts 2>/dev/null || true
if [[ $hosts_modified -eq 1 ]]; then
log_info "Added distraction blocks to /etc/hosts"
fi
}
# Unblock Steam and distractions from /etc/hosts
unblock_distractions() {
log_info "Unblocking Steam and distractions in /etc/hosts"
# Remove immutable flag temporarily
sudo chattr -i /etc/hosts 2>/dev/null || true
# Remove blocking entries
local temp_hosts="/tmp/hosts.tmp.$$"
sudo cp /etc/hosts "$temp_hosts"
for domain in "${STEAM_DOMAINS[@]}" "${DISTRACTION_DOMAINS[@]}"; do
sudo sed -i "/^0.0.0.0[[:space:]]*$domain/d" "$temp_hosts"
done
sudo mv "$temp_hosts" /etc/hosts
sudo chmod 644 /etc/hosts
# Re-apply immutable flag
sudo chattr +i /etc/hosts 2>/dev/null || true
log_info "Removed distraction blocks from /etc/hosts"
}
# Check if Steam is currently running (to track decay)
is_steam_running() {
pgrep -x "steam" > /dev/null 2>&1
}
# Main tracking loop
main_loop() {
log_info "Starting thesis work tracker daemon"
# Initialize state
init_state
# Load initial state
load_state
local total_work_seconds=${TOTAL_WORK_SECONDS:-0}
local steam_access=${STEAM_ACCESS_GRANTED:-0}
local session_start=${LAST_WORK_SESSION_START:-0}
local session_seconds=${CURRENT_SESSION_SECONDS:-0}
# Apply initial blocking state
if [[ $steam_access -eq 0 ]]; then
block_distractions
fi
local last_status_log=$(date +%s)
local last_decay_check=$(date +%s)
while true; do
local current_time=$(date +%s)
# Check if thesis work is active
if is_thesis_work_active; then
# Track work time
if [[ $session_start -eq 0 ]]; then
session_start=$current_time
log_info "Thesis work session started"
fi
# Increment session time
session_seconds=$((session_seconds + CHECK_INTERVAL))
total_work_seconds=$((total_work_seconds + CHECK_INTERVAL))
# Check if we've reached the quota
if [[ $total_work_seconds -ge $WORK_QUOTA_REQUIRED ]] && [[ $steam_access -eq 0 ]]; then
log_info "Work quota reached! Granting Steam access."
steam_access=1
unblock_distractions
fi
else
# No thesis work active
if [[ $session_start -ne 0 ]]; then
log_info "Thesis work session ended. Session duration: $((session_seconds / 60)) minutes"
session_start=0
session_seconds=0
fi
# Check for Steam usage and apply decay
if [[ $steam_access -eq 1 ]] && is_steam_running; then
local time_since_decay=$((current_time - last_decay_check))
if [[ $time_since_decay -ge 3600 ]]; then # Every hour
total_work_seconds=$((total_work_seconds - WORK_DECAY_PER_HOUR))
if [[ $total_work_seconds -lt 0 ]]; then
total_work_seconds=0
fi
last_decay_check=$current_time
log_info "Steam usage detected. Applied decay. Remaining work time: $((total_work_seconds / 60)) minutes"
# Revoke access if below quota
if [[ $total_work_seconds -lt $WORK_QUOTA_REQUIRED ]]; then
log_info "Work quota depleted. Revoking Steam access."
steam_access=0
block_distractions
fi
fi
fi
fi
# Save state periodically
save_state "$total_work_seconds" "$steam_access" "$session_seconds" "$session_start"
# Log status every 5 minutes
if [[ $((current_time - last_status_log)) -ge 300 ]]; then
local work_minutes=$((total_work_seconds / 60))
local quota_minutes=$((WORK_QUOTA_REQUIRED / 60))
local remaining_minutes=$((quota_minutes - work_minutes))
if [[ $remaining_minutes -lt 0 ]]; then
remaining_minutes=0
fi
log_info "Status: Total work time: ${work_minutes}m / ${quota_minutes}m | Steam access: $steam_access | Need: ${remaining_minutes}m more"
last_status_log=$current_time
fi
sleep "$CHECK_INTERVAL"
done
}
# Handle signals for graceful shutdown
cleanup() {
log_info "Received shutdown signal, saving state and exiting"
rm -f "$LOCK_FILE"
exit 0
}
trap cleanup SIGTERM SIGINT
# Check for lock file to prevent multiple instances
if [[ -f $LOCK_FILE ]]; then
log_error "Another instance is already running (lock file exists: $LOCK_FILE)"
exit 1
fi
# Create lock file
touch "$LOCK_FILE"
# Run main loop
main_loop