Initial Commit
This commit is contained in:
commit
f2ea6e1d0f
16 changed files with 520 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
/log/
|
||||
/.idea/
|
||||
/__pycache__/
|
||||
|
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
8
.idea/TrackAtt.iml
generated
Normal file
8
.idea/TrackAtt.iml
generated
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
4
README.md
Normal 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
15
activities_old.json
Normal 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
114
backend.py
Normal 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
39
json_extensions.py
Normal 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
106
static/script.js
Normal 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
127
static/style.css
Normal 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
4
static/style2.css
Normal 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
28
templates/index.html
Normal 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
41
templates/index2.html
Normal 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>
|
Loading…
Add table
Reference in a new issue