Add activity logs table
This commit is contained in:
26
app.py
26
app.py
@@ -160,6 +160,7 @@ def person_overview(person_id):
|
|||||||
def create_person():
|
def create_person():
|
||||||
name = request.form.get("name")
|
name = request.form.get("name")
|
||||||
new_person_id = db.create_person(name)
|
new_person_id = db.create_person(name)
|
||||||
|
db.activityRequest.log(current_user.id, 'CREATE_PERSON', 'person', new_person_id, f"Created person: {name}")
|
||||||
return render_template('partials/person.html', person_id=new_person_id, name=name), 200, {"HX-Trigger": "updatedPeople"}
|
return render_template('partials/person.html', person_id=new_person_id, name=name), 200, {"HX-Trigger": "updatedPeople"}
|
||||||
|
|
||||||
|
|
||||||
@@ -168,7 +169,9 @@ def create_person():
|
|||||||
@admin_required
|
@admin_required
|
||||||
@validate_person
|
@validate_person
|
||||||
def delete_person(person_id):
|
def delete_person(person_id):
|
||||||
|
name = db.get_person_name(person_id)
|
||||||
db.delete_person(person_id)
|
db.delete_person(person_id)
|
||||||
|
db.activityRequest.log(current_user.id, 'DELETE_PERSON', 'person', person_id, f"Deleted person: {name}")
|
||||||
return "", 200, {"HX-Trigger": "updatedPeople"}
|
return "", 200, {"HX-Trigger": "updatedPeople"}
|
||||||
|
|
||||||
|
|
||||||
@@ -187,7 +190,9 @@ def get_person_edit_form(person_id):
|
|||||||
@require_ownership
|
@require_ownership
|
||||||
def update_person_name(person_id):
|
def update_person_name(person_id):
|
||||||
new_name = request.form.get("name")
|
new_name = request.form.get("name")
|
||||||
|
old_name = db.get_person_name(person_id)
|
||||||
db.update_person_name(person_id, new_name)
|
db.update_person_name(person_id, new_name)
|
||||||
|
db.activityRequest.log(current_user.id, 'UPDATE_PERSON_NAME', 'person', person_id, f"Updated name for {old_name} to {new_name}")
|
||||||
return render_template('partials/person.html', person_id=person_id, name=new_name), 200, {"HX-Trigger": "updatedPeople"}
|
return render_template('partials/person.html', person_id=person_id, name=new_name), 200, {"HX-Trigger": "updatedPeople"}
|
||||||
|
|
||||||
|
|
||||||
@@ -203,6 +208,7 @@ def create_exercise():
|
|||||||
name = request.form.get("name")
|
name = request.form.get("name")
|
||||||
attribute_ids = request.form.getlist('attribute_ids')
|
attribute_ids = request.form.getlist('attribute_ids')
|
||||||
exercise = db.create_exercise(name, attribute_ids)
|
exercise = db.create_exercise(name, attribute_ids)
|
||||||
|
db.activityRequest.log(current_user.id, 'CREATE_EXERCISE', 'exercise', exercise['exercise_id'], f"Created exercise: {name}")
|
||||||
return render_template('partials/exercise.html',
|
return render_template('partials/exercise.html',
|
||||||
exercise_id=exercise['exercise_id'],
|
exercise_id=exercise['exercise_id'],
|
||||||
name=exercise['name'],
|
name=exercise['name'],
|
||||||
@@ -250,6 +256,7 @@ def update_exercise(exercise_id):
|
|||||||
new_name = request.form.get('name')
|
new_name = request.form.get('name')
|
||||||
attribute_ids = request.form.getlist('attribute_ids')
|
attribute_ids = request.form.getlist('attribute_ids')
|
||||||
exercise = db.update_exercise(exercise_id, new_name, attribute_ids)
|
exercise = db.update_exercise(exercise_id, new_name, attribute_ids)
|
||||||
|
db.activityRequest.log(current_user.id, 'UPDATE_EXERCISE', 'exercise', exercise_id, f"Updated exercise: {new_name}")
|
||||||
return render_template('partials/exercise.html',
|
return render_template('partials/exercise.html',
|
||||||
exercise_id=exercise_id,
|
exercise_id=exercise_id,
|
||||||
name=exercise['name'],
|
name=exercise['name'],
|
||||||
@@ -280,6 +287,23 @@ def settings():
|
|||||||
return render_template('settings.html', people=people, exercises=exercises, all_attributes=formatted_options)
|
return render_template('settings.html', people=people, exercises=exercises, all_attributes=formatted_options)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/settings/activity_logs")
|
||||||
|
@login_required
|
||||||
|
def settings_activity_logs():
|
||||||
|
limit = 50
|
||||||
|
offset = request.args.get('offset', 0, type=int)
|
||||||
|
logs = db.activityRequest.get_recent_logs(limit=limit, offset=offset)
|
||||||
|
|
||||||
|
# Check if there are more logs to load
|
||||||
|
has_more = len(logs) == limit
|
||||||
|
|
||||||
|
return render_template('partials/activity_logs.html',
|
||||||
|
logs=logs,
|
||||||
|
offset=offset,
|
||||||
|
limit=limit,
|
||||||
|
has_more=has_more)
|
||||||
|
|
||||||
|
|
||||||
# Routes moved to routes/tags.py blueprint
|
# Routes moved to routes/tags.py blueprint
|
||||||
|
|
||||||
@ app.route("/person/<int:person_id>/exercise/<int:exercise_id>/sparkline", methods=['GET'])
|
@ app.route("/person/<int:person_id>/exercise/<int:exercise_id>/sparkline", methods=['GET'])
|
||||||
@@ -352,7 +376,9 @@ def add_exercise():
|
|||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def delete_exercise(exercise_id):
|
def delete_exercise(exercise_id):
|
||||||
|
exercise = db.get_exercise(exercise_id)
|
||||||
db.exercises.delete_exercise(exercise_id)
|
db.exercises.delete_exercise(exercise_id)
|
||||||
|
db.activityRequest.log(current_user.id, 'DELETE_EXERCISE', 'exercise', exercise_id, f"Deleted exercise: {exercise['name']}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@app.teardown_appcontext
|
@app.teardown_appcontext
|
||||||
|
|||||||
2
db.py
2
db.py
@@ -11,6 +11,7 @@ from features.person_overview import PersonOverview
|
|||||||
from features.stats import Stats
|
from features.stats import Stats
|
||||||
from features.dashboard import Dashboard
|
from features.dashboard import Dashboard
|
||||||
from features.schema import Schema
|
from features.schema import Schema
|
||||||
|
from features.activity import Activity
|
||||||
from utils import get_exercise_graph_model
|
from utils import get_exercise_graph_model
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class DataBase():
|
|||||||
self.people_graphs = PeopleGraphs(self.execute)
|
self.people_graphs = PeopleGraphs(self.execute)
|
||||||
self.dashboard = Dashboard(self.execute)
|
self.dashboard = Dashboard(self.execute)
|
||||||
self.schema = Schema(self.execute)
|
self.schema = Schema(self.execute)
|
||||||
|
self.activityRequest = Activity(self.execute)
|
||||||
|
|
||||||
db_url = urlparse(os.environ['DATABASE_URL'])
|
db_url = urlparse(os.environ['DATABASE_URL'])
|
||||||
# if db_url is null then throw error
|
# if db_url is null then throw error
|
||||||
|
|||||||
41
features/activity.py
Normal file
41
features/activity.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from flask import request, current_app
|
||||||
|
from utils import get_client_ip
|
||||||
|
|
||||||
|
class Activity:
|
||||||
|
def __init__(self, db_connection_method):
|
||||||
|
self.execute = db_connection_method
|
||||||
|
|
||||||
|
def log(self, person_id, action, entity_type=None, entity_id=None, details=None):
|
||||||
|
"""Records an action in the activity_log table."""
|
||||||
|
try:
|
||||||
|
ip_address = get_client_ip()
|
||||||
|
user_agent = request.user_agent.string if request else None
|
||||||
|
sql = """
|
||||||
|
INSERT INTO activity_log (person_id, action, entity_type, entity_id, details, ip_address, user_agent)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
self.execute(sql, [person_id, action, entity_type, entity_id, details, ip_address, user_agent], commit=True)
|
||||||
|
except Exception as e:
|
||||||
|
# We don't want logging to break the main application flow
|
||||||
|
current_app.logger.error(f"Error logging activity: {e}")
|
||||||
|
|
||||||
|
def get_recent_logs(self, limit=50, offset=0):
|
||||||
|
"""Fetches recent activity logs with person names, supporting pagination."""
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
al.id,
|
||||||
|
al.person_id,
|
||||||
|
p.name as person_name,
|
||||||
|
al.action,
|
||||||
|
al.entity_type,
|
||||||
|
al.entity_id,
|
||||||
|
al.details,
|
||||||
|
al.ip_address,
|
||||||
|
al.user_agent,
|
||||||
|
al.timestamp
|
||||||
|
FROM activity_log al
|
||||||
|
LEFT JOIN person p ON al.person_id = p.person_id
|
||||||
|
ORDER BY al.timestamp DESC
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
"""
|
||||||
|
return self.execute(query, [limit, offset])
|
||||||
@@ -105,11 +105,12 @@ def signup():
|
|||||||
form = SignupForm()
|
form = SignupForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
hashed_password = generate_password_hash(form.password.data)
|
hashed_password = generate_password_hash(form.password.data)
|
||||||
create_person(
|
new_person_id = create_person(
|
||||||
name=form.name.data,
|
name=form.name.data,
|
||||||
email=form.email.data,
|
email=form.email.data,
|
||||||
password_hash=hashed_password
|
password_hash=hashed_password
|
||||||
)
|
)
|
||||||
|
db.activityRequest.log(new_person_id, 'SIGNUP', 'person', new_person_id, f"User signed up: {form.email.data}")
|
||||||
flash("Account created successfully. Please log in.", "success")
|
flash("Account created successfully. Please log in.", "success")
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
return render_template('auth/signup.html', form=form)
|
return render_template('auth/signup.html', form=form)
|
||||||
@@ -122,11 +123,11 @@ def login():
|
|||||||
person = get_person_by_email(form.email.data)
|
person = get_person_by_email(form.email.data)
|
||||||
if person and check_password_hash(person.password_hash, form.password.data):
|
if person and check_password_hash(person.password_hash, form.password.data):
|
||||||
login_user(person)
|
login_user(person)
|
||||||
record_login_attempt(form.email.data, True, person.id)
|
db.activityRequest.log(person.id, 'LOGIN_SUCCESS', 'person', person.id, f"User logged in: {form.email.data}")
|
||||||
flash("Logged in successfully.", "success")
|
flash("Logged in successfully.", "success")
|
||||||
return redirect(url_for('calendar.get_calendar', person_id=person.id))
|
return redirect(url_for('calendar.get_calendar', person_id=person.id))
|
||||||
else:
|
else:
|
||||||
record_login_attempt(form.email.data, False, person.id if person else None)
|
db.activityRequest.log(person.id if person else None, 'LOGIN_FAILURE', 'person', person.id if person else None, f"Failed login attempt for: {form.email.data}")
|
||||||
flash("Invalid email or password.", "danger")
|
flash("Invalid email or password.", "danger")
|
||||||
return render_template('auth/login.html', form=form)
|
return render_template('auth/login.html', form=form)
|
||||||
|
|
||||||
@@ -134,6 +135,8 @@ def login():
|
|||||||
@auth.route('/logout')
|
@auth.route('/logout')
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def logout():
|
||||||
|
person_id = current_user.id if current_user.is_authenticated else None
|
||||||
logout_user()
|
logout_user()
|
||||||
|
db.activityRequest.log(person_id, 'LOGOUT', 'person', person_id, "User logged out")
|
||||||
flash('You have been logged out.', 'success')
|
flash('You have been logged out.', 'success')
|
||||||
return redirect(url_for('auth.login'))
|
return redirect(url_for('auth.login'))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from flask import Blueprint, render_template, request, current_app
|
from flask import Blueprint, render_template, request, current_app
|
||||||
from jinja2_fragments import render_block
|
from jinja2_fragments import render_block
|
||||||
from flask_htmx import HTMX
|
from flask_htmx import HTMX
|
||||||
|
from flask_login import current_user
|
||||||
from extensions import db # Still need db for execute method
|
from extensions import db # Still need db for execute method
|
||||||
from decorators import validate_person, validate_workout
|
from decorators import validate_person, validate_workout
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ def update_workout_note(person_id, workout_id):
|
|||||||
"""Updates a specific workout note."""
|
"""Updates a specific workout note."""
|
||||||
note = request.form.get('note')
|
note = request.form.get('note')
|
||||||
_update_workout_note_for_person(person_id, workout_id, note) # Use local helper
|
_update_workout_note_for_person(person_id, workout_id, note) # Use local helper
|
||||||
|
db.activityRequest.log(current_user.id, 'UPDATE_NOTE', 'workout', workout_id, f"Updated note for workout {workout_id}")
|
||||||
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
|
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
|
||||||
|
|
||||||
@notes_bp.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['GET'])
|
@notes_bp.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['GET'])
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, request, current_app
|
from flask import Blueprint, render_template, redirect, url_for, request, current_app
|
||||||
from jinja2_fragments import render_block
|
from jinja2_fragments import render_block
|
||||||
from flask_htmx import HTMX
|
from flask_htmx import HTMX
|
||||||
from flask_login import login_required
|
from flask_login import login_required, current_user
|
||||||
from extensions import db
|
from extensions import db
|
||||||
from decorators import validate_workout, validate_topset, require_ownership, validate_person
|
from decorators import validate_workout, validate_topset, require_ownership, validate_person
|
||||||
from utils import convert_str_to_date
|
from utils import convert_str_to_date
|
||||||
@@ -140,6 +140,7 @@ def _get_workout_view_model(person_id, workout_id):
|
|||||||
@require_ownership
|
@require_ownership
|
||||||
def create_workout(person_id):
|
def create_workout(person_id):
|
||||||
new_workout_id = db.create_workout(person_id)
|
new_workout_id = db.create_workout(person_id)
|
||||||
|
db.activityRequest.log(current_user.id, 'CREATE_WORKOUT', 'workout', new_workout_id, f"Created workout for person_id: {person_id}")
|
||||||
# Use the local helper function to get the view model
|
# Use the local helper function to get the view model
|
||||||
view_model = _get_workout_view_model(person_id, new_workout_id)
|
view_model = _get_workout_view_model(person_id, new_workout_id)
|
||||||
if "error" in view_model: # Handle case where workout creation might fail or is empty
|
if "error" in view_model: # Handle case where workout creation might fail or is empty
|
||||||
@@ -153,6 +154,7 @@ def create_workout(person_id):
|
|||||||
@require_ownership
|
@require_ownership
|
||||||
def delete_workout(person_id, workout_id):
|
def delete_workout(person_id, workout_id):
|
||||||
db.delete_workout(workout_id)
|
db.delete_workout(workout_id)
|
||||||
|
db.activityRequest.log(current_user.id, 'DELETE_WORKOUT', 'workout', workout_id, f"Deleted workout: {workout_id}")
|
||||||
return redirect(url_for('calendar.get_calendar', person_id=person_id))
|
return redirect(url_for('calendar.get_calendar', person_id=person_id))
|
||||||
|
|
||||||
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/start_date_edit_form", methods=['GET'])
|
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/start_date_edit_form", methods=['GET'])
|
||||||
@@ -172,6 +174,7 @@ def get_workout_start_date_edit_form(person_id, workout_id):
|
|||||||
def update_workout_start_date(person_id, workout_id):
|
def update_workout_start_date(person_id, workout_id):
|
||||||
new_start_date_str = request.form.get('start-date')
|
new_start_date_str = request.form.get('start-date')
|
||||||
db.update_workout_start_date(workout_id, new_start_date_str)
|
db.update_workout_start_date(workout_id, new_start_date_str)
|
||||||
|
db.activityRequest.log(current_user.id, 'UPDATE_WORKOUT_START_DATE', 'workout', workout_id, f"Updated start date to {new_start_date_str}")
|
||||||
# Convert string back to date for rendering the partial
|
# Convert string back to date for rendering the partial
|
||||||
new_start_date = convert_str_to_date(new_start_date_str, '%Y-%m-%d')
|
new_start_date = convert_str_to_date(new_start_date_str, '%Y-%m-%d')
|
||||||
return render_template('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=new_start_date)
|
return render_template('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=new_start_date)
|
||||||
@@ -225,6 +228,7 @@ def create_topset(person_id, workout_id):
|
|||||||
weight = request.form.get("weight")
|
weight = request.form.get("weight")
|
||||||
new_topset_id = db.create_topset(workout_id, exercise_id, repetitions, weight)
|
new_topset_id = db.create_topset(workout_id, exercise_id, repetitions, weight)
|
||||||
exercise = db.get_exercise(exercise_id)
|
exercise = db.get_exercise(exercise_id)
|
||||||
|
db.activityRequest.log(current_user.id, 'ADD_SET', 'topset', new_topset_id, f"Added set: {repetitions} x {weight}kg {exercise['name']} in workout {workout_id}")
|
||||||
return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=new_topset_id, exercise_id=exercise_id, exercise_name=exercise.get('name'), repetitions=repetitions, weight=weight), 200, {"HX-Trigger": "topsetAdded"}
|
return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=new_topset_id, exercise_id=exercise_id, exercise_name=exercise.get('name'), repetitions=repetitions, weight=weight), 200, {"HX-Trigger": "topsetAdded"}
|
||||||
|
|
||||||
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/topset/<int:topset_id>", methods=['PUT'])
|
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/topset/<int:topset_id>", methods=['PUT'])
|
||||||
@@ -237,6 +241,7 @@ def update_topset(person_id, workout_id, topset_id):
|
|||||||
weight = request.form.get("weight")
|
weight = request.form.get("weight")
|
||||||
db.update_topset(exercise_id, repetitions, weight, topset_id)
|
db.update_topset(exercise_id, repetitions, weight, topset_id)
|
||||||
exercise = db.get_exercise(exercise_id)
|
exercise = db.get_exercise(exercise_id)
|
||||||
|
db.activityRequest.log(current_user.id, 'UPDATE_SET', 'topset', topset_id, f"Updated set {topset_id}: {repetitions} x {weight}kg {exercise['name']}")
|
||||||
return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=topset_id, exercise_id=exercise_id, exercise_name=exercise.get('name'), repetitions=repetitions, weight=weight)
|
return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=topset_id, exercise_id=exercise_id, exercise_name=exercise.get('name'), repetitions=repetitions, weight=weight)
|
||||||
|
|
||||||
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/topset/<int:topset_id>/delete", methods=['DELETE'])
|
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/topset/<int:topset_id>/delete", methods=['DELETE'])
|
||||||
@@ -244,7 +249,9 @@ def update_topset(person_id, workout_id, topset_id):
|
|||||||
@validate_topset
|
@validate_topset
|
||||||
@require_ownership
|
@require_ownership
|
||||||
def delete_topset(person_id, workout_id, topset_id):
|
def delete_topset(person_id, workout_id, topset_id):
|
||||||
|
topset = db.get_topset(topset_id)
|
||||||
db.delete_topset(topset_id)
|
db.delete_topset(topset_id)
|
||||||
|
db.activityRequest.log(current_user.id, 'DELETE_SET', 'topset', topset_id, f"Deleted set {topset_id}: {topset['exercise_name']} in workout {workout_id}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/exercise/most_recent_topset_for_exercise", methods=['GET'])
|
@workout_bp.route("/person/<int:person_id>/workout/<int:workout_id>/exercise/most_recent_topset_for_exercise", methods=['GET'])
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 253 KiB |
73
templates/partials/activity_logs.html
Normal file
73
templates/partials/activity_logs.html
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{% if offset == 0 %}
|
||||||
|
<div class="overflow-x-auto rounded-lg">
|
||||||
|
<div class="align-middle inline-block min-w-full">
|
||||||
|
<div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actor</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Details
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP & Source
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="activity-logs-tbody" class="bg-white divide-y divide-gray-200">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for log in logs %}
|
||||||
|
<tr class="hover:bg-gray-50 transition-colors">
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm text-gray-500">{{ log.timestamp.strftime('%Y-%m-%d
|
||||||
|
%H:%M:%S') }}</td>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ log.person_name or
|
||||||
|
'System' }}</td>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
<span class="px-2 py-1 text-xs font-semibold rounded-full
|
||||||
|
{% if 'DELETE' in log.action %}bg-red-100 text-red-800
|
||||||
|
{% elif 'CREATE' in log.action or 'ADD' in log.action %}bg-green-100 text-green-800
|
||||||
|
{% elif 'UPDATE' in log.action %}bg-blue-100 text-blue-800
|
||||||
|
{% else %}bg-gray-100 text-gray-800{% endif %}">
|
||||||
|
{{ log.action }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="p-4 text-sm text-gray-600">{{ log.details }}</td>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm text-gray-400">
|
||||||
|
<div class="font-mono text-gray-500">{{ log.ip_address }}</div>
|
||||||
|
<div class="text-xs truncate max-w-[150px] text-gray-400" title="{{ log.user_agent }}">
|
||||||
|
{{ log.user_agent or 'Unknown Source' }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if has_more %}
|
||||||
|
<tr id="load-more-row">
|
||||||
|
<td colspan="5" class="p-4 text-center">
|
||||||
|
<button hx-get="/settings/activity_logs?offset={{ offset + limit }}"
|
||||||
|
hx-target="#load-more-row" hx-swap="outerHTML"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-cyan-700 bg-cyan-100 hover:bg-cyan-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 transition-colors">
|
||||||
|
Load More...
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if offset == 0 %}
|
||||||
|
{% if not logs %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="p-8 text-center text-gray-500 italic">No activity logs found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<input type="radio" name="settings_tabs" id="radio-users" class="peer/users hidden" checked>
|
<input type="radio" name="settings_tabs" id="radio-users" class="peer/users hidden" checked>
|
||||||
<input type="radio" name="settings_tabs" id="radio-exercises" class="peer/exercises hidden">
|
<input type="radio" name="settings_tabs" id="radio-exercises" class="peer/exercises hidden">
|
||||||
<input type="radio" name="settings_tabs" id="radio-export" class="peer/export hidden">
|
<input type="radio" name="settings_tabs" id="radio-export" class="peer/export hidden">
|
||||||
|
<input type="radio" name="settings_tabs" id="radio-activity" class="peer/activity hidden">
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation -->
|
||||||
<div class="border-b border-gray-200 mb-6 bg-gray-50 z-10">
|
<div class="border-b border-gray-200 mb-6 bg-gray-50 z-10">
|
||||||
@@ -50,6 +51,20 @@
|
|||||||
Data & Export
|
Data & Export
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="mr-2">
|
||||||
|
<label for="radio-activity" hx-get="/settings/activity_logs" hx-target="#activity-logs-container"
|
||||||
|
hx-trigger="click"
|
||||||
|
class="inline-flex items-center justify-center p-4 border-b-2 rounded-t-lg group cursor-pointer transition-colors
|
||||||
|
peer-checked/activity:border-cyan-600 peer-checked/activity:text-cyan-600 border-transparent hover:text-gray-700 hover:border-gray-300">
|
||||||
|
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Activity
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -303,6 +318,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Activity Tab Content -->
|
||||||
|
<div class="hidden peer-checked/activity:block">
|
||||||
|
<div class="bg-white shadow rounded-lg p-4 sm:p-6 lg:p-8 mb-8">
|
||||||
|
<div class="mb-6 border-b border-gray-100 pb-4">
|
||||||
|
<h3 class="text-xl font-bold text-gray-900">Activity Logs</h3>
|
||||||
|
<p class="text-sm text-gray-500">Review recent actions and administrative changes.</p>
|
||||||
|
</div>
|
||||||
|
<div id="activity-logs-container">
|
||||||
|
<div class="flex justify-center p-12">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-cyan-600 mb-4"></div>
|
||||||
|
<p class="text-sm text-gray-500">Loading activity history...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user