diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 69cbd7a..5ba31e2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,7 +3,7 @@ name: Build and Release
on:
push:
tags:
- - 'v*.*.*'
+ - 'v*.*.*' # Trigger on semantic versioning tags
jobs:
build:
@@ -22,7 +22,7 @@ jobs:
- name: Get version from tag
id: vars
- run: echo "VERSION=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV"
+ run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Install dependencies
run: |
@@ -33,9 +33,18 @@ jobs:
run: |
python setup.py sdist bdist_wheel
+ - name: Create Release
+ id: create_release
+ run: |
+ TAG_NAME=${GITHUB_REF#refs/tags/}
+ echo "Creating release for tag $TAG_NAME"
+ echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
+
- name: Upload Package to Release
+ if: startsWith(github.ref, 'refs/tags/v') # Run this step if the tag starts with 'v'
uses: softprops/action-gh-release@v1
with:
+ tag_name: ${{ env.TAG_NAME }}
files: |
dist/*
env:
diff --git a/.vscode/sftp.json b/.vscode/sftp.json
index bf7623b..aa68bb4 100644
--- a/.vscode/sftp.json
+++ b/.vscode/sftp.json
@@ -1,11 +1,20 @@
{
- "name": "Radxa Zero 3E",
+ "name": "Radxa",
"host": "10.0.1.14",
"protocol": "sftp",
"port": 22,
"username": "root",
- "remotePath": "/root/workspace/py_config_gs",
+ "remotePath": "/root/workspace/improver",
"uploadOnSave": false,
"useTempFile": false,
- "openSsh": false
-}
\ No newline at end of file
+ "openSsh": false,
+ "ignore": [
+ "**/*.log",
+ "**/*.tmp",
+ ".venv/**",
+ ".git/**",
+ "**/*.env",
+ "**/.DS_Store"
+
+ ]
+}
diff --git a/README.md b/README.md
index ae89411..dd4bb28 100644
--- a/README.md
+++ b/README.md
@@ -78,3 +78,9 @@ sudo systemctl enable py-config-gs
```
pip uninstall py_config_gs-0.1-py3-none-any.whl
```
+
+
+
+This is an open project, so you can help, too.
+
+We try to collect, organize and share as much information regarding different aspects of the project as we can. But sometimes we overlook things that seem obvious to us, developers, but are not so obvious to end-users, people who are less familiar with nuts and bolts behind the scene. That is why we set up this wiki and let anyone having a GitHub account to make additions and improvements to the knowledgebase. Read [How to contribute](https://github.com/OpenIPC/wiki/blob/master/en/contribute.md).
\ No newline at end of file
diff --git a/py_config_gs/app.py b/py_config_gs/app.py
index 88d49b7..e788574 100644
--- a/py_config_gs/app.py
+++ b/py_config_gs/app.py
@@ -1,199 +1,298 @@
import logging
-from flask import Flask, render_template, request, Response, redirect, url_for, send_from_directory, flash
import json
import os
import subprocess
-from importlib.metadata import version
-
-
-#app_version = version('py-config-gs')
-with open('version.txt', 'r') as f:
+from flask import (
+ Flask,
+ render_template,
+ request,
+ Response,
+ redirect,
+ url_for,
+ send_from_directory,
+ flash,
+)
+
+# Constants
+ALLOWED_EXTENSIONS = {"key"}
+GS_UPLOAD_FOLDER = "/etc"
+VERSION_FILE = "version.txt"
+DEV_SETTINGS_FILE = os.path.expanduser("~/config/py-config-gs.json")
+PROD_SETTINGS_FILE = "/config/py-config-gs.json"
+
+# Load version for footer
+with open(VERSION_FILE, "r") as f:
app_version = f.read().strip()
+# Initialize global variables
+settings = {}
+config_files = []
# Configure logging
-logging.basicConfig(level=logging.DEBUG, # Set the log level to DEBUG
- format='%(asctime)s - %(levelname)s - %(message)s')
+logging.basicConfig(
+ level=logging.DEBUG,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+)
logger = logging.getLogger(__name__)
app = Flask(__name__)
-app.secret_key = os.getenv('SECRET_KEY', 'default_secret_key')
+app.secret_key = os.getenv("SECRET_KEY", "default_secret_key")
+
+# Set the settings file based on the environment
+SETTINGS_FILE = (
+ DEV_SETTINGS_FILE if os.getenv("FLASK_ENV") == "development" else PROD_SETTINGS_FILE
+)
+logger.info(f"Settings file path: {SETTINGS_FILE}")
+logger.info(f"App version: {app_version}")
-if os.getenv('FLASK_ENV') == 'development':
- # In development, use the home folder settings file
- SETTINGS_FILE = os.path.expanduser('~/config/py-config-gs.json')
-else:
- # In production, use the config folder
- SETTINGS_FILE = '/config/py-config-gs.json'
-# Log the SETTINGS_FILE path
-logger.info(f'Settings file path: {SETTINGS_FILE}')
-logger.info(f'App version: {app_version}')
+def load_config():
+ """Load configuration settings from the settings file."""
+ global settings, config_files
+ try:
+ with open(SETTINGS_FILE, "r") as f:
+ settings = json.load(f)
+ config_files = settings.get("config_files", [])
+ logger.debug(f"Config files loaded: {config_files}")
+ except Exception as e:
+ logger.error(f"Error loading configuration: {e}")
+ raise
+
-# Load settings.json
-with open(SETTINGS_FILE, 'r') as f:
- settings = json.load(f)
+load_config()
+VIDEO_DIR = os.path.expanduser(settings["VIDEO_DIR"])
+SERVER_PORT = settings["SERVER_PORT"]
-# Access configuration files and video directory
-config_files = settings['config_files']
-VIDEO_DIR = os.path.expanduser(settings['VIDEO_DIR'])
-SERVER_PORT = settings['SERVER_PORT']
+logger.debug(f"Loaded settings: {settings}")
+logger.debug(f"VIDEO_DIR is set to: {VIDEO_DIR}")
-logger.debug(f'Loaded settings: {settings}')
-logger.debug(f'VIDEO_DIR is set to: {VIDEO_DIR}')
def stream_journal():
"""Stream journalctl output in real-time."""
- process = subprocess.Popen(
- ['journalctl', '-f'],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True
- )
-
- while True:
- output = process.stdout.readline()
- if output:
- yield f"data: {output}\n\n"
- else:
- break
+ if os.getenv("FLASK_ENV") != "development":
+ process = subprocess.Popen(
+ ["journalctl", "-f"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ while True:
+ output = process.stdout.readline()
+ if output:
+ yield f"data: {output}\n\n"
+ else:
+ break
+ else:
+ logger.info("No data in DEVELOPMENT mode")
+ yield "data: No data in DEVELOPMENT mode\n\n"
-@app.route('/journal')
+
+@app.route("/journal")
def journal():
- return render_template('journal.html', version=app_version)
+ return render_template("journal.html", version=app_version)
+
-@app.route('/stream')
+@app.route("/stream")
def stream():
- return Response(stream_journal(), content_type='text/event-stream')
+ return Response(stream_journal(), content_type="text/event-stream")
-@app.route('/')
+
+@app.route("/")
def home():
- # List of services that you want to control
- services = ['openipc']
+ services = ["openipc", "wifibroadcast.service"]
service_statuses = {}
-
- # Fetches the current status (enabled/disabled) for each service.
- for service in services:
- # Check if the service is enabled or disabled
- result = subprocess.run(['systemctl', 'is-enabled', service], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- status = result.stdout.decode('utf-8').strip()
- service_statuses[service] = status
- return render_template('home.html', config_files=config_files, version=app_version, services=service_statuses )
+ load_config()
+
+ if os.getenv("FLASK_ENV") != "development":
+ for service in services:
+ result = subprocess.run(
+ ["systemctl", "is-enabled", service],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ )
+ service_statuses[service] = result.stdout.decode("utf-8").strip()
+
+ return render_template(
+ "home.html",
+ config_files=config_files,
+ version=app_version,
+ services=service_statuses,
+ )
+
-@app.route('/edit/', methods=['GET', 'POST'])
+@app.route("/edit/", methods=["GET", "POST"])
def edit(filename):
- file_path = next((item['path'] for item in config_files if item['name'] == filename), None)
-
- if request.method == 'POST':
- content = request.form['content']
- with open(file_path, 'w') as f:
+ """Edit a configuration file."""
+ load_config()
+ logger.debug(f"Config files available: {config_files}")
+
+ file_path = next(
+ (item["path"] for item in config_files if item["name"] == filename), None
+ )
+
+ if file_path is None:
+ logger.error(f"File path not found for: {filename}")
+ flash(f"Configuration file not found: {filename}", "error")
+ return redirect(url_for("home"))
+
+ if request.method == "POST":
+ content = request.form["content"]
+ with open(file_path, "w") as f:
f.write(content)
- logger.debug(f'Updated configuration file: {filename}')
- return redirect(url_for('home'))
+ logger.debug(f"Updated configuration file: {filename}")
+ return redirect(url_for("home"))
- with open(file_path, 'r') as f:
+ with open(file_path, "r") as f:
content = f.read()
-
- return render_template('edit.html', filename=filename, content=content, version=app_version)
-
-@app.route('/save/', methods=['POST'])
-def save(filename):
- file_path = next((item['path'] for item in config_files if item['name'] == filename), None)
- content = request.form['content']
- with open(file_path, 'w') as f:
- f.write(content)
- logger.debug(f'Saved configuration file: {filename}')
- return redirect(url_for('home'))
-
-@app.route('/videos')
+
+ return render_template(
+ "edit.html", filename=filename, content=content, version=app_version
+ )
+
+
+@app.route("/videos")
def videos():
- video_files = [f for f in os.listdir(VIDEO_DIR) if f.endswith(('.mp4', '.mkv', '.avi'))]
- logger.debug(f'VIDEO_DIR: {VIDEO_DIR}')
- logger.debug(f'Video files found: {video_files}')
- return render_template('videos.html', video_files=video_files, version=app_version)
+ """List video files in the video directory."""
+ video_files = [
+ f for f in os.listdir(VIDEO_DIR) if f.endswith((".mp4", ".mkv", ".avi"))
+ ]
+ logger.debug(f"VIDEO_DIR: {VIDEO_DIR}")
+ logger.debug(f"Video files found: {video_files}")
+ flash(f"Loading from VIDEO_DIR: {VIDEO_DIR}", "info")
+ return render_template("videos.html", video_files=video_files, version=app_version)
-@app.route('/play/')
+@app.route("/play/")
def play(filename):
+ """Serve a video file."""
try:
- # Ensure the file exists in the VIDEO_DIR and is served from there
return send_from_directory(VIDEO_DIR, filename)
except FileNotFoundError:
- logger.error(f'Video file not found: {filename}')
+ logger.error(f"Video file not found: {filename}")
return "File not found", 404
-@app.route('/temperature')
+
+@app.route("/temperature")
def get_temperature():
+ """Get SOC and GPU temperatures."""
try:
- soc_temp = int(open('/sys/class/thermal/thermal_zone0/temp').read().strip()) / 1000.0 # Convert to °C
- gpu_temp = int(open('/sys/class/thermal/thermal_zone1/temp').read().strip()) / 1000.0 # Convert to °C
- soc_temp_f = (soc_temp * 9/5) + 32
- gpu_temp_f = (gpu_temp * 9/5) + 32
-
+ if os.getenv("FLASK_ENV") != "development":
+ soc_temp = (
+ int(open("/sys/class/thermal/thermal_zone0/temp").read().strip())
+ / 1000.0
+ )
+ gpu_temp = (
+ int(open("/sys/class/thermal/thermal_zone1/temp").read().strip())
+ / 1000.0
+ )
+ else:
+ soc_temp = gpu_temp = 0
+
return {
- 'soc_temperature': f"{soc_temp:.1f}",
- 'soc_temperature_f': f"{soc_temp_f:.1f}",
- 'gpu_temperature': f"{gpu_temp:.1f}",
- 'gpu_temperature_f': f"{gpu_temp_f:.1f}"
+ "soc_temperature": f"{soc_temp:.1f}",
+ "gpu_temperature": f"{gpu_temp:.1f}",
}
-
except Exception as e:
- logger.error(f'Error getting temperature: {e}')
- return {'error': str(e)}, 500
+ logger.error(f"Error getting temperature: {e}")
+ return {"error": str(e)}, 500
+
-@app.route('/backup')
+@app.route("/backup")
def backup():
+ """Create backups of configuration files."""
for item in config_files:
- file_path = item['path']
- backup_path = file_path + '.bak'
- with open(file_path, 'r') as f:
+ file_path = item["path"]
+ backup_path = f"{file_path}.bak"
+ with open(file_path, "r") as f:
content = f.read()
- with open(backup_path, 'w') as f:
+ with open(backup_path, "w") as f:
f.write(content)
- logger.debug('Backup created for configuration files.')
- return redirect(url_for('home'))
+ logger.debug("Backup created for configuration files.")
+ return redirect(url_for("home"))
-@app.route('/run_command', methods=['POST'])
-def run_command():
- selected_command = request.form.get('command')
- # Construct the first command based on the dropdown value
+@app.route("/run_command", methods=["POST"])
+def run_command():
+ """Run a selected command."""
+ selected_command = request.form.get("command")
cli_command = f"echo cli -s {selected_command} > /dev/udp/localhost/14550"
- logger.debug(f'Running command: {cli_command}')
+ logger.debug(f"Running command: {cli_command}")
+ flash(f"Running command: {cli_command}", "info")
- # Run the commands
subprocess.run(cli_command, shell=True)
subprocess.run("echo killall -1 majestic > /dev/udp/localhost/14550", shell=True)
- # Redirect back to the home page after the commands are run
- return redirect(url_for('home'))
+ return redirect(url_for("home"))
+
-@app.route('/service_action', methods=['POST'])
+@app.route("/service_action", methods=["POST"])
def service_action():
- service_name = request.form.get('service_name')
- action = request.form.get('action')
+ """Perform an action on a service."""
+ service_name = request.form.get("service_name")
+ action = request.form.get("action")
if service_name and action:
try:
- if action == 'enable':
- subprocess.run(['sudo', 'systemctl', 'enable', service_name], check=True)
- flash(f'Service {service_name} enabled successfully.', 'success')
- elif action == 'disable':
- subprocess.run(['sudo', 'systemctl', 'disable', service_name], check=True)
- flash(f'Service {service_name} disabled successfully.', 'success')
- elif action == 'restart':
- subprocess.run(['sudo', 'systemctl', 'restart', service_name], check=True)
- flash(f'Service {service_name} restarted successfully.', 'success')
- else:
- flash('Invalid action.', 'error')
+ command = ["sudo", "systemctl", action, service_name]
+ subprocess.run(command, check=True)
+ flash(f"Service {service_name} {action}d successfully.", "success")
except subprocess.CalledProcessError as e:
- flash(f'Failed to {action} service {service_name}: {e}', 'error')
+ flash(f"Failed to {action} service {service_name}: {e}", "error")
+
+ return redirect(url_for("home"))
+
+
+def allowed_file(filename):
+ """Check if the uploaded file is allowed."""
+ return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
+
+
+@app.route("/upload", methods=["GET", "POST"])
+def upload_file():
+ """Handle file upload."""
+ if request.method == "POST":
+ if "file" not in request.files:
+ flash("No file part")
+ return redirect(request.url)
+ file = request.files["file"]
+ if file.filename == "":
+ flash("No selected file")
+ return redirect(request.url)
+ if file and allowed_file(file.filename):
+ file_path = os.path.join(GS_UPLOAD_FOLDER, "gs.key")
+ file.save(file_path)
+ flash("File successfully uploaded")
+ return redirect(url_for("home"))
+ return render_template("upload.html")
+
+
+@app.route("/settings", methods=["GET", "POST"])
+def settings_view():
+ """Manage application settings."""
+ load_config()
+
+ if request.method == "POST":
+ try:
+ config_files = request.form.getlist("config_files")
+ video_dir = request.form.get("VIDEO_DIR")
+ server_port = request.form.get("SERVER_PORT")
+
+ settings_data = {
+ "VIDEO_DIR": video_dir,
+ "SERVER_PORT": server_port,
+ "config_files": config_files,
+ }
+
+ with open(SETTINGS_FILE, "w") as f:
+ json.dump(settings_data, f, indent=4)
+
+ flash("Settings updated successfully.")
+ except Exception as e:
+ flash(f"Error saving settings: {e}", "error")
- return redirect(url_for('home'))
+ return render_template("settings.html", settings=settings)
-def main():
- app.run(host='0.0.0.0', port=SERVER_PORT)
-if __name__ == '__main__':
- main()
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=SERVER_PORT)
diff --git a/py_config_gs/static/css/styles.css b/py_config_gs/static/css/styles.css
index 5249561..18a5ade 100644
--- a/py_config_gs/static/css/styles.css
+++ b/py_config_gs/static/css/styles.css
@@ -1,15 +1,13 @@
-/* styles.css */
-
/* General Styles */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f4f4f4;
+ line-height: 1.6;
}
-h1,
-h2 {
+h1, h2 {
color: #333;
}
@@ -28,10 +26,12 @@ p {
display: flex;
background-color: #e2e2e2;
border-radius: 5px;
+ flex-wrap: wrap; /* Allows tabs to wrap on small screens */
}
.tabs li {
margin: 0;
+ flex: 1 1 auto; /* Allow tabs to be responsive */
}
.tabs a {
@@ -39,6 +39,7 @@ p {
padding: 10px 15px;
text-decoration: none;
color: #333;
+ text-align: center;
border-radius: 5px;
margin-right: 5px;
transition: background-color 0.3s;
@@ -88,12 +89,13 @@ p {
/* Footer Styles */
footer {
- position: fixed;
- bottom: 0;
- width: 100%;
background-color: #f1f1f1;
text-align: center;
padding: 10px;
+ width: 100%;
+ position: relative; /* Change from fixed to relative */
+ bottom: 0;
+ flex-shrink: 0; /* Ensure the footer is not squeezed by the content */
}
/* Styled Box */
@@ -161,13 +163,14 @@ table td:nth-child(3) {
/* Button Styles */
button {
- padding: 8px 12px; /* Increase button size */
+ padding: 12px 16px; /* Increase button size for touch screens */
margin: 0 5px;
border: none;
cursor: pointer;
background-color: #007bff;
color: white;
border-radius: 4px;
+ font-size: 16px;
}
button[type="submit"]:hover {
@@ -178,3 +181,143 @@ button[type="submit"]:hover {
form {
display: inline;
}
+
+/* Responsive Layout */
+@media (max-width: 768px) {
+ body {
+ padding: 10px;
+ }
+
+ .tabs ul {
+ flex-direction: column; /* Stack tabs on small screens */
+ }
+
+ button {
+ width: 100%; /* Make buttons full-width on mobile */
+ margin-bottom: 10px;
+ }
+
+ table th, table td {
+ font-size: 14px; /* Smaller text for tables */
+ padding: 10px;
+ }
+
+ .content {
+ padding: 15px;
+ }
+
+ footer {
+ padding: 8px;
+ }
+}
+
+
+/* Configuration Files Section */
+.config-section {
+ border: 1px solid #ccc; /* Thin border around the section */
+ border-radius: 5px; /* Optional: Add border radius for smooth edges */
+ padding: 20px;
+ margin-bottom: 20px; /* Space between this section and others */
+ background-color: #fff;
+}
+
+.config-section h2 {
+ margin-top: 0; /* Remove extra top margin */
+ font-size: 24px;
+ color: #333;
+}
+
+/* Configuration Content */
+.config-content {
+ display: flex;
+ flex-direction: column; /* Align children vertically */
+ gap: 15px; /* Add space between Edit and Actions */
+}
+
+/* Individual Config Items */
+.config-item h3 {
+ font-size: 20px;
+ margin: 0 0 10px 0; /* Space below each heading */
+ color: #007bff; /* Optional: Add color to the headings */
+}
+
+.config-item p {
+ margin: 0;
+ color: #555;
+ font-size: 16px;
+}
+
+/* For mobile responsiveness, if needed */
+@media (max-width: 768px) {
+ .config-content {
+ gap: 10px; /* Reduce gap on smaller screens */
+ }
+}
+
+
+
+/* Header container to hold the logo on the left and center the heading */
+.header-container {
+ display: flex;
+ align-items: center; /* Vertically center the logo and heading */
+ justify-content: center; /* Initially center everything */
+ position: relative; /* For the absolute positioning of the logo */
+ margin-bottom: 20px;
+}
+
+/* Logo styling */
+.header-logo {
+ position: absolute;
+ left: 0; /* Align the logo to the left */
+ max-width: 60px; /* Set the logo size */
+ height: auto;
+ margin-left: 20px; /* Add space from the left edge */
+}
+
+/* Center the heading */
+.header-container h1 {
+ margin: 0;
+ text-align: center;
+ font-size: 24px;
+ color: #333;
+ flex-grow: 1; /* Let the heading take up the remaining space */
+}
+
+/* Adjust the layout on smaller screens */
+@media (max-width: 768px) {
+ .header-container {
+ flex-direction: column; /* Stack the logo and heading vertically */
+ }
+
+ .header-logo {
+ position: static;
+ margin-left: 0;
+ margin-bottom: 10px; /* Add space below the logo when stacked */
+ }
+}
+
+/* Flash Message Styles */
+.flash-message {
+ background-color: #d1ecf1; /* Light blue background */
+ color: #0c5460; /* Dark blue text */
+
+ border: 1px solid #4046ed; /* Border to define the area */
+ border-radius: 5px; /* Rounded corners */
+ margin-bottom: 20px; /* Space below the flash message */
+
+ width: 100%; /* Fill the width of the container */
+ max-width: 100%; /* Ensure it doesn't exceed the viewport width */
+ height: 50px; /* Fixed height */
+ overflow-y: auto; /* Allow vertical scrolling */
+
+ padding: 10px; /* Padding for inner content */
+ box-sizing: border-box; /* Include padding in the height and width */
+}
+
+/* Additional styles for message list */
+.flash-message ul {
+ list-style-type: none; /* Remove default list styling */
+ padding: 0; /* Remove padding */
+ margin: 0; /* Remove margin */
+}
+
diff --git a/py_config_gs/static/images/OpenIPCFPV.png b/py_config_gs/static/images/OpenIPCFPV.png
new file mode 100644
index 0000000..502a057
Binary files /dev/null and b/py_config_gs/static/images/OpenIPCFPV.png differ
diff --git a/py_config_gs/templates/base.html b/py_config_gs/templates/base.html
index 5390b0c..2d78034 100644
--- a/py_config_gs/templates/base.html
+++ b/py_config_gs/templates/base.html
@@ -4,23 +4,42 @@
- {% block title %}Py-Config-GS{% endblock %}
+ {% block title %}OpenIPC Improver ConfigMan GS{% endblock %}
@@ -31,7 +50,7 @@ Welcome to the Configuration Manager
diff --git a/py_config_gs/templates/home.html b/py_config_gs/templates/home.html
index 22cb969..c0cf194 100644
--- a/py_config_gs/templates/home.html
+++ b/py_config_gs/templates/home.html
@@ -2,21 +2,48 @@
{% extends "base.html" %}
{% block content %}
-Configuration Files
-{% if config_files %}
-
-{% else %}
-No configuration files available.
-{% endif %}
-
+
+
+
+
+
+
Configuration Files
+
+
+
Edit
+ {% if config_files %}
+
+ {% else %}
+
No configuration files available.
+ {% endif %}
+
+
+
+
+
+
Radxa Temps
+
@@ -46,26 +73,9 @@ Radxa Temps
+
-
-
-
-
-
- {% with messages = get_flashed_messages(with_categories=true) %}
- {% if messages %}
-
- {% for category, message in messages %}
- - {{ message }}
- {% endfor %}
-
- {% endif %}
- {% endwith %}
-
-
-
-
-
+
System Control
@@ -107,11 +117,11 @@ System Control
{% endfor %}
-
-
+
-
+
Camera Control
+
Work in Progress
-
+
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/py_config_gs/templates/settings.html b/py_config_gs/templates/settings.html
new file mode 100644
index 0000000..ce6c49c
--- /dev/null
+++ b/py_config_gs/templates/settings.html
@@ -0,0 +1,52 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/py_config_gs/templates/upload.html b/py_config_gs/templates/upload.html
new file mode 100644
index 0000000..411701e
--- /dev/null
+++ b/py_config_gs/templates/upload.html
@@ -0,0 +1,20 @@
+
+{% extends "base.html" %}
+
+{% block content %}
+
+
Upload gs.key
+
+ {% with messages = get_flashed_messages() %}
+ {% if messages %}
+
+ {% for message in messages %}
+ - {{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+{% endblock %}
\ No newline at end of file
diff --git a/py_config_gs/version.txt b/py_config_gs/version.txt
index 90a27f9..8fb1297 100644
--- a/py_config_gs/version.txt
+++ b/py_config_gs/version.txt
@@ -1 +1 @@
-1.0.5
+1.0.5-next
diff --git a/requirements.txt b/requirements.txt
index 0d3f12b..3a08d2c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,10 +1,13 @@
blinker==1.8.2
click==8.1.7
-flask==3.0.3
-importlib-metadata==8.5.0
+Flask==3.0.3
+importlib_metadata==8.5.0
itsdangerous==2.2.0
-jinja2==3.1.4
+Jinja2==3.1.4
MarkupSafe==2.1.5
-werkzeug==3.0.4
+packaging==24.1
+python-dotenv==1.0.1
+setuptools==75.1.0
+setuptools-scm==8.1.0
+Werkzeug==3.0.4
zipp==3.20.2
-setuptools-scm==8.1.0
\ No newline at end of file