Initial Commit

This commit is contained in:
ddnthemc 2025-04-20 11:21:24 +02:00
commit f2ea6e1d0f
16 changed files with 520 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/log/
/.idea/
/__pycache__/

3
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

8
.idea/TrackAtt.iml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/TrackAtt.iml" filepath="$PROJECT_DIR$/.idea/TrackAtt.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

4
README.md Normal file
View file

@ -0,0 +1,4 @@
ActivityTrackerWeb is a web full stack implementation of an activity tracker.
Currently just a playground to test concepts and possible user experiences.
It's a *Work in Progress!*

15
activities_old.json Normal file
View file

@ -0,0 +1,15 @@
[
"Dormire",
"Guida Auto",
"Work",
"Cesso",
"Giardino",
"Cena/Pranzo",
"Cucina",
"Allenamento",
"Messa",
"Duolinguo",
"Online",
"SelfStudy",
"Puzza"
]

114
backend.py Normal file
View file

@ -0,0 +1,114 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
from typing import List
import uvicorn
import time
from pathlib import Path
from json_extensions import load_from_json_file, save_to_json_file
Base_Log = Path('./log')
Activities_File = Path('activities.json')
Base_Log.mkdir(exist_ok=True, parents=True)
app = FastAPI()
# Mount static files like CSS/JS/images
app.mount("/static", StaticFiles(directory="static"), name="static")
# Set up templates directory
templates = Jinja2Templates(directory="templates")
# Dummy shared state
current_activity = "No activity yet"
if Activities_File.is_file():
activities = load_from_json_file(Activities_File)
else:
activities = [
'Dormire',
'Guida Auto',
'Work',
'Cesso',
'Giardino',
'Cena/Pranzo',
'Cucina',
'Allenamento',
'Messa',
'Duolinguo',
'Online',
'SelfStudy',
]
save_to_json_file(Activities_File, activities)
# for i in range(99):
# activities.append(f'Act {i}')
connections: List[WebSocket] = []
@app.get("/", response_class=HTMLResponse)
async def get_homepage(request: Request, u: str = 'Nobody'):
return templates.TemplateResponse("index2.html", {"request": request, 'user': u})
@app.get("/api/init")
async def get_initial_data():
return JSONResponse(content={
"current": current_activity,
"activities": activities
})
@app.websocket("/ws/activity")
async def activity_websocket(websocket: WebSocket, u: str = 'Unk'):
await websocket.accept()
connections.append(websocket)
try:
while True:
data = await websocket.receive_json()
print(data)
if data.get("action") == "select":
now = time.time()
data['user'] = u
data['epoch'] = now
dump_data(data)
global current_activity
current_activity = data["activity"]
if current_activity not in activities:
activities.append(current_activity)
save_to_json_file(Activities_File, activities)
await broadcast({
"type": "activity_update",
"activity": current_activity
})
except WebSocketDisconnect:
connections.remove(websocket)
def dump_data(dt: dict):
user = dt.get('user', 'Ukn')
epoch = dt.get('epoch', 0.0)
human_epoch = time.strftime('%Y_%m_%d_%H%M%S', time.localtime(epoch))
outfile = Base_Log / f'Act_{user}_{int(1000.0 * epoch)}_{human_epoch}.json'
save_to_json_file(outfile, dt)
async def broadcast(message: dict):
for conn in connections:
try:
await conn.send_json(message)
except Exception:
connections.remove(conn)
def main():
pass
if __name__ == '__main__':
uvicorn.run(app, host="127.0.0.1", port=8000)

39
json_extensions.py Normal file
View file

@ -0,0 +1,39 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import json
from decimal import Decimal
# from task_row_x import TaskRow, SimpleTaskRow
def load_from_json_file(infile):
with open(infile, 'r') as fin:
obj = json.load(fin)
return obj
class SpecialEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Decimal):
return str(obj) # Or you could return str(obj) depending on your needs
# if isinstance(obj, TaskRow) or isinstance(obj, SimpleTaskRow):
if hasattr(obj, 'get_dict') and callable(getattr(obj, 'get_dict')):
return obj.get_dict()
if isinstance(obj, set):
return [str(v) for v in obj]
return super().default(obj)
def save_to_json_file(out_name, obj):
with open(out_name, 'w', encoding='utf-8', newline='\n') as fout:
json.dump(obj, fout, indent=4, cls=SpecialEncoder)
def save_to_json_str(obj):
return json.dumps(obj, indent=4, cls=SpecialEncoder)
def main():
pass
if __name__ == '__main__':
main()

106
static/script.js Normal file
View file

