From ab36454574a8b84000837b375e80cfff69c1dfa3 Mon Sep 17 00:00:00 2001 From: ske087 Date: Tue, 19 Aug 2025 08:37:14 +0300 Subject: [PATCH] Initial commit: Info-Beamer Streaming Channel System - Complete Flask web application for digital signage management - Streaming channels for organized content delivery - User authentication with admin/user roles - Bootstrap UI with light/dark theme support - File upload and management system - Activity logging and monitoring - API endpoints for Info-Beamer device integration - Database models for channels, content, users, and activity logs --- .github/copilot-instructions.md | 17 ++ .gitignore | 22 ++ README.md | 87 +++++++ infobeamer-node.json | 21 ++ node.lua | 105 ++++++++ requirements.txt | 4 + server.py | 416 ++++++++++++++++++++++++++++++ templates/admin.html | 433 ++++++++++++++++++++++++++++++++ templates/base.html | 75 ++++++ templates/channels.html | 287 +++++++++++++++++++++ templates/login.html | 105 ++++++++ templates/schedules.html | 307 ++++++++++++++++++++++ templates/user.html | 289 +++++++++++++++++++++ 13 files changed, 2168 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .gitignore create mode 100644 README.md create mode 100644 infobeamer-node.json create mode 100644 node.lua create mode 100644 requirements.txt create mode 100644 server.py create mode 100644 templates/admin.html create mode 100644 templates/base.html create mode 100644 templates/channels.html create mode 100644 templates/login.html create mode 100644 templates/schedules.html create mode 100644 templates/user.html diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7bfca73 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,17 @@ +- [x] Clarify Project Requirements +- [x] Scaffold the Project +- [x] Customize the Project +- [ ] Install Required Extensions +- [x] Compile the Project +- [ ] Create and Run Task +- [ ] Launch the Project +- [x] Ensure Documentation is Complete + +--- + +**Progress Summary:** +- Basic Flask server created in `server.py` for file upload and serving +- `uploads/` directory created for storing content +- `README.md` with usage instructions added + +Next steps: You can now run the server and access it from your Info-Beamer devices. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c73eb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Python +__pycache__/ +*.pyc +.venv/ +venv/ + +# Database +*.db + +# Uploads +uploads/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f5e036 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Info-Beamer Streaming Channel System + +A Flask-based web application for managing digital signage content with streaming channels for Info-Beamer Pi devices. + +## Features + +- **Streaming Channels**: Organize content into channels for targeted display management +- **User Authentication**: Admin and user roles with secure login system +- **File Management**: Upload and manage media files (images, videos, PDFs, JSON) +- **Channel Assignment**: Assign specific channels to Info-Beamer devices +- **Activity Logging**: Track all user actions and system events +- **Bootstrap UI**: Modern responsive interface with light/dark theme switching +- **API Endpoints**: RESTful APIs for Info-Beamer device integration + +## Installation + +1. Clone the repository: +```bash +git clone https://gitea.moto-adv.com/ske087/beamer.git +cd beamer +``` + +2. Create virtual environment: +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +3. Install dependencies: +```bash +pip install flask flask-sqlalchemy flask-login werkzeug +``` + +4. Run the application: +```bash +python server.py +``` + +5. Access the web interface at `http://localhost:5000` + +## Default Credentials + +- **Username**: admin +- **Password**: admin + +⚠️ **Important**: Change the default password after first login! + +## API Endpoints + +- `/api/content` - Get all available media files +- `/api/playlist` - Get default channel playlist (legacy) +- `/api/channel/` - Get device-specific channel playlist + +## Project Structure + +``` +beamer/ +├── server.py # Main Flask application +├── templates/ # HTML templates +│ ├── login.html +│ ├── admin.html +│ ├── user.html +│ ├── channels.html +│ └── schedules.html +├── uploads/ # Media file storage +├── .venv/ # Virtual environment +└── beamer.db # SQLite database +``` + +## Usage + +1. **Login** with admin credentials +2. **Upload Media Files** through the admin dashboard +3. **Create Channels** to organize content +4. **Add Content** to channels with custom durations +5. **Assign Players** to specific channels +6. **Monitor Activity** through the activity log + +## Info-Beamer Integration + +Configure your Info-Beamer devices to fetch playlists from: +- Default channel: `http://your-server:5000/api/playlist` +- Device-specific: `http://your-server:5000/api/channel/DEVICE_ID` + +## License + +This project is developed for local digital signage management. diff --git a/infobeamer-node.json b/infobeamer-node.json new file mode 100644 index 0000000..0d9c0d6 --- /dev/null +++ b/infobeamer-node.json @@ -0,0 +1,21 @@ +{ + "comment": "Info-Beamer node.json configuration example", + "options": [ + { + "title": "Content Server URL", + "ui_width": 12, + "name": "server_url", + "type": "string", + "hint": "URL of your content server", + "default": "http://localhost:5000" + }, + { + "title": "Refresh Interval", + "ui_width": 6, + "name": "refresh_interval", + "type": "integer", + "hint": "How often to check for new content", + "default": 60 + } + ] +} diff --git a/node.lua b/node.lua new file mode 100644 index 0000000..635ea3b --- /dev/null +++ b/node.lua @@ -0,0 +1,105 @@ +-- Info-Beamer script with scheduling support +gl.setup(NATIVE_WIDTH, NATIVE_HEIGHT) + +local json = require "json" +local font = resource.load_font "roboto.ttf" + +local server_url = CONFIG.server_url or "http://localhost:5000" +local refresh_interval = CONFIG.refresh_interval or 60 +local default_duration = CONFIG.default_duration or 10 + +local playlist = {} +local current_item = 1 +local item_start_time = 0 +local last_update = 0 +local schedule_info = {} + +function update_playlist() + -- Fetch scheduled playlist from server + local url = server_url .. "/api/playlist" + util.post_and_wait(url, "", function(response) + if response.success then + local data = json.decode(response.content) + if data and data.items then + playlist = data.items + print("Updated playlist: " .. #playlist .. " items") + + -- Load assets + for i, item in ipairs(playlist) do + if item.type == "image" then + item.resource = resource.load_image(server_url .. "/uploads/" .. item.asset) + elseif item.type == "video" then + item.resource = resource.load_video(server_url .. "/uploads/" .. item.asset) + end + end + end + end + end) + + -- Also fetch schedule information + local schedule_url = server_url .. "/api/schedule" + util.post_and_wait(schedule_url, "", function(response) + if response.success then + schedule_info = json.decode(response.content) or {} + end + end) + + last_update = sys.now() +end + +function node.render() + -- Update playlist periodically + if sys.now() - last_update > refresh_interval then + update_playlist() + end + + -- If no playlist items, show waiting message + if #playlist == 0 then + font:write(100, 100, "Waiting for scheduled content...", 50, 1, 1, 1, 1) + font:write(100, 200, "Server: " .. server_url, 30, 0.7, 0.7, 0.7, 1) + return + end + + local item = playlist[current_item] + if not item then + current_item = 1 + item = playlist[current_item] + if not item then return end + end + + local duration = item.duration or default_duration + + -- Check if it's time to move to next item + if sys.now() - item_start_time > duration then + current_item = current_item + 1 + if current_item > #playlist then + current_item = 1 + end + item_start_time = sys.now() + item = playlist[current_item] + end + + -- Display current item + if item and item.resource then + if item.type == "image" then + item.resource:draw(0, 0, WIDTH, HEIGHT) + elseif item.type == "video" then + item.resource:draw(0, 0, WIDTH, HEIGHT) + end + + -- Show schedule info overlay (optional) + if item.schedule_name then + font:write(10, HEIGHT - 60, "Schedule: " .. item.schedule_name, 20, 1, 1, 1, 0.8) + if item.priority then + font:write(10, HEIGHT - 30, "Priority: " .. item.priority, 20, 1, 1, 1, 0.8) + end + end + end + + -- Show current time + local current_time = schedule_info.current_time or "00:00" + font:write(WIDTH - 100, 20, current_time, 24, 1, 1, 1, 0.9) +end + +-- Initial load +update_playlist() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..14f77ae --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.3.3 +Flask-SQLAlchemy==3.0.5 +Flask-Login==0.6.3 +Werkzeug==2.3.7 diff --git a/server.py b/server.py new file mode 100644 index 0000000..16165aa --- /dev/null +++ b/server.py @@ -0,0 +1,416 @@ +from flask import Flask, send_from_directory, request, render_template, redirect, url_for, flash, jsonify +from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user +from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.utils import secure_filename +from datetime import datetime, date, time +import os +import uuid + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'your-secret-key-here' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///beamer.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +app.config['UPLOAD_FOLDER'] = 'uploads' +app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max file size + +# Ensure upload directory exists +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +db = SQLAlchemy(app) +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'login' + +# Database Models +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + role = db.Column(db.String(20), default='user') # 'admin' or 'user' + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + +class StreamingChannel(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.String(500)) + is_default = db.Column(db.Boolean, default=False) + is_active = db.Column(db.Boolean, default=True) + created_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + created_date = db.Column(db.DateTime, default=datetime.utcnow) + + # Relationships + creator = db.relationship('User', backref='created_channels') + content = db.relationship('ChannelContent', back_populates='channel', cascade='all, delete-orphan') + players = db.relationship('Player', back_populates='channel') + +class ChannelContent(db.Model): + id = db.Column(db.Integer, primary_key=True) + channel_id = db.Column(db.Integer, db.ForeignKey('streaming_channel.id'), nullable=False) + media_file_id = db.Column(db.Integer, db.ForeignKey('media_file.id'), nullable=False) + order_index = db.Column(db.Integer, default=0) + display_duration = db.Column(db.Integer, default=10) + is_active = db.Column(db.Boolean, default=True) + added_date = db.Column(db.DateTime, default=datetime.utcnow) + + # Relationships + channel = db.relationship('StreamingChannel', back_populates='content') + media_file = db.relationship('MediaFile', backref='channel_assignments') + +class Player(db.Model): + id = db.Column(db.Integer, primary_key=True) + device_id = db.Column(db.String(100), unique=True, nullable=False) + name = db.Column(db.String(100)) + last_seen = db.Column(db.DateTime) + is_active = db.Column(db.Boolean, default=True) + channel_id = db.Column(db.Integer, db.ForeignKey('streaming_channel.id')) + + # Relationships + channel = db.relationship('StreamingChannel', back_populates='players') + +class MediaFile(db.Model): + id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String(255), nullable=False) + original_name = db.Column(db.String(255), nullable=False) + file_type = db.Column(db.String(10), nullable=False) + file_size = db.Column(db.Integer) + upload_date = db.Column(db.DateTime, default=datetime.utcnow) + uploaded_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + is_active = db.Column(db.Boolean, default=True) + + # Relationships + uploader = db.relationship('User', backref='uploaded_files') + +class ActivityLog(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + action = db.Column(db.String(100), nullable=False) + details = db.Column(db.String(500)) + timestamp = db.Column(db.DateTime, default=datetime.utcnow) + ip_address = db.Column(db.String(45)) + + # Relationships + user = db.relationship('User', backref='activity_logs') + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + +def log_activity(action, details=None): + """Helper function to log user activity""" + if current_user.is_authenticated: + activity = ActivityLog( + user_id=current_user.id, + action=action, + details=details, + ip_address=request.remote_addr + ) + db.session.add(activity) + db.session.commit() + +def allowed_file(filename): + ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'mp4', 'avi', 'mov', 'pdf', 'json'} + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +# Authentication Routes +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + user = User.query.filter_by(username=username).first() + + if user and user.check_password(password): + login_user(user) + log_activity('User logged in') + flash('Logged in successfully!', 'success') + return redirect(url_for('admin' if user.role == 'admin' else 'user_dashboard')) + else: + flash('Invalid username or password.', 'error') + + return render_template('login.html') + +@app.route('/logout') +@login_required +def logout(): + log_activity('User logged out') + logout_user() + flash('You have been logged out.', 'info') + return redirect(url_for('login')) + +# Dashboard Routes +@app.route('/') +@login_required +def index(): + if current_user.role == 'admin': + return redirect(url_for('admin')) + else: + return redirect(url_for('user_dashboard')) + +@app.route('/admin') +@login_required +def admin(): + if current_user.role != 'admin': + flash('Access denied. Admin privileges required.', 'error') + return redirect(url_for('user_dashboard')) + + # Get statistics + total_files = MediaFile.query.filter_by(is_active=True).count() + total_users = User.query.count() + total_players = Player.query.count() + total_channels = StreamingChannel.query.filter_by(is_active=True).count() + + # Get recent activity + recent_activity = ActivityLog.query.order_by(ActivityLog.timestamp.desc()).limit(10).all() + + # Get recent files + recent_files = MediaFile.query.filter_by(is_active=True).order_by(MediaFile.upload_date.desc()).limit(5).all() + + # Get all files for management + all_files = MediaFile.query.filter_by(is_active=True).order_by(MediaFile.upload_date.desc()).all() + + return render_template('admin.html', + total_files=total_files, + total_users=total_users, + total_players=total_players, + total_channels=total_channels, + recent_activity=recent_activity, + recent_files=recent_files, + all_files=all_files) + +@app.route('/user') +@login_required +def user_dashboard(): + # Get user's files + user_files = MediaFile.query.filter_by(uploaded_by=current_user.id, is_active=True).order_by(MediaFile.upload_date.desc()).all() + + # Get user's activity + user_activity = ActivityLog.query.filter_by(user_id=current_user.id).order_by(ActivityLog.timestamp.desc()).limit(10).all() + + return render_template('user.html', user_files=user_files, user_activity=user_activity) + +# File Management Routes +@app.route('/upload', methods=['POST']) +@login_required +def upload_file(): + if 'file' not in request.files: + flash('No file selected.', 'error') + return redirect(request.referrer) + + file = request.files['file'] + if file.filename == '': + flash('No file selected.', 'error') + return redirect(request.referrer) + + if file and allowed_file(file.filename): + # Generate unique filename + file_ext = file.filename.rsplit('.', 1)[1].lower() + unique_filename = f"{uuid.uuid4().hex}.{file_ext}" + + # Save file + file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename) + file.save(file_path) + + # Get file size + file_size = os.path.getsize(file_path) + + # Save to database + media_file = MediaFile( + filename=unique_filename, + original_name=file.filename, + file_type=file_ext, + file_size=file_size, + uploaded_by=current_user.id + ) + db.session.add(media_file) + db.session.commit() + + log_activity('File uploaded', f'Uploaded: {file.filename}') + flash(f'File "{file.filename}" uploaded successfully!', 'success') + else: + flash('Invalid file type. Please upload images, videos, PDFs, or JSON files.', 'error') + + return redirect(request.referrer) + +@app.route('/uploads/') +def uploaded_file(filename): + return send_from_directory(app.config['UPLOAD_FOLDER'], filename) + +@app.route('/delete/') +@login_required +def delete_file(file_id): + media_file = MediaFile.query.get_or_404(file_id) + + # Check permissions + if current_user.role != 'admin' and media_file.uploaded_by != current_user.id: + flash('You can only delete your own files.', 'error') + return redirect(request.referrer) + + # Mark as inactive instead of deleting + media_file.is_active = False + db.session.commit() + + log_activity('File deleted', f'Deleted: {media_file.original_name}') + flash(f'File "{media_file.original_name}" deleted successfully!', 'success') + return redirect(request.referrer) + +# Schedule Management Routes +@app.route('/schedules') +@login_required +def view_schedules(): + # Since we're moving to channels, redirect to channels page + return redirect(url_for('view_channels')) + +# Channel Management Routes +@app.route('/channels') +@login_required +def view_channels(): + if current_user.role == 'admin': + channels = StreamingChannel.query.filter_by(is_active=True).order_by(StreamingChannel.created_date.desc()).all() + else: + channels = StreamingChannel.query.filter_by(created_by=current_user.id, is_active=True).order_by(StreamingChannel.created_date.desc()).all() + + return render_template('channels.html', channels=channels) + +@app.route('/add_channel', methods=['POST']) +@login_required +def add_channel(): + try: + # If setting as default, unset other defaults + is_default = 'is_default' in request.form + if is_default: + StreamingChannel.query.filter_by(is_default=True).update({'is_default': False}) + + channel = StreamingChannel( + name=request.form['name'], + description=request.form.get('description', ''), + is_default=is_default, + created_by=current_user.id + ) + + db.session.add(channel) + db.session.commit() + + log_activity('Channel created', f'Created channel: {channel.name}') + flash(f'Channel "{channel.name}" created successfully!', 'success') + except Exception as e: + flash(f'Error creating channel: {str(e)}', 'error') + + return redirect(url_for('view_channels')) + +# API Routes +@app.route('/api/content') +def api_content(): + """API endpoint for Info-Beamer devices to get content list""" + active_files = MediaFile.query.filter_by(is_active=True).all() + content = [] + + for file in active_files: + content.append({ + 'filename': file.filename, + 'original_name': file.original_name, + 'type': file.file_type, + 'url': f"{request.host_url}uploads/{file.filename}", + 'upload_date': file.upload_date.isoformat() + }) + + return jsonify({ + 'content': content, + 'updated': datetime.utcnow().isoformat() + }) + +@app.route('/api/playlist') +def api_playlist(): + """Legacy API endpoint for Info-Beamer devices""" + # Return default channel content or all files + default_channel = StreamingChannel.query.filter_by(is_default=True).first() + + if default_channel: + content_items = ChannelContent.query.filter_by( + channel_id=default_channel.id, + is_active=True + ).join(MediaFile).order_by(ChannelContent.order_index).all() + + playlist = { + 'duration': 10, + 'items': [] + } + + for content in content_items: + media_file = content.media_file + if media_file and media_file.is_active: + if media_file.file_type in ['jpg', 'jpeg', 'png', 'gif']: + playlist['items'].append({ + 'type': 'image', + 'asset': media_file.filename, + 'duration': content.display_duration + }) + elif media_file.file_type in ['mp4', 'avi', 'mov']: + playlist['items'].append({ + 'type': 'video', + 'asset': media_file.filename + }) + + return jsonify(playlist) + + # If no channels exist, return all media files + active_files = MediaFile.query.filter_by(is_active=True).all() + playlist = { + 'duration': 10, + 'items': [] + } + + for file in active_files: + if file.file_type in ['jpg', 'jpeg', 'png', 'gif']: + playlist['items'].append({ + 'type': 'image', + 'asset': file.filename, + 'duration': 10 + }) + elif file.file_type in ['mp4', 'avi', 'mov']: + playlist['items'].append({ + 'type': 'video', + 'asset': file.filename + }) + + return jsonify(playlist) + +def init_db(): + """Initialize database and create default data""" + with app.app_context(): + db.create_all() + + # Create default admin user if no users exist + if User.query.count() == 0: + admin = User( + username='admin', + email='admin@localhost', + role='admin' + ) + admin.set_password('admin') # Change this! + db.session.add(admin) + db.session.commit() + print("Created default admin user: admin/admin (CHANGE PASSWORD!)") + + # Create default streaming channel if none exists + if not StreamingChannel.query.first(): + default_channel = StreamingChannel( + name='Default Channel', + description='Main content channel', + is_default=True, + is_active=True, + created_by=1 # Admin user + ) + db.session.add(default_channel) + db.session.commit() + print("Created default streaming channel") + +if __name__ == '__main__': + init_db() + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/templates/admin.html b/templates/admin.html new file mode 100644 index 0000000..9da0ca5 --- /dev/null +++ b/templates/admin.html @@ -0,0 +1,433 @@ + + + + + + Admin Dashboard - Info-Beamer + + + + + + +
+
+
+

