diff --git a/packetserver/http/server.py b/packetserver/http/server.py
index 0a113b5..274a9c1 100644
--- a/packetserver/http/server.py
+++ b/packetserver/http/server.py
@@ -17,6 +17,18 @@ app = FastAPI(
# Define templates EARLY (before importing dashboard)
templates = Jinja2Templates(directory=BASE_DIR / "templates")
+from datetime import datetime, timezone
+
+@templates.env.filters.register
+def timestamp_to_date(ts):
+ if ts is None:
+ return "Never"
+ try:
+ dt = datetime.fromtimestamp(float(ts), tz=timezone.utc)
+ return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
+ except (ValueError, TypeError):
+ return "Invalid"
+
# Static files
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
diff --git a/packetserver/http/templates/profile.html b/packetserver/http/templates/profile.html
index 1491a55..59eb938 100644
--- a/packetserver/http/templates/profile.html
+++ b/packetserver/http/templates/profile.html
@@ -32,8 +32,8 @@
| HTTP Enabled | {% if profile.http_enabled %}Yes{% else %}No{% endif %} |
| RF Gateway Enabled | {% if profile.rf_enabled %}Yes{% else %}No (blacklisted){% endif %} |
- | HTTP Account Created | {{ profile.http_created_at }} |
- | Last HTTP Login | {{ profile.http_last_login or "Never" }} |
+ | HTTP Account Created | {{ profile.http_created_at | timestamp_to_date }} |
+ | Last HTTP Login | {{ profile.http_last_login | timestamp_to_date or "Never" }} |
diff --git a/packetserver/runners/http_user_manager.py b/packetserver/runners/http_user_manager.py
index 680a034..3a8fdbc 100644
--- a/packetserver/runners/http_user_manager.py
+++ b/packetserver/runners/http_user_manager.py
@@ -91,6 +91,11 @@ def main():
p_dump = subparsers.add_parser("dump", help="Dump JSON details of the BBS user (incl. UUID and hidden flag)")
p_dump.add_argument("callsign", help="Callsign to dump")
+ # sync missing
+ p_sync = subparsers.add_parser("sync-missing", help="Add missing HttpUser objects for existing BBS users")
+ p_sync.add_argument("--dry-run", action="store_true", help="Show what would be done without changes")
+ p_sync.add_argument("--enable", action="store_true", help="Set http_enabled=True for new users (default False)")
+
args = parser.parse_args()
# Open the database
@@ -264,6 +269,44 @@ def main():
print(json.dumps(dump_data, indent=4))
+ elif args.command == "sync-missing":
+ import secrets
+ import string
+
+ def generate_password(length=20):
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
+ return ''.join(secrets.choice(alphabet) for _ in range(length))
+
+ bbs_users = root.get('users', {})
+ http_users = get_or_create_http_users(root)
+
+ missing = [call for call in bbs_users if call not in http_users and call != "SYSTEM"]
+ if not missing:
+ print("No missing HTTP users—all BBS users have HttpUser objects")
+ else:
+ print(f"Found {len(missing)} BBS users without HTTP accounts:")
+ for call in sorted(missing):
+ print(f" - {call}")
+
+ if args.dry_run:
+ print("\n--dry-run: No changes made")
+ else:
+ confirm_msg = f"Create {len(missing)} new HttpUser objects (http_enabled={'True' if args.enable else 'False'})?"
+ if not confirm(confirm_msg):
+ print("Aborted")
+ else:
+ created_count = 0
+ for call in missing:
+ password = generate_password() # strong random, not printed
+ new_http = HttpUser(call, password)
+ new_http.http_enabled = args.enable
+ http_users[call] = new_http
+ created_count += 1
+
+ transaction.commit()
+ print(f"\nSync complete: {created_count} HTTP users added (passwords random & hidden)")
+ print("Use 'set-password ' to set a known password before enabling login")
+
finally:
connection.close()
db.close()