@ -0,0 +1,106 @@
let currentActivity = '';
let socket;
function renderActivitiesOld(activities) {
const list = document.getElementById('activity-list');
list.innerHTML = '';
activities.forEach(activity => {
const div = document.createElement('div');
div.textContent = activity;
div.className = 'activity-item' + (activity === currentActivity ? ' selected' : '');
div.onclick = () => selectActivity(activity);
list.appendChild(div);
});
}
function renderActivities(activities) {
const list = document.getElementById('activity-list');
list.innerHTML = '';
activities.forEach(activity => {
const id = `activity-${activity.replace(/\s+/g, '-')}`;
const label = document.createElement('label');
label.className = 'activity-option';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'activity';
radio.value = activity;
radio.id = id;
if (activity === currentActivity) {
radio.checked = true;
}
radio.addEventListener('change', () => {
socket.send(JSON.stringify({ action: 'select', activity }));
});
const span = document.createElement('span');
span.textContent = activity;
label.appendChild(radio);
label.appendChild(span);
list.appendChild(label);
});
}
function selectActivity(activity) {
currentActivity = activity;
document.getElementById('current-activity').textContent = activity;
renderActivities(activityList);
socket.send(JSON.stringify({ action: 'select', activity }));
}
function addNewActivity() {
const input = document.getElementById('new-activity');
const newAct = input.value.trim();
if (newAct && !activityList.includes(newAct)) {
activityList.push(newAct);
selectActivity(newAct);
input.value = '';
}
}
let activityList = [];
async function initializePage() {
// Fetch initial data from server
const response = await fetch(`/api/init?u=${user}`);
const data = await response.json();
currentActivity = data.current;
activityList = data.activities;
document.getElementById('current-activity').textContent = currentActivity;
renderActivities(activityList);
}
function setupWebSocket() {
socket = new WebSocket(`ws://${window.location.host}/ws/activity?u=${user}`);
socket.onopen = () => {
console.log('WebSocket connected');
socket.send(JSON.stringify({ action: 'hello' }));
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'activity_update') {
currentActivity = msg.activity;
document.getElementById('current-activity').textContent = currentActivity;
renderActivities(activityList);
}
};
socket.onclose = () => {
console.log('WebSocket closed, retrying...');
setTimeout(setupWebSocket, 1000);
};
}
// Initialize on load
window.onload = () => {
initializePage();
setupWebSocket();
};

127
static/style.css Normal file
View file

@ -0,0 +1,127 @@
:root {
--primary: #0066cc;
--accent: #e0f0ff;
--text: #222;
--bg: #f9f9f9;
--border-radius: 0.5rem;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Segoe UI", sans-serif;
}
body {
background-color: var(--bg);
color: var(--text);
padding: 1rem;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
width: 100%;
max-width: 600px;
background-color: white;
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 2rem;
}
h1, h2 {
color: var(--primary);
}
#current-activity {
font-size: 1.2rem;
padding: 1rem;
background-color: var(--accent);
border-radius: var(--border-radius);
margin-top: 0.5rem;
}
#activity-list {
list-style-type: none;
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#activity-list li {
padding: 0.6rem 1rem;
background-color: #f0f0f0;
border-radius: var(--border-radius);
cursor: pointer;
transition: background 0.2s;
}
#activity-list li:hover {
background-color: #dceeff;
}
input[type="text"] {
width: 100%;
padding: 0.6rem;
font-size: 1rem;
margin-top: 0.5rem;
border: 1px solid #ccc;
border-radius: var(--border-radius);
}
button {
margin-top: 0.5rem;
padding: 0.6rem 1.2rem;
font-size: 1rem;
background-color: var(--primary);
color: white;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
transition: background 0.3s ease;
}
button:hover {
background-color: #004a99;
}
footer {
text-align: center;
font-size: 0.9rem;
color: #666;
margin-top: 2rem;
}
#activity-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.activity-option {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 1rem;
background-color: #f0f0f0;
border-radius: var(--border-radius);
cursor: pointer;
transition: background 0.2s;
}
.activity-option:hover {
background-color: #e6f2ff;
}
.activity-option input[type="radio"] {
accent-color: var(--primary);
width: 1.2rem;
height: 1.2rem;
}

4
static/style2.css Normal file
View file

@ -0,0 +1,4 @@
body { font-family: sans-serif; margin: 2rem; }
.activity-list { margin-top: 1rem; }
.activity-item { cursor: pointer; padding: 0.5rem; border: 1px solid #ccc; margin-bottom: 0.5rem; }
.activity-item.selected { background-color: #def; }

28
templates/index.html Normal file
View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Activity Tracker</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<h2>Select Your Activity</h2>
<div>
<strong>Current Activity:</strong> <span id="current-activity">Loading...</span>
</div>
<div class="activity-list" id="activity-list">
<!-- Dynamically populated list -->
</div>
<div>
<input type="text" id="new-activity" placeholder="New activity name" />
<button onclick="addNewActivity()">Add New Activity</button>
</div>
<script src="/static/script.js"></script>
</body>
</html>

41
templates/index2.html Normal file
View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Activity Tracker</title>
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
<div class="container">
<header>
<h1>Che stai Affà</h1>
</header>
<section id="current">
<h2>{{ user }} Current Activity:</h2>
<div id="current-activity" class="highlight">Loading...</div>
</section>
<section id="select">
<h2>Select Existing Activity:</h2>
<ul id="activity-list"></ul>
</section>
<section id="new">
<h2>Or Add a New One:</h2>
<input type="text" id="new-activity" placeholder="Type activity..." />
<button onclick="addNewActivity()">Add & Select</button>
</section>
<footer>
<p>Connected via WebSocket | All changes sync in real-time</p>
</footer>
</div>
<script>
let user = "{{ user }}"
</script>
<script src="/static/script.js"></script>
</body>
</html>