This commit is contained in:
Tomas Dvorak
2026-01-26 08:13:18 +01:00
parent aa036b6550
commit dfc079288f
505 changed files with 95755 additions and 5712 deletions
+479
View File
@@ -0,0 +1,479 @@
#!/usr/bin/env python3
"""
Browser-based Terminal Tamagotchi
Terminal interface in browser with real-time updates
"""
import os
import time
import random
import subprocess
from datetime import datetime
from flask import Flask, render_template, jsonify
from flask_socketio import SocketIO, emit
app = Flask(__name__)
app.config['SECRET_KEY'] = 'terminal-tamagotchi'
socketio = SocketIO(app, cors_allowed_origins="*")
class BrowserTerminalTamagotchi:
def __init__(self, project_name="MyClub"):
self.project_name = project_name
self.stats_file = os.path.join(os.path.dirname(__file__), 'stats.md')
self.stats = {}
self.last_check = time.time()
self.typing_detected = False
self.recent_changes = 0
# Pet state
self.pet = {
'name': project_name.lower() + '-chi',
'mood': 'content',
'evolution': 'baby',
'level': 1,
'energy': 100,
'hunger': 0,
'coding': 0,
'face': '(o.o)',
'activity': 'idle'
}
# ASCII faces
self.faces = {
'baby': {
'content': ['(o.o)', '(O.o)', '(o.O)'],
'coding': ['(>.<)', '(>_<)', '(>.>_)'],
'happy': ['(^.^)', '(^.^)', '(^o^)'],
'tired': ['(-_-)', '(=_=)', '(x_x)']
},
'child': {
'content': ['(o.o)', '(^.^)', '(•.•)'],
'coding': ['(>_<)', '(>o<)', '(x.x)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
},
'teen': {
'content': ['(o.o)', '(^.^)', '(•_•)'],
'coding': ['(>_<)', '(x_x)', '(>.<)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
},
'adult': {
'content': ['(o.o)', '(^.^)', '(•_•)'],
'coding': ['(>_<)', '(x_x)', '(>.<)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
},
'master': {
'content': ['(o.o)', '(^.^)', '(•_•)'],
'coding': ['(>_<)', '(x_x)', '(>.<)'],
'happy': ['(^.^)', '(◕‿◕)', '(´•ᴗ•`)'],
'tired': ['(-_-)', '(¬_¬)', '(=_=)']
}
}
def get_git_status(self):
"""Get git repository status"""
try:
project_root = os.path.dirname(os.path.dirname(self.stats_file))
# Check if we're in a git repository
git_dir = os.path.join(project_root, '.git')
if not os.path.exists(git_dir):
return {'status': 'no_repo', 'branch': None, 'changes': []}
# Try different git status commands to get all files
commands = [
['git', 'status', '--porcelain', '--ignored=traditional'],
['git', 'status', '--porcelain', '--ignored'],
['git', 'status', '--porcelain']
]
result = None
for cmd in commands:
try:
result = subprocess.run(
cmd,
cwd=project_root,
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
break
except:
continue
if not result or result.returncode != 0:
return {'status': 'error', 'branch': None, 'changes': []}
# Get current branch
branch_result = subprocess.run(
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
cwd=project_root,
capture_output=True,
text=True,
timeout=5
)
current_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else 'unknown'
# Parse git status output
changes = []
for line in result.stdout.strip().split('\n'):
if line and len(line) >= 3:
status_code = line[:2]
file_path = line[3:]
if status_code == '??':
change_type = 'untracked'
elif status_code == '!!':
change_type = 'ignored'
elif status_code == ' M':
change_type = 'modified'
elif status_code == 'A':
change_type = 'added'
elif status_code == 'D':
change_type = 'deleted'
elif status_code[0] in ['M', 'A', 'D']:
change_type = 'staged'
else:
change_type = 'changed'
changes.append({
'file': file_path,
'type': change_type,
'status': status_code
})
# Debug: print what we found
print(f"Git status found {len(changes)} changes")
return {
'status': 'clean' if not changes else 'dirty',
'branch': current_branch,
'changes': changes,
'total_changes': len(changes)
}
except Exception as e:
print(f"Git status error: {e}") # Debug output
return {'status': 'error', 'branch': None, 'changes': []}
def scan_project_files(self):
"""Scan project files - using exact same logic as project_stats.py"""
try:
project_root = os.path.dirname(os.path.dirname(self.stats_file))
current_time = time.time()
# Use exact same constants as project_stats.py + tamagotchi_env
IGNORED_DIRS = {
'.git',
'.git_backup',
'__pycache__',
'node_modules',
'dist',
'build',
'.next',
'.cache',
'.venv',
'venv',
'tamagotchi_env', # Exclude tamagotchi virtual environment
}
ALLOWED_EXTS = {
'.tsx',
'.css',
'.go',
'.ts',
'.js',
'.html',
'.sql',
'.py',
}
file_stats = {
'directories': 0,
'files': 0,
'lines': 0,
'extensions': {},
'recent_changes': 0,
'current_file': None, # Track most recently modified file
'recent_files': [] # List of recently modified files
}
for current_dir, dirs, files in os.walk(project_root):
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
# Only count directories that are NOT the root directory (same as project_stats.py)
if current_dir != project_root:
file_stats['directories'] += 1
for name in files:
ext = os.path.splitext(name)[1]
if ext not in ALLOWED_EXTS:
continue
file_stats['files'] += 1
path = os.path.join(current_dir, name)
ext_key = ext or '<no_ext>'
line_count = 0
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for _ in f:
line_count += 1
except Exception:
line_count = 0
file_stats['lines'] += line_count
if ext_key not in file_stats['extensions']:
file_stats['extensions'][ext_key] = {'files': 0, 'lines': 0}
file_stats['extensions'][ext_key]['files'] += 1
file_stats['extensions'][ext_key]['lines'] += line_count
# Check for recent changes (last 30 seconds)
try:
mtime = os.path.getmtime(path)
if current_time - mtime < 30:
file_stats['recent_changes'] += 1
# Track the most recently modified file
rel_path = os.path.relpath(path, project_root)
file_info = {
'path': rel_path,
'name': name,
'ext': ext,
'mtime': mtime,
'size': os.path.getsize(path)
}
# Update current file (most recent)
if not file_stats['current_file'] or mtime > file_stats['current_file']['mtime']:
file_stats['current_file'] = file_info
# Add to recent files list (keep last 5)
file_stats['recent_files'].append(file_info)
file_stats['recent_files'] = sorted(
file_stats['recent_files'],
key=lambda x: x['mtime'],
reverse=True
)[:5]
# Debug output
print(f"Recent change detected: {rel_path} ({ext})")
except:
pass
return file_stats
except Exception as e:
return {'directories': 0, 'files': 0, 'lines': 0, 'extensions': {}, 'recent_changes': 0}
def update_pet_state(self, file_stats):
"""Update pet based on file stats"""
self.recent_changes = file_stats['recent_changes']
self.typing_detected = self.recent_changes > 0
lines = file_stats['lines']
if lines > 200000:
self.pet['evolution'] = 'master'
elif lines > 100000:
self.pet['evolution'] = 'adult'
elif lines > 50000:
self.pet['evolution'] = 'teen'
elif lines > 10000:
self.pet['evolution'] = 'child'
else:
self.pet['evolution'] = 'baby'
self.pet['level'] = 1 + (file_stats['files'] // 50) + (lines // 10000)
if self.typing_detected:
self.pet['mood'] = 'coding'
self.pet['activity'] = 'coding'
self.pet['coding'] = min(100, self.pet['coding'] + self.recent_changes * 10)
self.pet['energy'] = min(100, self.pet['energy'] + 5)
else:
self.pet['activity'] = 'idle'
self.pet['coding'] = max(0, self.pet['coding'] - 2)
if self.pet['energy'] > 70:
self.pet['mood'] = 'content'
elif self.pet['energy'] > 40:
self.pet['mood'] = 'tired'
else:
self.pet['mood'] = 'tired'
self.pet['hunger'] = min(100, self.pet['hunger'] + 1)
self.pet['energy'] = max(0, self.pet['energy'] - 1)
evolution_faces = self.faces.get(self.pet['evolution'], self.faces['baby'])
mood_faces = evolution_faces.get(self.pet['mood'], evolution_faces['content'])
self.pet['face'] = random.choice(mood_faces)
def get_terminal_output(self):
"""Generate ultra-compact terminal-style output"""
lines = []
# Header
lines.append(f"[{datetime.now().strftime('%H:%M:%S')}] {self.pet['name'].upper()} TERMINAL")
lines.append("=" * 50)
# Pet and status on one line
lines.append(f"[{self.pet['face']}] {self.pet['mood'].upper()} | LVL:{self.pet['level']} | {self.pet['evolution'].upper()}")
# Status bars on one line
energy_bar = "#" * int(self.pet['energy']//10) + " " * (10 - int(self.pet['energy']//10))
hunger_bar = "#" * int(self.pet['hunger']//10) + " " * (10 - int(self.pet['hunger']//10))
coding_bar = "#" * int(self.pet['coding']//10) + " " * (10 - int(self.pet['coding']//10))
lines.append(f"E:[{energy_bar}] H:[{hunger_bar}] C:[{coding_bar}]")
# Current activity (compact)
if self.typing_detected and self.stats.get('current_file'):
current_file = self.stats['current_file']
file_name = current_file['path'].split('/')[-1]
lines.append(f"EDITING: {file_name} ({current_file['ext']})")
elif self.typing_detected:
lines.append(f"ACTIVITY: {self.recent_changes} files changed")
else:
lines.append(f"Activity: {self.pet['activity'].upper()}")
# Git status (compact)
git_status = self.get_git_status()
if git_status['status'] == 'no_repo':
lines.append("GIT: Not a git repository")
elif git_status['status'] == 'error':
lines.append("GIT: Error checking git")
elif git_status['status'] == 'clean':
lines.append(f"GIT: Clean ({git_status['branch']})")
else:
untracked = len([c for c in git_status['changes'] if c['type'] == 'untracked'])
modified = len([c for c in git_status['changes'] if c['type'] == 'modified'])
staged = len([c for c in git_status['changes'] if c['type'] == 'staged'])
deleted = len([c for c in git_status['changes'] if c['type'] == 'deleted'])
ignored = len([c for c in git_status['changes'] if c['type'] == 'ignored'])
lines.append(f"GIT: {git_status['total_changes']} files ({git_status['branch']})")
lines.append(f" U:{untracked} M:{modified} S:{staged} D:{deleted} I:{ignored}")
# Recent files (compact)
if self.stats.get('recent_files'):
recent_names = [f['path'].split('/')[-1] for f in self.stats['recent_files'][:2]]
lines.append(f"RECENT: {', '.join(recent_names)}")
# Project stats (compact)
lines.append(f"PROJECT: {self.stats.get('files', 0)} files | {self.stats.get('lines', 0):,} lines")
# Top languages (compact)
extensions = self.stats.get('extensions', {})
if extensions:
sorted_exts = sorted(extensions.items(), key=lambda x: x[1]['lines'], reverse=True)[:3]
top_langs = [f"{ext}({data['files']})" for ext, data in sorted_exts]
lines.append(f"LANGUAGES: {', '.join(top_langs)}")
lines.append("CONTROLS: [f]eed [p]lay [r]efresh [q]uit")
lines.append("=" * 50)
return "\n".join(lines)
def feed_pet(self):
"""Feed the pet"""
self.pet['hunger'] = max(0, self.pet['hunger'] - 30)
self.pet['energy'] = min(100, self.pet['energy'] + 10)
self.pet['mood'] = 'content'
def play_with_pet(self):
"""Play with pet"""
self.pet['energy'] = max(0, self.pet['energy'] - 10)
self.pet['mood'] = 'content'
self.pet['activity'] = 'playing'
# Global instance
tamagotchi = None
@app.route('/')
def index():
"""Main terminal page"""
return render_template('terminal.html')
@app.route('/api/terminal-data')
def get_terminal_data():
"""Get terminal output as JSON"""
return jsonify({
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
@socketio.on('connect')
def handle_connect():
"""Handle client connection"""
emit('terminal_update', {
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
@socketio.on('command')
def handle_command(data):
"""Handle terminal commands"""
cmd = data.get('command', '').lower().strip()
if cmd == 'f':
tamagotchi.feed_pet()
print(f"Feed command received - Hunger: {tamagotchi.pet['hunger']}%")
elif cmd == 'p':
tamagotchi.play_with_pet()
print(f"Play command received - Energy: {tamagotchi.pet['energy']}%")
elif cmd == 'r':
print("Refresh command received - stats refresh automatically")
elif cmd == 'q':
emit('quit', {'message': 'Terminal session ended'})
return
# Send updated state
emit('terminal_update', {
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
def background_monitor():
"""Background thread to monitor files and broadcast updates"""
while True:
try:
# Scan files and update pet
file_stats = tamagotchi.scan_project_files()
tamagotchi.stats = file_stats
tamagotchi.update_pet_state(file_stats)
# Broadcast update to all clients
socketio.emit('terminal_update', {
'output': tamagotchi.get_terminal_output(),
'typing_detected': tamagotchi.typing_detected,
'recent_changes': tamagotchi.recent_changes
})
except Exception as e:
print(f"Monitor error: {e}")
time.sleep(2) # Update every 2 seconds
if __name__ == '__main__':
# Initialize tamagotchi
project_name = "MyClub"
tamagotchi = BrowserTerminalTamagotchi(project_name)
# Start background monitoring
import threading
monitor_thread = threading.Thread(target=background_monitor, daemon=True)
monitor_thread.start()
print("🖥️ Browser Terminal Tamagotchi Starting...")
print("🌐 Open your browser to: http://localhost:5000")
print("⌨️ Terminal interface in browser!")
print("🔥 Real-time file monitoring!")
socketio.run(app, host='0.0.0.0', port=5000, debug=False)
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
Form Testing Script for PHP Application
Tests form submission with valid data every 20 seconds
"""
import requests
import time
import random
from datetime import datetime, timedelta
class FormTester:
def __init__(self, base_url="http://192.168.2.38"):
self.base_url = base_url
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
})
def generate_test_data(self):
"""Generate realistic test data for the form"""
# Extended list of company names with various Czech company types
company_prefixes = [
"Česká", "Slovenská", "Evropská", "Mezinárodní", "Regionální", "Místní",
"Nová", "Moderní", "Tradiční", "První", "Jednotná", "Spojená"
]
company_main = [
"Technologická", "Průmyslová", "Obchodní", "Stavební", "Dopravní",
"Energetická", "Zemědělská", "Potravinářská", "Textilní", "Kovovýroba",
"Softwarová", "Marketingová", "Finanční", "Logistická", "Vzdělávací"
]
company_suffixes = [
"s.r.o.", "a.s.", "o.s.", "v.o.s.", "k.s.", "Ltd.", "Inc.", "GmbH",
"Společnost", "Firma", "Podnik", "Závod", "Družstvo", "Holding"
]
# Extended list of Czech cities and towns
cities = [
"Praha", "Brno", "Ostrava", "Plzeň", "Liberec", "Olomouc", "Budějovice",
"Hradec Králové", "Ústí nad Labem", "Pardubice", "Karlovy Vary", "Jihlava",
"Zlín", "Mladá Boleslav", "Kladno", "Most", "Opava", "Frýdek-Místek",
"Teplice", "Chomutov", "Jablonec nad Nisou", "Mělník", "Prostějov",
"Přerov", "Trutnov", "Tábor", "Kolín", "Kroměříž", "Havířov", "České Budějovice",
"Děčín", "Krnov", "Šumperk", "Orlová", "Litvínov", "Vsetín", "Valašské Meziříčí",
"Česká Lípa", "Beroun", "Žďár nad Sázavou", "Klatovy", "Rumburk", "Vrchlabí"
]
# Generate random company name with structure
prefix = random.choice(company_prefixes)
main = random.choice(company_main)
suffix = random.choice(company_suffixes)
# Sometimes add additional descriptors
if random.random() < 0.3: # 30% chance to add extra descriptor
descriptors = ["Group", "International", "Central", "Premium", "Plus", "Max"]
company_name = f"{prefix} {main} {random.choice(descriptors)} {suffix}"
else:
company_name = f"{prefix} {main} {suffix}"
# Sometimes add a number
if random.random() < 0.2: # 20% chance to add number
company_name += f" {random.randint(1, 500)}"
# Generate random city
city = random.choice(cities)
# Generate random founding date (between 1950 and 2024)
start_year = 1950
end_year = 2024
year = random.randint(start_year, end_year)
month = random.randint(1, 12)
# Generate valid day for the month
if month in [4, 6, 9, 11]:
day = random.randint(1, 30)
elif month == 2:
# Handle leap years
if (year % 400 == 0) or (year % 100 != 0 and year % 4 == 0):
day = random.randint(1, 29)
else:
day = random.randint(1, 28)
else:
day = random.randint(1, 31)
founding_date = f"{year:04d}-{month:02d}-{day:02d}"
return {
'nazev': company_name,
'mesto': city,
'datum_zalozeni': founding_date,
'odeslat': 'Uložit do databáze'
}
def submit_form(self, form_data):
"""Submit the form with provided data"""
try:
response = self.session.post(
f"{self.base_url}/zapisfirem.php",
data=form_data,
timeout=30
)
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Form submitted:")
print(f" Name: {form_data['nazev']}")
print(f" City: {form_data['mesto']}")
print(f" Date: {form_data['datum_zalozeni']}")
print(f" Status Code: {response.status_code}")
print(f" Response Length: {len(response.text)} characters")
# Check for success message
if "Firma byla úspěšně vložena do databáze" in response.text:
print(" ✅ Success: Company inserted into database")
elif "Firma byla spn vloena do databze" in response.text: # Handle encoding issues
print(" ✅ Success: Company inserted into database (encoding detected)")
else:
print(" ⚠️ Unknown response - check if submission was successful")
print("-" * 60)
return response.status_code == 200
except requests.exceptions.RequestException as e:
print(f"❌ Error submitting form: {e}")
return False
def run_test(self, interval_seconds=20, max_iterations=None):
"""Run the form testing loop"""
print(f"🧪 Starting form testing...")
print(f"📡 Target: {self.base_url}/zapisfirem.php")
print(f"⏰ Interval: {interval_seconds} seconds")
print(f"🔄 Max iterations: {'Unlimited' if max_iterations is None else max_iterations}")
print("=" * 60)
iteration = 0
try:
while True:
if max_iterations and iteration >= max_iterations:
print(f"🏁 Reached maximum iterations ({max_iterations})")
break
iteration += 1
print(f"📝 Iteration {iteration}")
# Generate and submit test data
form_data = self.generate_test_data()
success = self.submit_form(form_data)
if not success:
print("⚠️ Submission failed, but continuing...")
# Wait for the specified interval
if max_iterations is None or iteration < max_iterations:
print(f"⏳ Waiting {interval_seconds} seconds...")
time.sleep(interval_seconds)
except KeyboardInterrupt:
print(f"\n🛑 Testing stopped by user after {iteration} iterations")
except Exception as e:
print(f"❌ Unexpected error: {e}")
print("🏁 Form testing completed")
def main():
"""Main function to run the tester"""
print("PHP Form Testing Script")
print("======================")
print("This script will submit form data every 0.5 seconds to test your PHP application.")
print("Press Ctrl+C to stop the testing at any time.")
print()
# Create and run the tester
tester = FormTester()
# Run with 0.5-second interval for faster testing (you can change this)
tester.run_test(interval_seconds=0.001)
if __name__ == "__main__":
main()
+200
View File
@@ -0,0 +1,200 @@
import argparse
import json
from pathlib import Path
from typing import List, Dict
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
MATCH_FIELDS: List[Dict[str, str]] = [
{
"name": "competition_code",
"label": "Kód soutěže",
"help": "Např. A1A. Použije se, pokud není vyplněno competition_external_id.",
},
{
"name": "competition_external_id",
"label": "External ID soutěže (UUID)",
"help": "UUID soutěže z odkazu na fotbal.cz. Má přednost před competition_code.",
},
{"name": "round", "label": "Kolo", "help": "Např. 2. kolo, 10. kolo atd."},
{
"name": "is_home",
"label": "Domácí / venku",
"help": "Hodnota home/away (nebo 1/0, true/false, yes/no).",
},
{"name": "opponent_name", "label": "Soupeř", "help": "Název soupeře."},
{
"name": "opponent_club_link",
"label": "Odkaz na klub soupeře",
"help": "URL na klub soupeře z fotbal.cz z URL se přečte UUID pro loga.",
},
{
"name": "external_match_id",
"label": "External ID zápasu (UUID)",
"help": "UUID zápasu; pokud není, vezme se UUID z match_link.",
},
{
"name": "kickoff_date",
"label": "Datum výkopu (YYYY-MM-DD)",
"help": "Datum zápasu, např. 2025-03-15.",
},
{
"name": "kickoff_time",
"label": "Čas výkopu (HH:MM)",
"help": "Čas zápasu, např. 17:00.",
},
{
"name": "score_fulltime",
"label": "Konečný stav",
"help": "Např. 2:1. Může být prázdné pro budoucí zápasy.",
},
{
"name": "score_halftime",
"label": "Poločasu",
"help": "Např. 1:0. Volitelné.",
},
{
"name": "match_link",
"label": "Odkaz na zápas",
"help": "URL na detail zápasu na is.fotbal.cz.",
},
{"name": "venue", "label": "Hřiště", "help": "Název hřiště / stadionu."},
{"name": "note", "label": "Poznámka", "help": "Libovolná poznámka k zápasu."},
]
TABLE_FIELDS: List[Dict[str, str]] = [
{
"name": "competition_code",
"label": "Kód soutěže",
"help": "Např. A1A. Použije se, pokud není vyplněno competition_external_id.",
},
{
"name": "competition_external_id",
"label": "External ID soutěže (UUID)",
"help": "UUID soutěže z odkazu na fotbal.cz. Má přednost před competition_code.",
},
{"name": "rank", "label": "Pořadí", "help": "Pozice v tabulce, např. 1."},
{"name": "team_name", "label": "Tým", "help": "Název týmu."},
{
"name": "team_club_link",
"label": "Odkaz na klub týmu",
"help": "URL na klub z fotbal.cz z URL se přečte UUID pro loga.",
},
{"name": "played", "label": "Zápasy", "help": "Počet odehraných zápasů."},
{"name": "wins", "label": "Výhry", "help": "Počet výher."},
{"name": "draws", "label": "Remízy", "help": "Počet remíz."},
{"name": "losses", "label": "Prohry", "help": "Počet proher."},
{
"name": "score",
"label": "Skóre",
"help": "Souhrnné skóre ve formátu góly:inkasované, např. 45:17.",
},
{"name": "points", "label": "Body", "help": "Počet bodů v tabulce."},
]
def _auto_width(sheet):
for column_cells in sheet.columns:
max_length = 0
column = column_cells[0].column
for cell in column_cells:
value = str(cell.value) if cell.value is not None else ""
if len(value) > max_length:
max_length = len(value)
adjusted = max(max_length + 2, 12)
sheet.column_dimensions[get_column_letter(column)].width = adjusted
def generate_workbook(output_dir: Path) -> None:
wb = Workbook()
ws_matches = wb.active
ws_matches.title = "matches"
ws_matches.append([f["name"] for f in MATCH_FIELDS])
for cell in ws_matches[1]:
cell.font = Font(bold=True)
_auto_width(ws_matches)
ws_tables = wb.create_sheet(title="tables")
ws_tables.append([f["name"] for f in TABLE_FIELDS])
for cell in ws_tables[1]:
cell.font = Font(bold=True)
_auto_width(ws_tables)
path = output_dir / "manual_facr_templates.xlsx"
wb.save(path)
def generate_forms_spec(output_dir: Path) -> None:
spec = {
"matches_form": {
"title": "Manuální zápasy (FAČR)",
"description": "Formulář pro ruční zadání zápasů ve struktuře odpovídající CSV/XLSX/JSON importu.",
"fields": [
{
"name": f["name"],
"label": f["label"],
"help_text": f["help"],
"type": "text",
"required": f["name"]
in {"competition_code", "competition_external_id", "is_home", "opponent_name"},
}
for f in MATCH_FIELDS
],
},
"tables_form": {
"title": "Manuální tabulky (FAČR)",
"description": "Formulář pro ruční zadání tabulek soutěží ve struktuře odpovídající CSV/XLSX/JSON importu.",
"fields": [
{
"name": f["name"],
"label": f["label"],
"help_text": f["help"],
"type": "text",
"required": f["name"]
in {"competition_code", "competition_external_id", "rank", "team_name"},
}
for f in TABLE_FIELDS
],
},
}
path = output_dir / "manual_facr_google_forms.json"
path.write_text(json.dumps(spec, ensure_ascii=False, indent=2), encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(
description=(
"Generate manual FACR Excel templates (matches/tables) and a Google Forms "
"field specification from a single schema."
)
)
parser.add_argument(
"--output-dir",
type=str,
default=None,
help=(
"Output directory for generated files. "
"Defaults to <project_root>/static/manual-facr."
),
)
args = parser.parse_args()
if args.output_dir:
output_dir = Path(args.output_dir)
else:
output_dir = Path(__file__).resolve().parents[1] / "static" / "manual-facr"
output_dir.mkdir(parents=True, exist_ok=True)
generate_workbook(output_dir)
generate_forms_spec(output_dir)
if __name__ == "__main__":
main()
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
import os
import argparse
from collections import defaultdict
from datetime import datetime
IGNORED_DIRS = {
'.git',
'.git_backup',
'__pycache__',
'node_modules',
'dist',
'build',
'.next',
'.cache',
'.venv',
'venv',
}
ALLOWED_EXTS = {
'.tsx',
'.css',
'.go',
'.ts',
'.js',
'.html',
'.sql',
'.py',
}
def walk_project(root: str):
for current_dir, dirs, files in os.walk(root):
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
yield current_dir, dirs, files
def count_stats(root: str):
total_dirs = 0
total_files = 0
total_lines = 0
by_ext = defaultdict(lambda: [0, 0]) # ext -> [file_count, line_count]
for current_dir, dirs, files in walk_project(root):
if current_dir != root:
total_dirs += 1
for name in files:
ext = os.path.splitext(name)[1]
if ext not in ALLOWED_EXTS:
continue
total_files += 1
path = os.path.join(current_dir, name)
ext_key = ext or '<no_ext>'
line_count = 0
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for _ in f:
line_count += 1
except Exception:
line_count = 0
total_lines += line_count
by_ext[ext_key][0] += 1
by_ext[ext_key][1] += line_count
return total_dirs, total_files, total_lines, by_ext
def print_tree(root: str, max_depth: int | None = None):
tree_output = []
root = os.path.abspath(root)
for current_dir, dirs, files in walk_project(root):
rel = os.path.relpath(current_dir, root)
if rel == '.':
depth = 0
name = os.path.basename(root.rstrip(os.sep)) or root
else:
depth = rel.count(os.sep) + 1
name = os.path.basename(current_dir)
if max_depth is not None and depth > max_depth:
dirs[:] = []
continue
indent = ' ' * depth
tree_output.append(f"{indent}{name}/")
file_indent = ' ' * (depth + 1)
for filename in sorted(files):
tree_output.append(f"{file_indent}{filename}")
return tree_output
def main():
parser = argparse.ArgumentParser(description='Project statistics: files, folders, lines, and structure.')
parser.add_argument('path', nargs='?', default='.', help='Root path (default: current directory)')
parser.add_argument('--max-tree-depth', type=int, default=3, help='Max depth for printed tree (default: 3)')
parser.add_argument('--output-md', action='store_true', help='Output to stats.md file instead of console')
args = parser.parse_args()
root = os.path.abspath(args.path)
total_dirs, total_files, total_lines, by_ext = count_stats(root)
tree_lines = print_tree(root, max_depth=args.max_tree_depth)
if args.output_md:
# Get script directory for output file
script_dir = os.path.dirname(os.path.abspath(__file__))
output_file = os.path.join(script_dir, 'stats.md')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(f"# Project Statistics\n\n")
f.write(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"**Path:** `{root}`\n\n")
f.write("## Summary\n\n")
f.write(f"- **Total directories:** {total_dirs}\n")
f.write(f"- **Total files:** {total_files}\n")
f.write(f"- **Total lines (approximate):** {total_lines}\n\n")
f.write("## Lines by Extension\n\n")
f.write("| Extension | Files | Lines |\n")
f.write("|-----------|-------|-------|\n")
for ext, (file_count, line_count) in sorted(by_ext.items(), key=lambda kv: kv[1][1], reverse=True):
label = ext if ext else '<no_ext>'
f.write(f"| {label} | {file_count} | {line_count} |\n")
f.write(f"\n## Directory Tree (max depth {args.max_tree_depth})\n\n")
f.write("```\n")
for line in tree_lines:
f.write(line + "\n")
f.write("```\n")
print(f"Statistics saved to: {output_file}")
else:
# Original console output
print(f"Analyzing project at: {root}")
print()
print('=== SUMMARY ===')
print(f"Total directories: {total_dirs}")
print(f"Total files: {total_files}")
print(f"Total lines (approximate): {total_lines}")
print()
print('=== LINES BY EXTENSION ===')
for ext, (file_count, line_count) in sorted(by_ext.items(), key=lambda kv: kv[1][1], reverse=True):
label = ext if ext else '<no_ext>'
print(f"{label:10} files={file_count:6} lines={line_count:10}")
print()
print(f"=== DIRECTORY TREE (max depth {args.max_tree_depth}) ===")
for line in tree_lines:
print(line)
if __name__ == '__main__':
main()
+1
View File
@@ -0,0 +1 @@
psutil>=5.9.0
+3
View File
@@ -0,0 +1,3 @@
flask>=2.3.0
flask-socketio>=5.3.0
psutil>=5.9.0
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
Project Tamagotchi - Browser Terminal
Single launcher for the browser-based terminal tamagotchi
"""
import os
import sys
import subprocess
def main():
print("=" * 60)
print("🐣 PROJECT TAMAGOTCHI - BROWSER TERMINAL")
print("=" * 60)
print("Features:")
print(" • Terminal interface in your browser")
print(" • Real-time file monitoring (every 2 seconds)")
print(" • ASCII display (no emojis)")
print(" • Activity detection when you edit files")
print(" • Command history (use arrow keys)")
print(" • Terminal effects and animations")
print()
print("🎮 Controls in browser:")
print(" • f - Feed the tamagotchi")
print(" • p - Play with the tamagotchi")
print(" • r - Refresh stats (automatic)")
print(" • q - Quit session")
print()
print("🔧 How it works:")
print(" • Scans your project for code files")
print(" • Detects file changes in real-time")
print(" • Updates tamagotchi mood based on activity")
print(" • Shows project statistics")
print()
# Check dependencies
script_dir = os.path.dirname(os.path.abspath(__file__))
venv_python = os.path.join(script_dir, 'tamagotchi_env', 'bin', 'python')
if not os.path.exists(venv_python):
print("📦 Setting up environment...")
try:
subprocess.run([sys.executable, '-m', 'venv', 'tamagotchi_env'],
cwd=script_dir, check=True, capture_output=True)
print("✅ Virtual environment created")
except subprocess.CalledProcessError:
print("❌ Failed to create virtual environment")
return 1
# Install dependencies if needed
print("📦 Checking dependencies...")
try:
venv_pip = os.path.join(script_dir, 'tamagotchi_env', 'bin', 'pip')
result = subprocess.run([venv_pip, 'install', 'flask', 'flask-socketio'],
cwd=script_dir, check=True, capture_output=True)
print("✅ Dependencies ready")
except subprocess.CalledProcessError:
print("✅ Dependencies already installed")
# Get project name
project_name = "MyClub"
if len(sys.argv) > 1:
project_name = sys.argv[1]
print(f"📊 Project: {project_name}")
print("🌐 Starting browser terminal...")
print("-" * 60)
print("🌐 OPEN YOUR BROWSER TO: http://localhost:5000")
print("⌨️ Terminal interface with real-time updates!")
print("🔥 Edit files to see activity detection!")
print("📱 Works on mobile too!")
print("🛑 Press Ctrl+C to stop the server")
print("=" * 60)
# Launch browser terminal
terminal_script = os.path.join(script_dir, 'browser_terminal_tamagotchi.py')
if os.path.exists(terminal_script):
try:
subprocess.run([venv_python, terminal_script, project_name])
except KeyboardInterrupt:
print("\n👋 Browser terminal stopped! Thanks for playing!")
return 0
else:
print(f"❌ Error: {terminal_script} not found!")
return 1
if __name__ == '__main__':
exit(main())
# Testing file tracking Mon Jan 12 17:44:15 CET 2026
# Testing stable terminal Mon Jan 12 17:46:45 CET 2026
# Testing bigger terminal Mon Jan 12 17:50:30 CET 2026
# Testing much bigger UI Mon Jan 12 17:51:32 CET 2026
+1157
View File
File diff suppressed because it is too large Load Diff
+335
View File
@@ -0,0 +1,335 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terminal Tamagotchi</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
color: #00ff00;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
.terminal-header {
background: #333;
padding: 8px;
border-bottom: 2px solid #666;
display: flex;
align-items: center;
gap: 8px;
}
.terminal-button {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
}
.terminal-button.close { background: #ff5f56; }
.terminal-button.minimize { background: #ffbd2e; }
.terminal-button.maximize { background: #27c93f; }
.terminal-title {
color: #fff;
font-size: 14px;
flex: 1;
text-align: center;
}
.terminal-body {
flex: 1;
padding: 25px;
overflow: hidden; /* No scrolling at all */
font-size: 18px; /* Much bigger font */
line-height: 1.5; /* More spacing */
white-space: pre-wrap;
background: #0a0a0a;
max-height: calc(100vh - 120px); /* More height */
box-sizing: border-box;
}
.terminal-body::-webkit-scrollbar {
width: 8px;
}
.terminal-body::-webkit-scrollbar-track {
background: #1a1a1a;
}
.terminal-body::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
.terminal-body::-webkit-scrollbar-thumb:hover {
background: #555;
}
.terminal-input {
background: #0a0a0a;
border: none;
color: #00ff00;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
font-size: 18px; /* Match terminal body */
padding: 12px 25px; /* Bigger padding */
outline: none;
width: 100%;
border-top: 1px solid #333;
box-sizing: border-box;
}
.terminal-input::placeholder {
color: #666;
}
.typing-indicator {
color: #ffff00;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.cursor {
display: inline-block;
width: 8px;
height: 16px;
background: #00ff00;
animation: cursor-blink 1s infinite;
vertical-align: text-bottom;
}
@keyframes cursor-blink {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
.status-bar {
background: #333;
color: #00ff00;
padding: 8px 25px; /* Bigger padding */
font-size: 14px; /* Bigger status bar */
border-top: 1px solid #666;
display: flex;
justify-content: space-between;
align-items: center;
}
.status-bar > div {
flex: 1;
text-align: center;
}
.status-bar > div:first-child {
text-align: left;
}
.status-bar > div:last-child {
text-align: right;
}
.activity-detected {
color: #ff6b6b;
font-weight: bold;
}
/* Scanlines effect */
.terminal-body::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
transparent 50%,
rgba(0, 255, 0, 0.03) 50%
);
background-size: 100% 4px;
pointer-events: none;
z-index: 1;
}
.terminal-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
}
.terminal-content {
position: relative;
z-index: 2;
}
</style>
</head>
<body>
<div class="terminal-header">
<button class="terminal-button close"></button>
<button class="terminal-button minimize"></button>
<button class="terminal-button maximize"></button>
<div class="terminal-title">terminal-tamagotchi@myclub: ~$</div>
</div>
<div class="terminal-wrapper">
<div class="terminal-body" id="terminal">
<div class="terminal-content" id="terminalContent">
Initializing terminal monitor...
Scanning project files...
</div>
</div>
</div>
<input type="text" class="terminal-input" id="terminalInput" placeholder="Enter command (f=feed, p=play, r=refresh, q=quit)..." autocomplete="off">
<div class="status-bar">
<div id="statusLeft">Ready</div>
<div id="statusCenter">No activity</div>
<div id="statusRight">Connected</div>
</div>
<script>
const socket = io();
let commandHistory = [];
let historyIndex = -1;
const terminal = document.getElementById('terminalContent');
const input = document.getElementById('terminalInput');
const statusLeft = document.getElementById('statusLeft');
const statusCenter = document.getElementById('statusCenter');
const statusRight = document.getElementById('statusRight');
function updateTerminal(data) {
terminal.textContent = data.output;
// Update status - simplified to avoid conflicts
if (data.typing_detected) {
statusLeft.textContent = `ACTIVITY: ${data.recent_changes} files`;
statusLeft.className = 'activity-detected';
// Extract current file info from terminal output
const lines = data.output.split('\n');
const currentFileLine = lines.find(line => line.includes('File:'));
if (currentFileLine) {
const fileName = currentFileLine.split('File:')[1]?.trim();
statusCenter.textContent = `Editing: ${fileName}`;
} else {
statusCenter.textContent = 'Files being modified...';
}
} else {
statusLeft.textContent = 'Monitoring...';
statusLeft.className = '';
statusCenter.textContent = 'No activity';
}
// Auto-scroll to bottom
const terminalBody = document.getElementById('terminal');
terminalBody.scrollTop = terminalBody.scrollHeight;
}
function sendCommand(cmd) {
if (!cmd.trim()) return;
// Add to history
commandHistory.push(cmd);
historyIndex = commandHistory.length;
// Show command in terminal
terminal.textContent += `\n$ ${cmd}\n`;
// Send to server
console.log("Sending command:", cmd); // Debug log
socket.emit('command', { command: cmd });
// Clear input
input.value = '';
}
// Socket events
socket.on('connect', () => {
statusRight.textContent = 'Connected';
statusRight.style.color = '#00ff00';
});
socket.on('disconnect', () => {
statusRight.textContent = 'Disconnected';
statusRight.style.color = '#ff0000';
});
socket.on('terminal_update', (data) => {
updateTerminal(data);
});
socket.on('quit', (data) => {
terminal.textContent += `\n${data.message}\nConnection closed.\n`;
input.disabled = true;
input.placeholder = 'Session ended';
});
// Input handling
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
sendCommand(input.value);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = '';
}
}
});
// Focus input on page load
window.addEventListener('load', () => {
input.focus();
// Load initial data
fetch('/api/terminal-data')
.then(response => response.json())
.then(data => updateTerminal(data))
.catch(error => {
terminal.textContent += `Error loading data: ${error}\n`;
});
});
// Keep focus on terminal
document.addEventListener('click', () => {
input.focus();
});
// Add some terminal-like effects
setInterval(() => {
if (Math.random() < 0.01) { // 1% chance of flicker
terminal.style.opacity = '0.9';
setTimeout(() => {
terminal.style.opacity = '1';
}, 50);
}
}, 1000);
</script>
</body>
</html>