added usage limit visuals and DB

This commit is contained in:
2025-06-05 11:40:52 +02:00
parent 97fde3388b
commit a78f3c0786
9 changed files with 212 additions and 66 deletions

Binary file not shown.

View File

@@ -0,0 +1,36 @@
"""add docupulse settings table
Revision ID: add_docupulse_settings
Revises: add_notifs_table
Create Date: 2024-03-19 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from datetime import datetime
# revision identifiers, used by Alembic.
revision = 'add_docupulse_settings'
down_revision = 'add_notifs_table'
branch_labels = None
depends_on = None
def upgrade():
# Create docupulse_settings table
op.create_table('docupulse_settings',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('max_rooms', sa.Integer(), nullable=False, server_default='10'),
sa.Column('max_conversations', sa.Integer(), nullable=False, server_default='10'),
sa.Column('max_storage', sa.Integer(), nullable=False, server_default='10737418240'), # 10GB in bytes
sa.Column('updated_at', sa.DateTime(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
sa.PrimaryKeyConstraint('id')
)
# Insert default settings
op.execute("""
INSERT INTO docupulse_settings (id, max_rooms, max_conversations, max_storage, updated_at)
VALUES (1, 10, 10, 10737418240, CURRENT_TIMESTAMP)
""")
def downgrade():
op.drop_table('docupulse_settings')

View File

@@ -165,6 +165,46 @@ class SiteSettings(db.Model):
db.session.commit() db.session.commit()
return settings return settings
class DocuPulseSettings(db.Model):
__tablename__ = 'docupulse_settings'
id = db.Column(db.Integer, primary_key=True)
max_rooms = db.Column(db.Integer, default=10)
max_conversations = db.Column(db.Integer, default=10)
max_storage = db.Column(db.Integer, default=10737418240) # 10GB in bytes
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@classmethod
def get_settings(cls):
settings = cls.query.first()
if not settings:
settings = cls(
max_rooms=10,
max_conversations=10,
max_storage=10737418240 # 10GB in bytes
)
db.session.add(settings)
db.session.commit()
return settings
@classmethod
def get_usage_stats(cls):
settings = cls.get_settings()
total_rooms = Room.query.count()
total_conversations = Conversation.query.count()
total_storage = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.deleted == False).scalar() or 0
return {
'max_rooms': settings.max_rooms,
'max_conversations': settings.max_conversations,
'max_storage': settings.max_storage,
'current_rooms': total_rooms,
'current_conversations': total_conversations,
'current_storage': total_storage,
'rooms_percentage': (total_rooms / settings.max_rooms) * 100 if settings.max_rooms > 0 else 0,
'conversations_percentage': (total_conversations / settings.max_conversations) * 100 if settings.max_conversations > 0 else 0,
'storage_percentage': (total_storage / settings.max_storage) * 100 if settings.max_storage > 0 else 0
}
class KeyValueSettings(db.Model): class KeyValueSettings(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False) key = db.Column(db.String(100), unique=True, nullable=False)

View File

@@ -1,6 +1,6 @@
from flask import Blueprint, jsonify from flask import Blueprint, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from models import db, Room, RoomFile, User from models import db, Room, RoomFile, User, DocuPulseSettings
import os import os
from datetime import datetime from datetime import datetime
@@ -242,3 +242,15 @@ def cleanup_orphaned_records():
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@admin.route('/api/admin/usage-stats', methods=['GET'])
@login_required
def get_usage_stats():
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
stats = DocuPulseSettings.get_usage_stats()
return jsonify(stats)
except Exception as e:
return jsonify({'error': str(e)}), 500

View File

@@ -1,6 +1,6 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
from flask_login import current_user, login_required from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings, DocuPulseSettings
from routes.auth import require_password_change from routes.auth import require_password_change
import os import os
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@@ -79,6 +79,9 @@ def init_routes(main_bp):
) )
).order_by(Event.timestamp.desc()).limit(7).all() ).order_by(Event.timestamp.desc()).limit(7).all()
# Get usage stats
usage_stats = DocuPulseSettings.get_usage_stats()
# Room count and size logic # Room count and size logic
if current_user.is_admin: if current_user.is_admin:
logger.info("Loading admin dashboard...") logger.info("Loading admin dashboard...")
@@ -201,26 +204,16 @@ def init_routes(main_bp):
RoomFile.uploaded_at.desc() RoomFile.uploaded_at.desc()
).limit(10).all() ).limit(10).all()
logger.info(f"Recent activity query results (non-admin): {len(recent_activity)}")
if len(recent_activity) == 0:
# Debug query to see what files exist
all_files = RoomFile.query.filter(
RoomFile.room_id.in_(room_ids),
RoomFile.deleted == False
).all()
logger.info(f"Total non-deleted files in accessible rooms: {len(all_files)}")
for file in all_files[:5]: # Log first 5 files for debugging
logger.info(f"File: {file.name}, Uploaded: {file.uploaded_at}, Type: {file.type}")
# Format the activity data # Format the activity data
formatted_activity = [] formatted_activity = []
user_perms = {p.room_id: p for p in RoomMemberPermission.query.filter(
RoomMemberPermission.room_id.in_(room_ids),
RoomMemberPermission.user_id==current_user.id
).all()}
for file, room, user in recent_activity: for file, room, user in recent_activity:
perm = user_perms.get(room.id) # Check if user has download permission
permission = RoomMemberPermission.query.filter_by(
room_id=room.id,
user_id=current_user.id
).first()
can_download = permission and permission.can_download if permission else False
activity = { activity = {
'name': file.name, 'name': file.name,
'type': file.type, 'type': file.type,
@@ -229,11 +222,12 @@ def init_routes(main_bp):
'uploaded_at': file.uploaded_at, 'uploaded_at': file.uploaded_at,
'is_starred': current_user in file.starred_by, 'is_starred': current_user in file.starred_by,
'is_deleted': file.deleted, 'is_deleted': file.deleted,
'can_download': perm.can_download if perm else False 'can_download': can_download
} }
formatted_activity.append(activity) formatted_activity.append(activity)
formatted_activities = formatted_activity formatted_activities = formatted_activity
# Get storage usage by file type for accessible rooms including trash
# Get storage usage by file type for accessible rooms
storage_by_type = db.session.query( storage_by_type = db.session.query(
case( case(
(RoomFile.name.like('%.%'), (RoomFile.name.like('%.%'),
@@ -249,16 +243,13 @@ def init_routes(main_bp):
# Get trash and starred stats for user's accessible rooms # Get trash and starred stats for user's accessible rooms
trash_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).count() trash_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).count()
starred_count = RoomFile.query.filter( starred_count = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.starred_by.contains(current_user)).count()
RoomFile.room_id.in_(room_ids), # Get oldest trash date and total trash size for accessible rooms
RoomFile.starred_by.contains(current_user)
).count()
# Get oldest trash date and total trash size for user's rooms
oldest_trash = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).order_by(RoomFile.deleted_at.asc()).first() oldest_trash = RoomFile.query.filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).order_by(RoomFile.deleted_at.asc()).first()
oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash and oldest_trash.deleted_at else None oldest_trash_date = oldest_trash.deleted_at.strftime('%Y-%m-%d') if oldest_trash and oldest_trash.deleted_at else None
trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).scalar() or 0 trash_size = db.session.query(db.func.sum(RoomFile.size)).filter(RoomFile.room_id.in_(room_ids), RoomFile.deleted==True).scalar() or 0
# Get files that will be deleted in next 7 days for user's rooms # Get files that will be deleted in next 7 days for accessible rooms
seven_days_from_now = datetime.utcnow() + timedelta(days=7) seven_days_from_now = datetime.utcnow() + timedelta(days=7)
thirty_days_ago = datetime.utcnow() - timedelta(days=30) thirty_days_ago = datetime.utcnow() - timedelta(days=30)
pending_deletion = RoomFile.query.filter( pending_deletion = RoomFile.query.filter(
@@ -268,7 +259,7 @@ def init_routes(main_bp):
RoomFile.deleted_at > thirty_days_ago - timedelta(days=7) RoomFile.deleted_at > thirty_days_ago - timedelta(days=7)
).count() ).count()
# Get trash file type breakdown for user's rooms # Get trash file type breakdown for accessible rooms
trash_by_type = db.session.query( trash_by_type = db.session.query(
case( case(
(RoomFile.name.like('%.%'), (RoomFile.name.like('%.%'),
@@ -281,44 +272,40 @@ def init_routes(main_bp):
RoomFile.deleted==True RoomFile.deleted==True
).group_by('extension').all() ).group_by('extension').all()
# Get conversation statistics # Get conversation stats
if current_user.is_admin: conversation_count = Conversation.query.count()
conversation_count = Conversation.query.count() message_count = Message.query.count()
message_count = Message.query.count() attachment_count = MessageAttachment.query.count()
attachment_count = MessageAttachment.query.count() conversation_total_size = db.session.query(func.sum(MessageAttachment.size)).scalar() or 0
conversation_total_size = db.session.query(db.func.sum(MessageAttachment.size)).scalar() or 0 recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all()
recent_conversations = Conversation.query.order_by(Conversation.created_at.desc()).limit(5).all()
else:
# For regular users, only show their conversations
conversation_count = Conversation.query.filter(Conversation.members.any(id=current_user.id)).count()
message_count = Message.query.join(Conversation).filter(Conversation.members.any(id=current_user.id)).count()
attachment_count = MessageAttachment.query.join(Message).join(Conversation).filter(Conversation.members.any(id=current_user.id)).count()
conversation_total_size = db.session.query(db.func.sum(MessageAttachment.size)).join(Message).join(Conversation).filter(Conversation.members.any(id=current_user.id)).scalar() or 0
recent_conversations = Conversation.query.filter(Conversation.members.any(id=current_user.id)).order_by(Conversation.created_at.desc()).limit(5).all()
return render_template('dashboard/dashboard.html', return render_template('dashboard/dashboard.html',
recent_contacts=recent_contacts, room_count=room_count,
active_count=active_count, file_count=file_count,
inactive_count=inactive_count, folder_count=folder_count,
room_count=room_count, total_size=total_size,
file_count=file_count, storage_by_type=storage_by_type,
folder_count=folder_count, recent_activities=formatted_activities,
total_size=total_size, # Room storage size trash_count=trash_count,
storage_by_type=storage_by_type, pending_deletion=pending_deletion,
trash_count=trash_count, oldest_trash_date=oldest_trash_date,
starred_count=starred_count, trash_size=trash_size,
oldest_trash_date=oldest_trash_date, trash_by_type=trash_by_type,
trash_size=trash_size, starred_count=starred_count,
pending_deletion=pending_deletion, recent_events=recent_events,
trash_by_type=trash_by_type, recent_contacts=recent_contacts,
recent_events=recent_events, active_count=active_count,
is_admin=current_user.is_admin, inactive_count=inactive_count,
conversation_count=conversation_count, recent_notifications=recent_notifications,
message_count=message_count, unread_notifications=get_unread_count(current_user.id),
attachment_count=attachment_count, conversation_count=conversation_count,
conversation_total_size=conversation_total_size, # Conversation storage size message_count=message_count,
recent_conversations=recent_conversations, attachment_count=attachment_count,
recent_notifications=recent_notifications) conversation_total_size=conversation_total_size,
recent_conversations=recent_conversations,
usage_stats=usage_stats,
is_admin=current_user.is_admin
)
UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics') UPLOAD_FOLDER = os.path.join(os.getcwd(), 'uploads', 'profile_pics')
if not os.path.exists(UPLOAD_FOLDER): if not os.path.exists(UPLOAD_FOLDER):

View File

@@ -0,0 +1,67 @@
{% from 'common/macros.html' import format_size %}
{% macro usage_limits(usage_stats) %}
<div class="masonry-card" style="width: 100%;">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0"><i class="fas fa-chart-line me-2"></i>Usage Limits</h5>
</div>
<!-- Rooms Progress -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<i class="fas fa-door-open me-2 icon-primary"></i>
<span class="text-muted">Rooms:</span>
</div>
<div class="fw-bold text-primary">{{ usage_stats.current_rooms }} / {{ usage_stats.max_rooms }}</div>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ usage_stats.rooms_percentage }}%; background-color: var(--secondary-color);"
aria-valuenow="{{ usage_stats.rooms_percentage }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
<!-- Conversations Progress -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<i class="fas fa-comments me-2 icon-primary"></i>
<span class="text-muted">Conversations:</span>
</div>
<div class="fw-bold text-primary">{{ usage_stats.current_conversations }} / {{ usage_stats.max_conversations }}</div>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ usage_stats.conversations_percentage }}%; background-color: var(--secondary-color);"
aria-valuenow="{{ usage_stats.conversations_percentage }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
<!-- Storage Progress -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="d-flex align-items-center">
<i class="fas fa-hdd me-2 icon-primary"></i>
<span class="text-muted">Storage:</span>
</div>
<div class="fw-bold text-primary">{{ format_size(usage_stats.current_storage) }} / {{ format_size(usage_stats.max_storage) }}</div>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar" role="progressbar"
style="width: {{ usage_stats.storage_percentage }}%; background-color: var(--secondary-color);"
aria-valuenow="{{ usage_stats.storage_percentage }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -26,6 +26,7 @@
{% from 'components/recent_activity.html' import recent_activity %} {% from 'components/recent_activity.html' import recent_activity %}
{% from 'components/conversation_storage.html' import conversation_storage %} {% from 'components/conversation_storage.html' import conversation_storage %}
{% from 'components/notification_overview.html' import notification_overview %} {% from 'components/notification_overview.html' import notification_overview %}
{% from 'components/usage_limits.html' import usage_limits %}
<style> <style>
.masonry { .masonry {
@@ -54,6 +55,9 @@
<!-- Storage Section --> <!-- Storage Section -->
<div class="mb-4"> <div class="mb-4">
<h2 class="section-title">Storage Overview</h2> <h2 class="section-title">Storage Overview</h2>
{% if current_user.is_admin %}
{{ usage_limits(usage_stats) }}
{% endif %}
<div class="masonry"> <div class="masonry">
{{ storage_overview(room_count, file_count, folder_count, total_size) }} {{ storage_overview(room_count, file_count, folder_count, total_size) }}
{{ storage_usage(storage_by_type) }} {{ storage_usage(storage_by_type) }}