Initial Repo Setup
This commit is contained in:
commit
a136a20592
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
build
|
||||
dist
|
||||
.venv
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
*_pycache_*
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.db
|
||||
*.sqlite
|
||||
*.egg-info
|
||||
*.egg
|
||||
*.log
|
||||
*.env
|
||||
.pypirc
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 IDSolutions
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
8
MANIFEST.in
Normal file
8
MANIFEST.in
Normal file
@ -0,0 +1,8 @@
|
||||
include firefly/templates/*.html
|
||||
include README.md
|
||||
include requirements.txt
|
||||
include LICENSE
|
||||
recursive-include firefly/templates *
|
||||
recursive-include firefly/static *
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[cod]
|
77
README.md
Normal file
77
README.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Firefly Database Viewer
|
||||
|
||||
A web-based viewer for Firefly databases with support for strings, lists, and hash tables.
|
||||
|
||||
## Features
|
||||
|
||||
- View all Firefly database keys and their values
|
||||
- Support for different data types (string, list, hash)
|
||||
- Modern, responsive UI with dark mode support
|
||||
- Real-time data refresh
|
||||
- Color-coded data type indicators
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
1. Clone the repository and navigate to the project directory
|
||||
|
||||
2. Create a virtual environment (recommended):
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. Install the package in development mode:
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Using pip (when published)
|
||||
|
||||
```bash
|
||||
pip install firefly-viewer
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.env` file in your working directory with your Firefly connection details:
|
||||
|
||||
```env
|
||||
FIREFLY_HOST=localhost
|
||||
FIREFLY_PORT=6379
|
||||
FIREFLY_PASSWORD=your_password
|
||||
FIREFLY_DEBUG=false
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
Start the viewer using the command-line interface:
|
||||
|
||||
```bash
|
||||
firefly-viewer
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--host`: Host to bind to (default: 0.0.0.0)
|
||||
- `--port`: Port to bind to (default: 5000)
|
||||
- `--debug/--no-debug`: Enable/disable debug mode
|
||||
|
||||
Example:
|
||||
```bash
|
||||
firefly-viewer --host 127.0.0.1 --port 8000 --debug
|
||||
```
|
||||
|
||||
### Web Interface
|
||||
|
||||
1. Open your web browser and navigate to the URL shown in the console (default: http://localhost:5000)
|
||||
2. Use the "Refresh Data" button to update the view with the latest database contents
|
||||
3. Toggle between light and dark themes using the theme button
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.7+
|
||||
- Firefly database server
|
||||
- Modern web browser
|
13
app.py
Normal file
13
app.py
Normal file
@ -0,0 +1,13 @@
|
||||
from flask import Flask
|
||||
from firefly.routes import routes
|
||||
from firefly.config import DEBUG
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(routes)
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=DEBUG)
|
38
build.py
Normal file
38
build.py
Normal file
@ -0,0 +1,38 @@
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
def clean_build():
|
||||
"""Clean up build artifacts"""
|
||||
dirs_to_clean = ['build', 'dist', '*.egg-info']
|
||||
for dir_pattern in dirs_to_clean:
|
||||
for item in glob.glob(dir_pattern):
|
||||
if os.path.isdir(item):
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
os.remove(item)
|
||||
|
||||
def build_package():
|
||||
"""Build the package"""
|
||||
clean_build()
|
||||
subprocess.check_call(['python', 'setup.py', 'sdist', 'bdist_wheel'])
|
||||
|
||||
def install_package():
|
||||
"""Install the package in development mode"""
|
||||
subprocess.check_call(['pip', 'install', '-e', '.'])
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
import glob
|
||||
|
||||
command = sys.argv[1] if len(sys.argv) > 1 else 'build'
|
||||
|
||||
if command == 'clean':
|
||||
clean_build()
|
||||
elif command == 'build':
|
||||
build_package()
|
||||
elif command == 'install':
|
||||
install_package()
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
print("Available commands: clean, build, install")
|
23
firefly/__init__.py
Normal file
23
firefly/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""
|
||||
Firefly Database Viewer
|
||||
A Flask-based web interface for viewing and interacting with Firefly databases.
|
||||
"""
|
||||
|
||||
import os
|
||||
from flask import Flask
|
||||
from .config import *
|
||||
from .database import DatabaseClient
|
||||
from .routes import routes
|
||||
from .services import KeyService
|
||||
from .logger import logger
|
||||
|
||||
def create_app():
|
||||
template_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||
app = Flask(__name__, template_folder=template_dir)
|
||||
app.debug = DEBUG
|
||||
app.register_blueprint(routes)
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
__version__ = "1.0.0"
|
27
firefly/cli.py
Normal file
27
firefly/cli.py
Normal file
@ -0,0 +1,27 @@
|
||||
import os
|
||||
import click
|
||||
from waitress import serve
|
||||
from .config import DEBUG
|
||||
from . import app
|
||||
|
||||
@click.command()
|
||||
@click.option('--host', default='0.0.0.0', help='Host to bind to')
|
||||
@click.option('--port', default=5000, help='Port to bind to')
|
||||
@click.option('--debug/--no-debug', default=None, help='Enable debug mode')
|
||||
def main(host, port, debug):
|
||||
"""Firefly Database Viewer - A web interface for viewing Firefly databases"""
|
||||
if debug is None:
|
||||
debug = DEBUG
|
||||
|
||||
if debug:
|
||||
click.echo(f"Starting Firefly Database Viewer in DEBUG mode on http://{host}:{port}")
|
||||
click.echo("Press CTRL+C to quit")
|
||||
app.run(host=host, port=port, debug=True)
|
||||
else:
|
||||
click.echo(f"Starting Firefly Database Viewer in PRODUCTION mode on http://{host}:{port}")
|
||||
click.echo("Using Waitress production server")
|
||||
click.echo("Press CTRL+C to quit")
|
||||
serve(app, host=host, port=port, threads=4)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
13
firefly/config.py
Normal file
13
firefly/config.py
Normal file
@ -0,0 +1,13 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# Database configuration
|
||||
FIREFLY_HOST = os.getenv("FIREFLY_HOST", "localhost")
|
||||
FIREFLY_PORT = int(os.getenv("FIREFLY_PORT", 6379))
|
||||
FIREFLY_PASSWORD = os.getenv("FIREFLY_PASSWORD", None)
|
||||
|
||||
# Debug configuration
|
||||
DEBUG = os.getenv("FIREFLY_DEBUG", "false").lower() == "true"
|
29
firefly/database.py
Normal file
29
firefly/database.py
Normal file
@ -0,0 +1,29 @@
|
||||
from ifireflylib import IFireflyClient as FireflyDatabase
|
||||
from .config import FIREFLY_HOST, FIREFLY_PORT, FIREFLY_PASSWORD
|
||||
from .logger import logger
|
||||
|
||||
class DatabaseClient:
|
||||
@staticmethod
|
||||
def create_client():
|
||||
try:
|
||||
return FireflyDatabase(
|
||||
host=FIREFLY_HOST,
|
||||
port=FIREFLY_PORT,
|
||||
password=FIREFLY_PASSWORD,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create Firefly client: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_key_type(client, key):
|
||||
try:
|
||||
type_result = client.execute_command("TYPE", key)
|
||||
if type_result and isinstance(type_result, str):
|
||||
if type_result.startswith(('+', '-', ':', '$', '*')):
|
||||
type_result = type_result[1:]
|
||||
return type_result.strip().lower()
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"TYPE command failed for {key}: {e}")
|
||||
return None
|
22
firefly/logger.py
Normal file
22
firefly/logger.py
Normal file
@ -0,0 +1,22 @@
|
||||
import logging
|
||||
import sys
|
||||
from .config import DEBUG
|
||||
|
||||
def setup_logger():
|
||||
logger = logging.getLogger("FireflyViewer")
|
||||
|
||||
if DEBUG:
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler("firefly_debug.log"),
|
||||
logging.StreamHandler(sys.stdout),
|
||||
],
|
||||
)
|
||||
else:
|
||||
logger.addHandler(logging.NullHandler())
|
||||
|
||||
return logger
|
||||
|
||||
logger = setup_logger()
|
118
firefly/routes.py
Normal file
118
firefly/routes.py
Normal file
@ -0,0 +1,118 @@
|
||||
from flask import Blueprint, render_template, jsonify, request
|
||||
from .database import DatabaseClient
|
||||
from .services import KeyService
|
||||
from .logger import logger
|
||||
|
||||
routes = Blueprint('routes', __name__)
|
||||
|
||||
@routes.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@routes.route('/api/keys', methods=['GET'])
|
||||
def get_keys():
|
||||
logger.debug("API endpoint /api/keys called")
|
||||
try:
|
||||
client = DatabaseClient.create_client()
|
||||
if not client:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Could not connect to Firefly database',
|
||||
'connection_status': False
|
||||
})
|
||||
|
||||
pattern = request.args.get('pattern', '*')
|
||||
keys = client.string_ops.keys(pattern)
|
||||
|
||||
if not keys:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {'strings': [], 'lists': [], 'hashes': []},
|
||||
'connection_status': True
|
||||
})
|
||||
|
||||
strings = []
|
||||
lists = []
|
||||
hashes = []
|
||||
|
||||
for key in keys:
|
||||
key_type = DatabaseClient.get_key_type(client, key)
|
||||
if key_type == "string":
|
||||
value = client.string_ops.string_get(key)
|
||||
if value is not None:
|
||||
strings.append({"key": key, "value": value})
|
||||
elif key_type == "list":
|
||||
value = client.list_ops.list_range(key, 0, -1)
|
||||
if value is not None:
|
||||
is_email = 'email' in key.lower()
|
||||
processed_value = KeyService.process_list_values(value, is_email)
|
||||
lists.append({"key": key, "value": processed_value})
|
||||
elif key_type == "hash":
|
||||
value = client.hash_ops.hash_get_all(key)
|
||||
if value is not None:
|
||||
processed_value = KeyService.parse_hash_data(value)
|
||||
hashes.append({"key": key, "value": processed_value})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'strings': strings,
|
||||
'lists': lists,
|
||||
'hashes': hashes
|
||||
},
|
||||
'connection_status': True
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting keys: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'connection_status': False
|
||||
})
|
||||
|
||||
@routes.route('/api/key/<key>', methods=['GET'])
|
||||
def get_key_value(key):
|
||||
try:
|
||||
client = DatabaseClient.create_client()
|
||||
if not client:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Could not connect to Firefly database',
|
||||
'connection_status': False
|
||||
})
|
||||
|
||||
key_type = DatabaseClient.get_key_type(client, key)
|
||||
if not key_type:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Key not found or type not determined',
|
||||
'connection_status': True
|
||||
}), 404
|
||||
|
||||
value = None
|
||||
if key_type == "string":
|
||||
value = client.string_ops.string_get(key)
|
||||
elif key_type == "hash":
|
||||
value = client.hash_ops.hash_get_all(key)
|
||||
value = KeyService.parse_hash_data(value)
|
||||
elif key_type == "list":
|
||||
value = client.list_ops.list_range(key, 0, -1)
|
||||
is_email = 'email' in key.lower()
|
||||
value = KeyService.process_list_values(value, is_email)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'key': key,
|
||||
'type': key_type,
|
||||
'value': value
|
||||
},
|
||||
'connection_status': True
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting key value: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'connection_status': False
|
||||
})
|
69
firefly/services.py
Normal file
69
firefly/services.py
Normal file
@ -0,0 +1,69 @@
|
||||
from .logger import logger
|
||||
|
||||
class KeyService:
|
||||
@staticmethod
|
||||
def parse_hash_data(hash_data):
|
||||
properly_parsed_hash = {}
|
||||
|
||||
if isinstance(hash_data, dict):
|
||||
all_pairs = []
|
||||
for k in hash_data.keys():
|
||||
if '=' in k:
|
||||
all_pairs.append(k)
|
||||
for v in hash_data.values():
|
||||
if isinstance(v, str) and '=' in v:
|
||||
all_pairs.append(v)
|
||||
|
||||
for pair in all_pairs:
|
||||
if '=' in pair:
|
||||
field, value = pair.split('=', 1)
|
||||
properly_parsed_hash[field.strip()] = KeyService._parse_value(value.strip())
|
||||
|
||||
elif isinstance(hash_data, str):
|
||||
lines = hash_data.strip().split('\n')
|
||||
for line in lines:
|
||||
if '=' in line:
|
||||
field, value = line.split('=', 1)
|
||||
properly_parsed_hash[field.strip()] = KeyService._parse_value(value.strip())
|
||||
|
||||
return properly_parsed_hash
|
||||
|
||||
@staticmethod
|
||||
def _parse_value(value):
|
||||
if value.startswith('[') and value.endswith(']'):
|
||||
try:
|
||||
array_str = value[1:-1]
|
||||
array_values = [v.strip() for v in array_str.split(',')]
|
||||
return ", ".join(array_values)
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing array value: {e}")
|
||||
return value
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def clean_redis_value(value):
|
||||
if isinstance(value, str) and value.startswith(('+', '-', ':', '$', '*')):
|
||||
return value[1:].strip()
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def format_list(values):
|
||||
formatted_items = []
|
||||
for i in range(0, len(values), 2):
|
||||
if i + 1 < len(values):
|
||||
field = KeyService.clean_redis_value(values[i])
|
||||
value = KeyService.clean_redis_value(values[i + 1])
|
||||
field = field.capitalize()
|
||||
formatted_items.append(f"{field}: {value}")
|
||||
return formatted_items
|
||||
|
||||
@staticmethod
|
||||
def process_list_values(values, is_email=False):
|
||||
if is_email and len(values) >= 2 and len(values) % 2 == 0:
|
||||
return KeyService.format_list(values)
|
||||
else:
|
||||
cleaned_values = []
|
||||
for v in values:
|
||||
cleaned_v = KeyService.clean_redis_value(v)
|
||||
cleaned_values.append(cleaned_v)
|
||||
return cleaned_values
|
304
firefly/templates/index.html
Normal file
304
firefly/templates/index.html
Normal file
@ -0,0 +1,304 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Firefly Database Viewer</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bs-body-bg: #ffffff;
|
||||
--bs-body-color: #212529;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-color: #f8f9fa;
|
||||
}
|
||||
|
||||
body {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.key-card {
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.key-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.value-container {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.type-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
.connection-status {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
.section-header {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
.email-format {
|
||||
background-color: var(--bs-body-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.email-field {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.email-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.field-value {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.field-value:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.field-value strong {
|
||||
color: var(--bs-body-color);
|
||||
min-width: 80px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .card {
|
||||
background-color: #2c3034;
|
||||
border-color: #373b3e;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .card-header {
|
||||
background-color: #343a40;
|
||||
border-bottom-color: #373b3e;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .field-value {
|
||||
border-bottom-color: #373b3e;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .section-header {
|
||||
border-bottom-color: #373b3e;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1001;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-4">
|
||||
<button class="btn btn-outline-secondary theme-toggle" onclick="toggleTheme()">
|
||||
<span class="theme-icon">🌙</span>
|
||||
</button>
|
||||
|
||||
<h1 class="mb-4">Firefly Database Viewer</h1>
|
||||
|
||||
<div class="alert alert-info mb-4">
|
||||
<h4 class="alert-heading">Connection Status</h4>
|
||||
<p id="connection-message">Checking connection...</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary mb-3" onclick="refreshData()">Refresh Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="data-container">
|
||||
<div id="strings-section" class="section">
|
||||
<h2 class="section-header">String Keys</h2>
|
||||
<div class="row" id="strings-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="lists-section" class="section">
|
||||
<h2 class="section-header">List Keys</h2>
|
||||
<div class="row" id="lists-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="hashes-section" class="section">
|
||||
<h2 class="section-header">Hash Keys</h2>
|
||||
<div class="row" id="hashes-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add theme toggle functionality
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const currentTheme = html.getAttribute('data-bs-theme');
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
html.setAttribute('data-bs-theme', newTheme);
|
||||
|
||||
// Update theme icon
|
||||
const themeIcon = document.querySelector('.theme-icon');
|
||||
themeIcon.textContent = newTheme === 'light' ? '🌙' : '☀️';
|
||||
|
||||
// Save preference
|
||||
localStorage.setItem('theme', newTheme);
|
||||
}
|
||||
|
||||
// Load saved theme preference
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-bs-theme', savedTheme);
|
||||
const themeIcon = document.querySelector('.theme-icon');
|
||||
themeIcon.textContent = savedTheme === 'light' ? '🌙' : '☀️';
|
||||
});
|
||||
|
||||
function formatValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
// Display list values with latest values at the top
|
||||
return value.map(v => typeof v === 'string' ? v.trim() : v).join('<br>');
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// Format as key-value pairs
|
||||
return Object.entries(value)
|
||||
.map(([key, val]) => {
|
||||
let displayVal = typeof val === 'string' ? val.trim() : val;
|
||||
// Special handling for email fields
|
||||
if (['from', 'to', 'subject', 'content'].includes(key.toLowerCase())) {
|
||||
return `<div class="field-value">
|
||||
<strong>${key.charAt(0).toUpperCase() + key.slice(1)}:</strong>
|
||||
${displayVal}
|
||||
</div>`;
|
||||
}
|
||||
// Regular field formatting
|
||||
return `<div class="field-value">
|
||||
<strong>${key}:</strong> ${displayVal}
|
||||
</div>`;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
function getTypeBadgeClass(type) {
|
||||
const classes = {
|
||||
'string': 'bg-primary',
|
||||
'list': 'bg-success',
|
||||
'hash': 'bg-info'
|
||||
};
|
||||
return classes[type] || 'bg-secondary';
|
||||
}
|
||||
|
||||
function updateConnectionStatus(connected, message) {
|
||||
const alertDiv = document.querySelector('.alert');
|
||||
const messageElement = document.getElementById('connection-message');
|
||||
|
||||
if (connected) {
|
||||
alertDiv.className = 'alert alert-success mb-4';
|
||||
messageElement.textContent = 'Connected to Firefly database successfully!';
|
||||
} else {
|
||||
alertDiv.className = 'alert alert-danger mb-4';
|
||||
messageElement.textContent = `Connection failed: ${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function createKeyCard(key, type, value) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'col-md-6 col-lg-4';
|
||||
card.innerHTML = `
|
||||
<div class="card key-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">${key}</h5>
|
||||
<span class="badge ${getTypeBadgeClass(type)} type-badge">${type}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="value-container">
|
||||
${formatValue(value)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return card;
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
console.log('Fetching data from /api/keys...');
|
||||
fetch('/api/keys')
|
||||
.then(response => {
|
||||
console.log('Received response:', response.status);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Parsed data:', data);
|
||||
updateConnectionStatus(data.connection_status, data.error || '');
|
||||
|
||||
if (data.success) {
|
||||
// Clear existing containers
|
||||
document.getElementById('strings-container').innerHTML = '';
|
||||
document.getElementById('lists-container').innerHTML = '';
|
||||
document.getElementById('hashes-container').innerHTML = '';
|
||||
|
||||
const { strings = [], lists = [], hashes = [] } = data.data || {};
|
||||
|
||||
// Update strings
|
||||
if (strings.length > 0) {
|
||||
console.log(`Found ${strings.length} string keys`);
|
||||
strings.forEach(item => {
|
||||
document.getElementById('strings-container')
|
||||
.appendChild(createKeyCard(item.key, 'string', item.value));
|
||||
});
|
||||
} else {
|
||||
document.getElementById('strings-container').innerHTML =
|
||||
'<div class="col-12"><div class="alert alert-info">No string keys found.</div></div>';
|
||||
}
|
||||
|
||||
// Update lists
|
||||
if (lists.length > 0) {
|
||||
console.log(`Found ${lists.length} list keys`);
|
||||
lists.forEach(item => {
|
||||
document.getElementById('lists-container')
|
||||
.appendChild(createKeyCard(item.key, 'list', item.value));
|
||||
});
|
||||
} else {
|
||||
document.getElementById('lists-container').innerHTML =
|
||||
'<div class="col-12"><div class="alert alert-info">No list keys found.</div></div>';
|
||||
}
|
||||
|
||||
// Update hashes
|
||||
if (hashes.length > 0) {
|
||||
console.log(`Found ${hashes.length} hash keys`);
|
||||
hashes.forEach(item => {
|
||||
document.getElementById('hashes-container')
|
||||
.appendChild(createKeyCard(item.key, 'hash', item.value));
|
||||
});
|
||||
} else {
|
||||
document.getElementById('hashes-container').innerHTML =
|
||||
'<div class="col-12"><div class="alert alert-info">No hash keys found.</div></div>';
|
||||
}
|
||||
} else {
|
||||
console.error('API request failed:', data.error);
|
||||
throw new Error(data.error || 'Unknown error occurred');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
updateConnectionStatus(false, error.message || 'Failed to fetch data from Firefly database');
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load
|
||||
refreshData();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
51
pyproject.toml
Normal file
51
pyproject.toml
Normal file
@ -0,0 +1,51 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "firefly-viewer"
|
||||
version = "1.0.0"
|
||||
description = "A web-based viewer for Firefly databases"
|
||||
readme = "README.md"
|
||||
authors = [{ name = "FireflyViewer", email = "your.email@example.com" }]
|
||||
license = { file = "LICENSE" }
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Flask",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Database :: Front-Ends",
|
||||
]
|
||||
keywords = ["firefly", "database", "viewer", "web", "flask"]
|
||||
dependencies = [
|
||||
"Flask>=3.0.2",
|
||||
"redis>=5.0.1",
|
||||
"python-dotenv>=1.0.1",
|
||||
"ifireflylib>=0.2.5",
|
||||
"click>=8.1.7",
|
||||
"waitress>=2.1.2",
|
||||
]
|
||||
requires-python = ">=3.7"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer"
|
||||
Documentation = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer#readme"
|
||||
Repository = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer.git"
|
||||
"Bug Tracker" = "https://gitea.innovativesdevsolutions.org/IDSolutions/firefly-viewer/issues"
|
||||
|
||||
[project.scripts]
|
||||
firefly-viewer = "firefly.cli:main"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["firefly"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
firefly = ["templates/*"]
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
Flask
|
||||
redis
|
||||
python-dotenv
|
||||
ifireflylib
|
||||
click
|
||||
waitress
|
||||
setuptools
|
48
setup.py
Normal file
48
setup.py
Normal file
@ -0,0 +1,48 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
with open("README.md", "r", encoding="utf-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
with open("requirements.txt", "r", encoding="utf-8") as fh:
|
||||
requirements = [line.strip() for line in fh if line.strip() and not line.startswith("#")]
|
||||
|
||||
setup(
|
||||
name="firefly-viewer",
|
||||
version="1.0.0",
|
||||
author="FireflyViewer",
|
||||
author_email="your.email@example.com", # You'll need to change this
|
||||
description="A web-based viewer for Firefly databases",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
url="https://gitea.innovativedevsolutions.org/IDSolutions/firefly-viewer", # You'll need to change this
|
||||
project_urls={
|
||||
"Bug Tracker": "https://gitea.innovativedevsolutions.org/IDSolutions/firefly-viewer/issues",
|
||||
},
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=requirements,
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"firefly-viewer=firefly.cli:main",
|
||||
],
|
||||
},
|
||||
package_data={
|
||||
"firefly": ["templates/*"],
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Flask",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Database :: Front-Ends",
|
||||
],
|
||||
python_requires=">=3.7",
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user