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