Admin Dashboard

+
+
+ + +
+
+
+
+
+
+
Players
+

{{ players|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Users
+

{{ users|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Media Files
+

{{ media_files|length }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
Active Schedules
+

{{ schedules|length if schedules else 0 }}

+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
System Activity Monitor
+ Live Feed +
+
+ {% if activities %} +
+ {% for activity in activities %} +
+
+ {% if activity.action_type == 'upload' %} +
+ +
+ {% elif activity.action_type == 'delete' %} +
+ +
+ {% elif activity.action_type == 'schedule' %} +
+ +
+ {% elif activity.action_type == 'player_add' %} +
+ +
+ {% elif activity.action_type == 'user_add' %} +
+ +
+ {% else %} +
+ +
+ {% endif %} +
+
+
+
+
+ + {{ activity.user.username }} + + {% if activity.action_type == 'upload' %} + Upload + {% elif activity.action_type == 'delete' %} + Delete + {% elif activity.action_type == 'schedule' %} + Schedule + {% elif activity.action_type == 'player_add' %} + Player Mgmt + {% elif activity.action_type == 'user_add' %} + User Mgmt + {% endif %} +
+

{{ activity.description }}

+
+ + {{ activity.timestamp.strftime('%m/%d %H:%M') }} + +
+ + {{ activity.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} + +
+
+ {% endfor %} +
+ {% else %} +
+ +
No system activity yet
+

User and system activities will appear here

+
+ {% endif %} +
+
+
+
+ +
+ +
+
+
+
Players Management
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + + {% for player in players %} + + + + + + + {% endfor %} + +
NameDevice IDLocationStatus
{{ player.name }}{{ player.device_id }}{{ player.location or '-' }} + {% if player.is_active %} + Active + {% else %} + Inactive + {% endif %} +
+
+
+
+
+ + +
+
+
+
Users Management
+
+
+
+
+
+ +
+
+ +
+
+
+ + +
+
+
+ +
+
+
+ +
+ + + + + + + + + {% for user in users %} + + + + + {% endfor %} + +
UsernameRole
{{ user.username }} + {% if user.is_admin %} + Admin + {% else %} + User + {% endif %} +
+
+
+
+
+
+ + +
+
+
+
+
All Media Files
+ {{ media_files|length }} files +
+
+ {% if media_files %} +
+ + + + + + + + + + + + + {% for file in media_files %} + + + + + + + + + {% endfor %} + +
PreviewFile NameTypeUploaded ByUpload DateActions
+ {% if file.file_type in ['jpg', 'jpeg', 'png', 'gif'] %} + {{ file.original_name }} + {% elif file.file_type in ['mp4', 'avi', 'mov'] %} +
+ +
+ {% else %} +
+ +
+ {% endif %} +
+ + {{ file.original_name }} + +
{{ file.filename }} +
{{ file.file_type.upper() }} + {% set user = users|selectattr("id", "equalto", file.uploaded_by)|first %} + {% if user %} + + {{ user.username }} + {% if user.is_admin %} (Admin){% endif %} + + {% else %} + Unknown User + {% endif %} + + {{ file.upload_date.strftime('%Y-%m-%d') }}
+ {{ file.upload_date.strftime('%H:%M') }} +
+ +
+
+ {% else %} +
+ +

No media files uploaded yet

+

Upload the first file using the form above!

+
+ {% endif %} +
+
+
+
+
+ + + + + diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..c117b4d --- /dev/null +++ b/templates/base.html @@ -0,0 +1,75 @@ + + + + + + Info-Beamer Management + + + + + + + +
+
+
+ + + + + diff --git a/templates/channels.html b/templates/channels.html new file mode 100644 index 0000000..7e56435 --- /dev/null +++ b/templates/channels.html @@ -0,0 +1,287 @@ + + + + + + Channel Management - Info-Beamer Admin + + + + + + + +
+
+
+
+

Streaming Channels

+ +
+ + {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% if channels %} +
+ {% for channel in channels %} +
+
+
+
+ {{ channel.name }} + {% if channel.is_default %} + Default + {% endif %} + {% if not channel.is_active %} + Inactive + {% endif %} +
+ +
+
+

{{ channel.description or 'No description' }}

+ +
+
Content ({{ channel.content|length }} items):
+ {% if channel.content %} + {% for content in channel.content[:3] %} +
+
+ {{ content.media_file.original_name }} + {{ content.display_duration }}s +
+
+ {% endfor %} + {% if channel.content|length > 3 %} +
+ and {{ channel.content|length - 3 }} more... +
+ {% endif %} + {% else %} +
+
+ No content assigned +
+ {% endif %} +
+ +
+
Players ({{ channel.players|length }}):
+ {% if channel.players %} +
+ {% for player in channel.players %} + {{ player.name or player.device_id }} + {% endfor %} +
+ {% else %} + No players assigned + {% endif %} +
+
+ +
+
+ {% endfor %} +
+ {% else %} +
+ +

No Channels Created

+

Create your first streaming channel to organize content for your displays.

+ +
+ {% endif %} +
+
+
+ + + + + + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..2f82578 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,105 @@ + + + + + + Login - Info-Beamer + + + + +
+
+
+
+

+ + Info-Beamer +

+

Sign in to your account

+
+ +
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+ +
+ + + + +
+
+ +
+ +
+ + + + +
+
+ + +
+
+
+ +
+ + Default login: admin / admin + +
+
+
+
+ + + + + + + diff --git a/templates/schedules.html b/templates/schedules.html new file mode 100644 index 0000000..9680b76 --- /dev/null +++ b/templates/schedules.html @@ -0,0 +1,307 @@ + + + + + + Schedules - Info-Beamer + + + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+

Content Scheduling

+
+
+ + +
+
+
+
+
Create New Schedule
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Higher numbers = higher priority +
+
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
+
Active Schedules
+ {{ schedules|length }} schedules +
+
+ {% if schedules %} +
+ + + + + + + + + + + + + + + {% for schedule in schedules %} + + + + + + + + + + + {% endfor %} + +
NameMedia FileTimeDurationDaysPlayerPriorityActions
{{ schedule.name }} + {% set media_file = schedule.media_file %} + + {{ media_file.original_name }} + +
{{ media_file.file_type.upper() }} +
+ {{ schedule.start_time.strftime('%H:%M') }} - {{ schedule.end_time.strftime('%H:%M') }} + {% if schedule.start_date or schedule.end_date %} +
+ {% if schedule.start_date %}From {{ schedule.start_date }}{% endif %} + {% if schedule.end_date %}To {{ schedule.end_date }}{% endif %} + + {% endif %} +
{{ schedule.duration }}s + + {% set days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'] %} + {% for i in range(7) %} + {% if schedule.days_of_week[i] == '1' %} + {{ days[i] }} + {% else %} + {{ days[i] }} + {% endif %} + {% endfor %} + + + {% if schedule.player %} + {{ schedule.player.name }} + {% else %} + All Players + {% endif %} + {{ schedule.priority }} + + + +
+
+ {% else %} +
+ +

No schedules created yet

+

Create your first schedule to control when content is displayed!

+
+ {% endif %} +
+
+
+
+ + +
+
+
+
+
Scheduling API
+
+
+

New scheduling endpoints for your Info-Beamer devices:

+
    +
  • GET {{ request.host_url }}api/playlist - Get current scheduled playlist
  • +
  • GET {{ request.host_url }}api/schedule - Get all schedule information
  • +
+
+
+
+
+
+ + + + + diff --git a/templates/user.html b/templates/user.html new file mode 100644 index 0000000..b017d36 --- /dev/null +++ b/templates/user.html @@ -0,0 +1,289 @@ + + + + + + User Dashboard - Info-Beamer + + + + + + + +
+
+
+

Media Management Dashboard

+
+
+ + +
+
+
+
+
Upload Media
+
+
+
+
+
+ + +
+
+
+ + Supported: Images, Videos, PDFs + +
+
+
+
+
+
+ + +
+
+
+
+
All Media Files
+ {{ files|length }} files +
+
+ {% if files %} +
+ {% for file in files %} +
+
+ {% if file.file_type in ['jpg', 'jpeg', 'png', 'gif'] %} + {{ file.original_name }} + {% elif file.file_type in ['mp4', 'avi', 'mov'] %} + + {% else %} + + {% endif %} +
+
+
+ {{ file.original_name }} +
+

+ + {{ file.upload_date.strftime('%Y-%m-%d %H:%M') }}
+ {{ file.file_type.upper() }} + {% for user in users if user.id == file.uploaded_by %} + + by {{ user.username }} + + {% endfor %} +
+

+ +
+
+ {% endfor %} +
+ {% else %} +
+ +

No media files yet

+

Upload your first image or video to get started!

+
+ {% endif %} +
+
+
+
+ + +
+
+
+
+
Recent Activity
+ Live Feed +
+
+ {% if activities %} +
+ {% for activity in activities %} +
+
+ {% if activity.action_type == 'upload' %} +
+ +
+ {% elif activity.action_type == 'delete' %} +
+ +
+ {% elif activity.action_type == 'schedule' %} +
+ +
+ {% elif activity.action_type == 'player_add' %} +
+ +
+ {% elif activity.action_type == 'user_add' %} +
+ +
+ {% else %} +
+ +
+ {% endif %} +
+
+
+
+ + {{ activity.user.username }} + +
+ {{ activity.timestamp.strftime('%H:%M') }} +
+

{{ activity.description }}

+ + {{ activity.timestamp.strftime('%Y-%m-%d') }} + {% if activity.action_type == 'upload' %} + Media Upload + {% elif activity.action_type == 'delete' %} + Media Delete + {% elif activity.action_type == 'schedule' %} + Schedule + {% elif activity.action_type == 'player_add' %} + Player Management + {% elif activity.action_type == 'user_add' %} + User Management + {% endif %} + +
+
+ {% endfor %} +
+ {% else %} +
+ +
No recent activity
+

System activities will appear here

+
+ {% endif %} +
+
+
+
+ + +
+
+
+
+
API Information
+
+
+

Use these endpoints for your Info-Beamer devices:

+
    +
  • GET {{ request.host_url }}api/content - Get all media files
  • +
  • GET {{ request.host_url }}api/playlist - Get playlist configuration
  • +
  • POST {{ request.host_url }}api/player/<device_id>/heartbeat - Player status
  • +
+
+
+
+
+
+ + + + +