api support

This commit is contained in:
2026-02-11 14:48:38 +01:00
parent b31bca91ad
commit 8024dc1bf8

381
app.py
View File

@@ -5,6 +5,8 @@ from bson.objectid import ObjectId
from datetime import datetime, timedelta from datetime import datetime, timedelta
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
import jwt
from functools import wraps
load_dotenv() load_dotenv()
@@ -65,6 +67,23 @@ def get_dashboard_context(user_id):
return current_entry, active_tasks return current_entry, active_tasks
# --- API Helpers ---
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'Token is missing!'}), 401
try:
if token.startswith('Bearer '):
token = token.split(" ")[1]
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
current_user_id = data['user_id']
except:
return jsonify({'message': 'Token is invalid!'}), 401
return f(current_user_id, *args, **kwargs)
return decorated
# --- Routes --- # --- Routes ---
@app.route('/') @app.route('/')
@@ -795,6 +814,368 @@ def delete_goal(goal_id):
db.goals.delete_one({'_id': ObjectId(goal_id), 'user_id': get_user_id()}) db.goals.delete_one({'_id': ObjectId(goal_id), 'user_id': get_user_id()})
return redirect(url_for('goals')) return redirect(url_for('goals'))
# --- Auth API ---
@app.route('/api/login', methods=['POST'])
def api_login():
auth = request.get_json()
if not auth or not auth.get('username') or not auth.get('password'):
return jsonify({'message': 'Missing credentials'}), 400
user = db.users.find_one({'username': auth['username']})
if user and bcrypt.check_password_hash(user['password'], auth['password']):
token = jwt.encode({
'user_id': str(user['_id']),
'exp': datetime.utcnow() + timedelta(days=30)
}, app.config['SECRET_KEY'], algorithm="HS256")
return jsonify({'token': token})
return jsonify({'message': 'Invalid credentials'}), 401
@app.route('/api/register', methods=['POST'])
def api_register():
data = request.get_json()
if not data or not data.get('username') or not data.get('password'):
return jsonify({'message': 'Missing credentials'}), 400
if db.users.find_one({'username': data['username']}):
return jsonify({'message': 'Username already exists'}), 409
hashed_password = bcrypt.generate_password_hash(data['password']).decode('utf-8')
user_id = db.users.insert_one({'username': data['username'], 'password': hashed_password}).inserted_id
# Auto login on register
token = jwt.encode({
'user_id': str(user_id),
'exp': datetime.utcnow() + timedelta(days=30)
}, app.config['SECRET_KEY'], algorithm="HS256")
return jsonify({'token': token})
# --- Dashboard / Status API ---
@app.route('/api/status', methods=['GET'])
@token_required
def api_status(user_id):
# Get standard dashboard context
current_entry, active_tasks = get_dashboard_context(user_id)
activities = list(db.activities.find({'user_id': user_id}))
return jsonify({
'is_tracking': current_entry is not None,
'entry': {
'id': str(current_entry['_id']),
'activity_id': str(current_entry['activity_id']),
'activity_name': current_entry.get('activity_name'),
'activity_color': current_entry.get('activity_color'),
'start_time': current_entry['start_time'].isoformat(),
'subcategory': current_entry.get('subcategory', ''),
'note': current_entry.get('note', '')
} if current_entry else None,
'active_tasks': [{
'id': str(t['_id']),
'name': t['name'],
'status': t['status'],
'completed_at': t['completed_at'].isoformat() if t.get('completed_at') else None
} for t in active_tasks],
'activities': [{
'id': str(a['_id']),
'name': a['name'],
'color': a.get('color', '#3498db'),
'subcategories': a.get('subcategories', [])
} for a in activities]
})
# --- Activity Management API ---
@app.route('/api/activities/create', methods=['POST'])
@token_required
def api_create_activity(user_id):
data = request.get_json()
name = data.get('name')
if not name: return jsonify({'message': 'Name required'}), 400
activity_id = db.activities.insert_one({
'user_id': user_id,
'name': name,
'color': data.get('color', '#3498db'),
'subcategories': data.get('subcategories', [])
}).inserted_id
# Optional default tasks
tasks = data.get('default_tasks', [])
for t in tasks:
db.tasks.insert_one({
'user_id': user_id,
'name': t,
'activity_id': ObjectId(activity_id),
'is_template': True,
'status': 'open',
'time_entry_id': None,
'created_at': datetime.now(),
'comments': []
})
return jsonify({'status': 'success', 'id': str(activity_id)})
@app.route('/api/activities/<activity_id>', methods=['PUT', 'DELETE'])
@token_required
def api_manage_activity(user_id, activity_id):
if request.method == 'DELETE':
# Optional: Check for confirmation or cascade delete logic
result = db.activities.delete_one({'_id': ObjectId(activity_id), 'user_id': user_id})
if result.deleted_count == 0: return jsonify({'message': 'Not found'}), 404
return jsonify({'status': 'deleted'})
if request.method == 'PUT':
data = request.get_json()
update_fields = {}
if 'name' in data: update_fields['name'] = data['name']
if 'color' in data: update_fields['color'] = data['color']
if 'subcategories' in data: update_fields['subcategories'] = data['subcategories']
db.activities.update_one(
{'_id': ObjectId(activity_id), 'user_id': user_id},
{'$set': update_fields}
)
return jsonify({'status': 'updated'})
# --- Task API ---
@app.route('/api/tasks', methods=['GET'])
@token_required
def api_tasks_list(user_id):
# Retrieve filters from query params
filter_status = request.args.get('status') # 'open', 'completed'
query = {'user_id': user_id, 'is_template': False}
if filter_status:
query['status'] = filter_status
tasks = list(db.tasks.find(query).sort('due_date', 1))
return jsonify([{
'id': str(t['_id']),
'name': t['name'],
'status': t['status'],
'activity_id': str(t.get('activity_id')) if t.get('activity_id') else None,
'subcategory': t.get('subcategory', ''),
'due_date': t['due_date'].isoformat() if t.get('due_date') else None,
'completed_at': t['completed_at'].isoformat() if t.get('completed_at') else None
} for t in tasks])
@app.route('/api/tasks/create', methods=['POST'])
@token_required
def api_create_task(user_id):
data = request.get_json()
if not data.get('name'): return jsonify({'message': 'Name required'}), 400
activity_id = data.get('activity_id')
# Check for active context if quick-add flag is present
time_entry_id = None
if data.get('link_to_active'):
current_entry = db.time_entries.find_one({'user_id': user_id, 'end_time': None})
if current_entry and (str(current_entry.get('activity_id')) == activity_id or not activity_id):
time_entry_id = current_entry['_id']
# If no activity specified but linking to active, verify contexts match or inherit?
# Keeping it simple: link if requested.
task_doc = {
'user_id': user_id,
'name': data['name'],
'status': 'open',
'is_template': data.get('is_template', False),
'source': 'api',
'created_at': datetime.now(),
'comments': [],
'subcategory': data.get('subcategory', ''),
'activity_id': ObjectId(activity_id) if activity_id else None,
'time_entry_id': time_entry_id
}
if data.get('due_date'):
try:
task_doc['due_date'] = datetime.fromisoformat(data['due_date'])
except: pass
new_id = db.tasks.insert_one(task_doc).inserted_id
return jsonify({'status': 'success', 'id': str(new_id)})
@app.route('/api/tasks/<task_id>', methods=['GET', 'PUT', 'DELETE'])
@token_required
def api_task_detail(user_id, task_id):
if request.method == 'GET':
t = db.tasks.find_one({'_id': ObjectId(task_id), 'user_id': user_id})
if not t: return jsonify({'message': 'Not found'}), 404
# Format comments
comments = []
for c in t.get('comments', []):
comments.append({
'text': c['text'],
'created_at': c['created_at'].isoformat()
})
return jsonify({
'id': str(t['_id']),
'name': t['name'],
'status': t['status'],
'comments': comments
# Add other fields as needed
})
if request.method == 'DELETE':
db.tasks.delete_one({'_id': ObjectId(task_id), 'user_id': user_id})
return jsonify({'status': 'deleted'})
if request.method == 'PUT':
data = request.get_json()
# Handle simple updates or comments
if 'comment' in data:
db.tasks.update_one(
{'_id': ObjectId(task_id), 'user_id': user_id},
{'$push': {'comments': {
'text': data['comment'],
'created_at': datetime.now(),
'user_id': user_id
}}}
)
return jsonify({'status': 'comment_added'})
update_fields = {}
if 'name' in data: update_fields['name'] = data['name']
if 'status' in data:
update_fields['status'] = data['status']
if data['status'] == 'completed':
update_fields['completed_at'] = datetime.now()
else:
update_fields['completed_at'] = None
db.tasks.update_one(
{'_id': ObjectId(task_id), 'user_id': user_id},
{'$set': update_fields}
)
return jsonify({'status': 'updated'})
# --- Logbook Detail / Edit API ---
@app.route('/api/logbook/<entry_id>', methods=['GET', 'PUT', 'DELETE'])
@token_required
def api_log_entry(user_id, entry_id):
if request.method == 'DELETE':
db.time_entries.delete_one({'_id': ObjectId(entry_id), 'user_id': user_id})
return jsonify({'status': 'deleted'})
if request.method == 'PUT':
data = request.get_json()
update_fields = {}
if 'note' in data: update_fields['note'] = data['note']
if 'subcategory' in data: update_fields['subcategory'] = data['subcategory']
try:
if 'start_time' in data:
# Expect ISO format
update_fields['start_time'] = datetime.fromisoformat(data['start_time'].replace('Z', '+00:00'))
if 'end_time' in data:
# If explicit null passed, sets to running? Usually we set a time.
if data['end_time']:
update_fields['end_time'] = datetime.fromisoformat(data['end_time'].replace('Z', '+00:00'))
except ValueError:
return jsonify({'message': 'Invalid date format'}), 400
db.time_entries.update_one(
{'_id': ObjectId(entry_id), 'user_id': user_id},
{'$set': update_fields}
)
return jsonify({'status': 'updated'})
# GET detail
entry = db.time_entries.find_one({'_id': ObjectId(entry_id), 'user_id': user_id})
if not entry: return jsonify({'message': 'Not found'}), 404
# Get associated tasks
tasks = list(db.tasks.find({'time_entry_id': ObjectId(entry_id)}))
return jsonify({
'id': str(entry['_id']),
'activity_id': str(entry['activity_id']),
'start_time': entry['start_time'].isoformat(),
'end_time': entry['end_time'].isoformat() if entry.get('end_time') else None,
'note': entry.get('note', ''),
'subcategory': entry.get('subcategory', ''),
'duration_seconds': (entry['end_time'] - entry['start_time']).total_seconds() if entry.get('end_time') else None,
'tasks': [{'id': str(t['_id']), 'name': t['name'], 'status': t['status']} for t in tasks]
})
# --- Goals API ---
@app.route('/api/goals', methods=['GET', 'POST'])
@token_required
def api_goals(user_id):
if request.method == 'GET':
# Re-using heavy calculation logic from web route for API response
goals_list = list(db.goals.find({'user_id': user_id}))
result_data = []
today = datetime.now()
start_of_today = today.replace(hour=0, minute=0, second=0, microsecond=0)
ranges = {
'daily': start_of_today,
'weekly': start_of_today - timedelta(days=today.weekday()),
'monthly': start_of_today.replace(day=1),
'yearly': start_of_today.replace(month=1, day=1)
}
for goal in goals_list:
start_date = ranges.get(goal['frequency'], start_of_today)
query = {
'user_id': user_id,
'start_time': {'$gte': start_date},
'end_time': {'$ne': None}
}
if goal.get('activity_id'): query['activity_id'] = goal['activity_id']
if goal.get('subcategory'): query['subcategory'] = goal['subcategory']
entries = list(db.time_entries.find(query))
total_seconds = sum([(e['end_time'] - e['start_time']).total_seconds() for e in entries])
current_hours = total_seconds / 3600
result_data.append({
'id': str(goal['_id']),
'name': goal['name'],
'target_hours': goal['target_hours'],
'current_hours': round(current_hours, 2),
'percent': min(100, int((current_hours / goal['target_hours']) * 100)),
'frequency': goal['frequency'],
'activity_id': str(goal.get('activity_id')) if goal.get('activity_id') else None
})
return jsonify(result_data)
if request.method == 'POST':
data = request.get_json()
if not data.get('activity_id'): return jsonify({'message': 'Activity required'}), 400
db.goals.insert_one({
'user_id': user_id,
'name': data.get('name', 'New Goal'),
'target_hours': float(data.get('target_hours', 1)),
'frequency': data.get('frequency', 'daily'),
'activity_id': ObjectId(data['activity_id']),
'subcategory': data.get('subcategory', ''),
'created_at': datetime.now()
})
return jsonify({'status': 'created'})
@app.route('/api/goals/<goal_id>', methods=['DELETE'])
@token_required
def api_goal_delete(user_id, goal_id):
db.goals.delete_one({'_id': ObjectId(goal_id), 'user_id': user_id})
return jsonify({'status': 'deleted'})
debug = os.getenv('DEBUG') == 'False' debug = os.getenv('DEBUG') == 'False'
host = os.getenv('HOST') host = os.getenv('HOST')
port = int(os.getenv('PORT', 80)) port = int(os.getenv('PORT', 80))