From f4f712031e5173d18f1f4bc314d5eebeac15ead7 Mon Sep 17 00:00:00 2001 From: oib Date: Sun, 27 Jul 2025 07:54:24 +0200 Subject: [PATCH] Reorganize project structure - Move development and test files to dev/ directory - Update .gitignore to exclude development files - Update paths in configuration files - Add new audio-player.js for frontend --- .gitignore | 12 + alembic.ini | 2 +- alembic/README | 1 - alembic/env.py | 61 --- alembic/script.py.mako | 28 -- .../0df481ee920b_add_publicstream_model.py | 71 --- .../1ab2db0e4b5e_make_username_unique.py | 86 ---- .../8be4811023d8_add_display_name_to_user.py | 49 -- ...872_add_processed_filename_to_uploadlog.py | 30 -- database.py | 5 +- dev_user.py | 40 -- migrations/0002_add_session_tables.py | 67 --- .../add_processed_filename_to_uploadlog.py | 24 - run-navigation-test.js | 179 -------- run_migrations.py | 2 +- static/audio-player.js | 424 ++++++++++++++++++ static/test-audio-player.html | 240 ---------- static/test-audio.html | 210 --------- static/test-audio.opus | Bin 395 -> 0 bytes testmail.py | 11 - tests/profile-auth.js | 374 --------------- 21 files changed, 442 insertions(+), 1474 deletions(-) delete mode 100644 alembic/README delete mode 100644 alembic/env.py delete mode 100644 alembic/script.py.mako delete mode 100644 alembic/versions/0df481ee920b_add_publicstream_model.py delete mode 100644 alembic/versions/1ab2db0e4b5e_make_username_unique.py delete mode 100644 alembic/versions/8be4811023d8_add_display_name_to_user.py delete mode 100644 alembic/versions/f86c93c7a872_add_processed_filename_to_uploadlog.py delete mode 100644 dev_user.py delete mode 100644 migrations/0002_add_session_tables.py delete mode 100644 migrations/add_processed_filename_to_uploadlog.py delete mode 100644 run-navigation-test.js create mode 100644 static/audio-player.js delete mode 100644 static/test-audio-player.html delete mode 100644 static/test-audio.html delete mode 100644 static/test-audio.opus delete mode 100644 testmail.py delete mode 100644 tests/profile-auth.js diff --git a/.gitignore b/.gitignore index 0f64611..6bf9ca1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,18 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# Development directory +dev/ + +# Configuration files +alembic.ini +*.ini +*.conf +*.config +*.yaml +*.yml +*.toml + # IDEs and editors .vscode/ .idea/ diff --git a/alembic.ini b/alembic.ini index 532e61e..7171cb6 100644 --- a/alembic.ini +++ b/alembic.ini @@ -5,7 +5,7 @@ # this is typically a path given in POSIX (e.g. forward slashes) # format, relative to the token %(here)s which refers to the location of this # ini file -script_location = %(here)s/alembic +script_location = %(here)s/dev/alembic # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time diff --git a/alembic/README b/alembic/README deleted file mode 100644 index 98e4f9c..0000000 --- a/alembic/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py deleted file mode 100644 index e7dc43d..0000000 --- a/alembic/env.py +++ /dev/null @@ -1,61 +0,0 @@ -from logging.config import fileConfig -import os -import sys -from sqlalchemy import engine_from_config -from sqlalchemy import pool -from alembic import context - -# Add the project root to the Python path -sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - -# Import your SQLAlchemy models and engine -from models import SQLModel -from database import engine - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# Import all your SQLModel models here so that Alembic can detect them -from models import User, DBSession - -# Set the target metadata to SQLModel.metadata -target_metadata = SQLModel.metadata - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode.""" - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - compare_type=True, - ) - - with context.begin_transaction(): - context.run_migrations() - -def run_migrations_online() -> None: - """Run migrations in 'online' mode.""" - connectable = engine - - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata, - compare_type=True, - ) - - with context.begin_transaction(): - context.run_migrations() - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako deleted file mode 100644 index 1101630..0000000 --- a/alembic/script.py.mako +++ /dev/null @@ -1,28 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - """Upgrade schema.""" - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - """Downgrade schema.""" - ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/0df481ee920b_add_publicstream_model.py b/alembic/versions/0df481ee920b_add_publicstream_model.py deleted file mode 100644 index 0cf6db8..0000000 --- a/alembic/versions/0df481ee920b_add_publicstream_model.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Add PublicStream model - -Revision ID: 0df481ee920b -Revises: f86c93c7a872 -Create Date: 2025-07-19 10:02:22.902696 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '0df481ee920b' -down_revision: Union[str, Sequence[str], None] = 'f86c93c7a872' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - # First create the new publicstream table - op.create_table('publicstream', - sa.Column('uid', sa.String(), nullable=False), - sa.Column('size', sa.Integer(), nullable=False), - sa.Column('mtime', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('uid') - ) - - # Drop the foreign key constraint first - op.drop_constraint('dbsession_user_id_fkey', 'dbsession', type_='foreignkey') - - # Then drop the unique constraint - op.drop_constraint(op.f('uq_user_username'), 'user', type_='unique') - - # Create the new index - op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True) - - # Recreate the foreign key constraint - op.create_foreign_key( - 'dbsession_user_id_fkey', 'dbsession', 'user', - ['user_id'], ['username'], ondelete='CASCADE' - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - # Drop the foreign key constraint first - op.drop_constraint('dbsession_user_id_fkey', 'dbsession', type_='foreignkey') - - # Drop the index - op.drop_index(op.f('ix_user_username'), table_name='user') - - # Recreate the unique constraint - op.create_unique_constraint(op.f('uq_user_username'), 'user', ['username']) - - # Recreate the foreign key constraint - op.create_foreign_key( - 'dbsession_user_id_fkey', 'dbsession', 'user', - ['user_id'], ['username'], ondelete='CASCADE' - ) - - # Drop the publicstream table - op.drop_table('publicstream') - # ### end Alembic commands ### diff --git a/alembic/versions/1ab2db0e4b5e_make_username_unique.py b/alembic/versions/1ab2db0e4b5e_make_username_unique.py deleted file mode 100644 index 36b489b..0000000 --- a/alembic/versions/1ab2db0e4b5e_make_username_unique.py +++ /dev/null @@ -1,86 +0,0 @@ -"""make username unique - -Revision ID: 1ab2db0e4b5e -Revises: -Create Date: 2025-06-27 13:04:10.085253 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql -import sqlmodel - -# revision identifiers, used by Alembic. -revision: str = '1ab2db0e4b5e' -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # 1. First, add the unique constraint to the username column - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.create_unique_constraint('uq_user_username', ['username']) - - # 2. Now create the dbsession table with the foreign key - op.create_table('dbsession', - sa.Column('token', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('user_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('ip_address', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('user_agent', sqlmodel.sql.sqltypes.AutoString(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('expires_at', sa.DateTime(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=False), - sa.Column('last_activity', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['user.username'], ), - sa.PrimaryKeyConstraint('token') - ) - - # 3. Drop old tables if they exist - if op.get_bind().engine.dialect.has_table(op.get_bind(), 'session'): - op.drop_index(op.f('ix_session_token'), table_name='session') - op.drop_index(op.f('ix_session_user_id'), table_name='session') - op.drop_table('session') - - if op.get_bind().engine.dialect.has_table(op.get_bind(), 'publicstream'): - op.drop_table('publicstream') - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # 1. First drop the dbsession table - op.drop_table('dbsession') - - # 2. Recreate the old tables - op.create_table('publicstream', - sa.Column('uid', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('size', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('mtime', sa.INTEGER(), autoincrement=False, nullable=False), - sa.Column('updated_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('uid', name=op.f('publicstream_pkey')) - ) - - op.create_table('session', - sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('token', sa.TEXT(), autoincrement=False, nullable=True), - sa.Column('ip_address', sa.VARCHAR(), autoincrement=False, nullable=False), - sa.Column('user_agent', sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column('created_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('expires_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('last_used_at', postgresql.TIMESTAMP(), autoincrement=False, nullable=False), - sa.Column('is_active', sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint('id', name=op.f('session_pkey')) - ) - - op.create_index(op.f('ix_session_user_id'), 'session', ['user_id'], unique=False) - op.create_index(op.f('ix_session_token'), 'session', ['token'], unique=True) - - # 3. Finally, remove the unique constraint from username - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.drop_constraint('uq_user_username', type_='unique') - # ### end Alembic commands ### diff --git a/alembic/versions/8be4811023d8_add_display_name_to_user.py b/alembic/versions/8be4811023d8_add_display_name_to_user.py deleted file mode 100644 index 8f85a1e..0000000 --- a/alembic/versions/8be4811023d8_add_display_name_to_user.py +++ /dev/null @@ -1,49 +0,0 @@ -"""add_display_name_to_user - -Revision ID: 8be4811023d8 -Revises: 0df481ee920b -Create Date: 2025-07-19 19:46:01.129412 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -import sqlmodel - - -# revision identifiers, used by Alembic. -revision: str = '8be4811023d8' -down_revision: Union[str, Sequence[str], None] = '0df481ee920b' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint(op.f('dbsession_user_id_fkey'), 'dbsession', type_='foreignkey') - op.create_foreign_key(None, 'dbsession', 'user', ['user_id'], ['username']) - op.alter_column('publicstream', 'storage_bytes', - existing_type=sa.INTEGER(), - nullable=False, - existing_server_default=sa.text('0')) - op.create_index(op.f('ix_publicstream_username'), 'publicstream', ['username'], unique=False) - op.drop_column('publicstream', 'size') - op.add_column('user', sa.Column('display_name', sqlmodel.sql.sqltypes.AutoString(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('user', 'display_name') - op.add_column('publicstream', sa.Column('size', sa.INTEGER(), autoincrement=False, nullable=False)) - op.drop_index(op.f('ix_publicstream_username'), table_name='publicstream') - op.alter_column('publicstream', 'storage_bytes', - existing_type=sa.INTEGER(), - nullable=True, - existing_server_default=sa.text('0')) - op.drop_constraint(None, 'dbsession', type_='foreignkey') - op.create_foreign_key(op.f('dbsession_user_id_fkey'), 'dbsession', 'user', ['user_id'], ['username'], ondelete='CASCADE') - # ### end Alembic commands ### diff --git a/alembic/versions/f86c93c7a872_add_processed_filename_to_uploadlog.py b/alembic/versions/f86c93c7a872_add_processed_filename_to_uploadlog.py deleted file mode 100644 index 0439b64..0000000 --- a/alembic/versions/f86c93c7a872_add_processed_filename_to_uploadlog.py +++ /dev/null @@ -1,30 +0,0 @@ -"""add_processed_filename_to_uploadlog - -Revision ID: f86c93c7a872 -Revises: 1ab2db0e4b5e -Create Date: 2025-06-28 15:56:29.169668 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = 'f86c93c7a872' -down_revision: Union[str, Sequence[str], None] = '1ab2db0e4b5e' -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - op.add_column('uploadlog', - sa.Column('processed_filename', sa.String(), nullable=True), - schema=None) - - -def downgrade() -> None: - """Downgrade schema.""" - op.drop_column('uploadlog', 'processed_filename', schema=None) diff --git a/database.py b/database.py index 4b28e56..504c7ac 100644 --- a/database.py +++ b/database.py @@ -1,11 +1,14 @@ # database.py — SQLModel engine/session for PostgreSQL -from sqlmodel import create_engine, Session +from sqlmodel import create_engine, Session, SQLModel import os POSTGRES_URL = os.getenv("DATABASE_URL", "postgresql://d2s:kuTy4ZKs2VcjgDh6@localhost:5432/dictastream") engine = create_engine(POSTGRES_URL, echo=False) +# SQLAlchemy Base class for models +Base = SQLModel + def get_db(): with Session(engine) as session: yield session diff --git a/dev_user.py b/dev_user.py deleted file mode 100644 index 81bb359..0000000 --- a/dev_user.py +++ /dev/null @@ -1,40 +0,0 @@ -# dev_user.py — Script to create and confirm a dev user for dicta2stream - -import os -from sqlmodel import Session -from database import engine -from models import User, UserQuota -from datetime import datetime -import uuid - -USERNAME = os.getenv("DEV_USERNAME", "devuser") -EMAIL = os.getenv("DEV_EMAIL", "devuser@localhost") -IP = os.getenv("DEV_IP", "127.0.0.1") - -with Session(engine) as session: - user = session.get(User, EMAIL) - if not user: - token = str(uuid.uuid4()) - user = User( - email=EMAIL, - username=USERNAME, - token=token, - confirmed=True, - ip=IP, - token_created=datetime.utcnow() - ) - session.add(user) - print(f"[INFO] Created new dev user: {USERNAME} with email: {EMAIL}") - else: - user.confirmed = True - user.ip = IP - print(f"[INFO] Existing user found. Marked as confirmed: {USERNAME}") - - quota = session.get(UserQuota, USERNAME) - if not quota: - quota = UserQuota(uid=USERNAME, storage_bytes=0) - session.add(quota) - print(f"[INFO] Created quota for user: {USERNAME}") - session.commit() - print(f"[INFO] Dev user ready: {USERNAME} ({EMAIL}) — confirmed, IP={IP}") - print(f"[INFO] To use: set localStorage uid and confirmed_uid to '{USERNAME}' in your browser.") diff --git a/migrations/0002_add_session_tables.py b/migrations/0002_add_session_tables.py deleted file mode 100644 index de03907..0000000 --- a/migrations/0002_add_session_tables.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Add session and public_stream tables - -Revision ID: 0002 -Revises: 0001 -Create Date: 2023-04-01 00:00:00.000000 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = '0002' -down_revision = '0001' -branch_labels = None -depends_on = None - -def upgrade(): - # Create public_stream table - op.create_table( - 'public_stream', - sa.Column('uid', sa.String(), nullable=False, comment='User ID of the stream owner'), - sa.Column('filename', sa.String(), nullable=False, comment='Name of the audio file'), - sa.Column('size', sa.BigInteger(), nullable=False, comment='File size in bytes'), - sa.Column('mtime', sa.Float(), nullable=False, comment='Last modified time as Unix timestamp'), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False, onupdate=sa.text('now()')), - sa.PrimaryKeyConstraint('uid', 'filename') - ) - - # Create session table - op.create_table( - 'session', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.String(), nullable=False, index=True, comment='Reference to user.username'), - sa.Column('token', sa.Text(), nullable=False, index=True, comment='Random session token'), - sa.Column('ip_address', sa.String(45), nullable=False, comment='IP address of the client'), - sa.Column('user_agent', sa.Text(), nullable=True, comment='User-Agent header from the client'), - sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), - sa.Column('expires_at', sa.DateTime(), nullable=False, comment='When the session expires'), - sa.Column('last_used_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False, onupdate=sa.text('now()')), - sa.Column('is_active', sa.Boolean(), server_default=sa.text('true'), nullable=False, comment='Whether the session is active'), - sa.ForeignKeyConstraint(['user_id'], ['user.username'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - - # Create indexes - op.create_index('ix_session_user_id', 'session', ['user_id'], unique=False) - op.create_index('ix_session_token', 'session', ['token'], unique=True) - op.create_index('ix_session_expires_at', 'session', ['expires_at'], unique=False) - op.create_index('ix_session_last_used_at', 'session', ['last_used_at'], unique=False) - op.create_index('ix_public_stream_uid', 'public_stream', ['uid'], unique=False) - op.create_index('ix_public_stream_updated_at', 'public_stream', ['updated_at'], unique=False) - - -def downgrade(): - # Drop indexes first - op.drop_index('ix_session_user_id', table_name='session') - op.drop_index('ix_session_token', table_name='session') - op.drop_index('ix_session_expires_at', table_name='session') - op.drop_index('ix_session_last_used_at', table_name='session') - op.drop_index('ix_public_stream_uid', table_name='public_stream') - op.drop_index('ix_public_stream_updated_at', table_name='public_stream') - - # Drop tables - op.drop_table('session') - op.drop_table('public_stream') diff --git a/migrations/add_processed_filename_to_uploadlog.py b/migrations/add_processed_filename_to_uploadlog.py deleted file mode 100644 index f4e421e..0000000 --- a/migrations/add_processed_filename_to_uploadlog.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Add processed_filename to UploadLog - -Revision ID: add_processed_filename_to_uploadlog -Revises: -Create Date: 2025-06-28 13:20:00.000000 - -""" -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = 'add_processed_filename_to_uploadlog' -down_revision = None -branch_labels = None -depends_on = None - -def upgrade(): - # Add the processed_filename column to the uploadlog table - op.add_column('uploadlog', - sa.Column('processed_filename', sa.String(), nullable=True)) - -def downgrade(): - # Remove the processed_filename column if rolling back - op.drop_column('uploadlog', 'processed_filename') diff --git a/run-navigation-test.js b/run-navigation-test.js deleted file mode 100644 index ef2d9fa..0000000 --- a/run-navigation-test.js +++ /dev/null @@ -1,179 +0,0 @@ -const puppeteer = require('puppeteer'); -const fs = require('fs'); -const path = require('path'); - -// Configuration -const BASE_URL = 'http://localhost:8000'; // Update this if your app runs on a different URL -const TEST_ITERATIONS = 5; -const OUTPUT_DIR = path.join(__dirname, 'performance-results'); -const TIMESTAMP = new Date().toISOString().replace(/[:.]/g, '-'); - -// Ensure output directory exists -if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -// Helper function to save results -function saveResults(data, filename) { - const filepath = path.join(OUTPUT_DIR, `${filename}-${TIMESTAMP}.json`); - fs.writeFileSync(filepath, JSON.stringify(data, null, 2)); - console.log(`Results saved to ${filepath}`); - return filepath; -} - -// Test runner -async function runNavigationTest() { - const browser = await puppeteer.launch({ - headless: false, // Set to true for CI/CD - devtools: true, - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-accelerated-2d-canvas', - '--no-first-run', - '--no-zygote', - '--single-process', - '--disable-gpu', - '--js-flags="--max-old-space-size=1024"' - ] - }); - - try { - const page = await browser.newPage(); - - // Enable performance metrics - await page.setViewport({ width: 1280, height: 800 }); - await page.setDefaultNavigationTimeout(60000); - - // Set up console logging - page.on('console', msg => console.log('PAGE LOG:', msg.text())); - - // Load the performance test script - const testScript = fs.readFileSync(path.join(__dirname, 'static', 'router-perf-test.js'), 'utf8'); - - // Test guest mode - console.log('Testing guest mode...'); - await page.goto(`${BASE_URL}`, { waitUntil: 'networkidle0' }); - - // Inject and run the test - const guestResults = await page.evaluate(async (script) => { - // Inject the test script - const scriptEl = document.createElement('script'); - scriptEl.textContent = script; - document.head.appendChild(scriptEl); - - // Run the test - const test = new RouterPerfTest(); - return await test.runTest('guest'); - }, testScript); - - saveResults(guestResults, 'guest-results'); - - // Test logged-in mode (if credentials are provided) - if (process.env.LOGIN_EMAIL && process.env.LOGIN_PASSWORD) { - console.log('Testing logged-in mode...'); - - // Navigate to the test page with authentication token - console.log('Authenticating with provided token...'); - await page.goto('https://dicta2stream.net/?token=d96561d7-6c95-4e10-80f7-62d5d3a5bd04', { - waitUntil: 'networkidle0', - timeout: 60000 - }); - - // Wait for authentication to complete and verify - try { - await page.waitForSelector('body.authenticated', { - timeout: 30000, - visible: true - }); - console.log('✅ Successfully authenticated'); - - // Verify user is actually logged in - const isAuthenticated = await page.evaluate(() => { - return document.body.classList.contains('authenticated'); - }); - - if (!isAuthenticated) { - throw new Error('Authentication failed - not in authenticated state'); - } - - // Force a navigation to ensure the state is stable - await page.goto('https://dicta2stream.net/#welcome-page', { - waitUntil: 'networkidle0', - timeout: 30000 - }); - - } catch (error) { - console.error('❌ Authentication failed:', error.message); - // Take a screenshot for debugging - await page.screenshot({ path: 'auth-failure.png' }); - console.log('Screenshot saved as auth-failure.png'); - throw error; - } - - // Wait for the page to fully load after login - await page.waitForTimeout(2000); - - // Run the test in logged-in mode - const loggedInResults = await page.evaluate(async (script) => { - const test = new RouterPerfTest(); - return await test.runTest('loggedIn'); - }, testScript); - - saveResults(loggedInResults, 'loggedin-results'); - - // Generate comparison report - const comparison = { - timestamp: new Date().toISOString(), - guest: { - avg: guestResults.overall.avg, - min: guestResults.overall.min, - max: guestResults.overall.max - }, - loggedIn: { - avg: loggedInResults.overall.avg, - min: loggedInResults.overall.min, - max: loggedInResults.overall.max - }, - difference: { - ms: loggedInResults.overall.avg - guestResults.overall.avg, - percent: ((loggedInResults.overall.avg - guestResults.overall.avg) / guestResults.overall.avg) * 100 - } - }; - - const reportPath = saveResults(comparison, 'performance-comparison'); - console.log(`\nPerformance comparison report generated at: ${reportPath}`); - - // Take a screenshot of the results - await page.screenshot({ path: path.join(OUTPUT_DIR, `results-${TIMESTAMP}.png`), fullPage: true }); - - return comparison; - } - - return guestResults; - - } catch (error) { - console.error('Test failed:', error); - // Take a screenshot on error - if (page) { - await page.screenshot({ path: path.join(OUTPUT_DIR, `error-${TIMESTAMP}.png`), fullPage: true }); - } - throw error; - - } finally { - await browser.close(); - } -} - -// Run the test -runNavigationTest() - .then(results => { - console.log('Test completed successfully'); - console.log('Results:', JSON.stringify(results, null, 2)); - process.exit(0); - }) - .catch(error => { - console.error('Test failed:', error); - process.exit(1); - }); diff --git a/run_migrations.py b/run_migrations.py index 4102bd4..d004dde 100644 --- a/run_migrations.py +++ b/run_migrations.py @@ -18,7 +18,7 @@ def run_migrations(): # Set up Alembic config alembic_cfg = Config() - alembic_cfg.set_main_option("script_location", "migrations") + alembic_cfg.set_main_option("script_location", "dev/migrations") alembic_cfg.set_main_option("sqlalchemy.url", database_url) # Run migrations diff --git a/static/audio-player.js b/static/audio-player.js new file mode 100644 index 0000000..5401d17 --- /dev/null +++ b/static/audio-player.js @@ -0,0 +1,424 @@ +/** + * Audio Player Module + * A shared audio player implementation based on the working "Your Stream" player + */ + +export class AudioPlayer { + constructor() { + // Audio state + this.audioElement = null; + this.currentUid = null; + this.isPlaying = false; + this.currentButton = null; + this.audioUrl = ''; + this.lastPlayTime = 0; + this.isLoading = false; + this.loadTimeout = null; // For tracking loading timeouts + + // Create a single audio element that we'll reuse + this.audioElement = new Audio(); + this.audioElement.preload = 'none'; + this.audioElement.crossOrigin = 'anonymous'; + + // Bind methods + this.loadAndPlay = this.loadAndPlay.bind(this); + this.stop = this.stop.bind(this); + this.cleanup = this.cleanup.bind(this); + } + + /** + * Load and play audio for a specific UID + * @param {string} uid - The user ID for the audio stream + * @param {HTMLElement} button - The play/pause button element + */ + /** + * Validates that a UID is in the correct UUID format + * @param {string} uid - The UID to validate + * @returns {boolean} True if valid, false otherwise + */ + isValidUuid(uid) { + // UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uid); + } + + /** + * Logs an error and updates the button state + * @param {HTMLElement} button - The button to update + * @param {string} message - Error message to log + */ + handleError(button, message) { + console.error(message); + if (button) { + this.updateButtonState(button, 'error'); + } + } + + async loadAndPlay(uid, button) { + // Validate UID exists and is in correct format + if (!uid) { + this.handleError(button, 'No UID provided for audio playback'); + return; + } + + if (!this.isValidUuid(uid)) { + this.handleError(button, `Invalid UID format: ${uid}. Expected UUID v4 format.`); + return; + } + + // If we're in the middle of loading, check if it's for the same UID + if (this.isLoading) { + // If same UID, ignore duplicate request + if (this.currentUid === uid) { + console.log('Already loading this UID, ignoring duplicate request:', uid); + return; + } + // If different UID, queue the new request + console.log('Already loading, queuing request for UID:', uid); + setTimeout(() => this.loadAndPlay(uid, button), 500); + return; + } + + // If already playing this stream, just toggle pause/play + if (this.currentUid === uid && this.audioElement) { + try { + if (this.isPlaying) { + console.log('Pausing current playback'); + try { + this.audioElement.pause(); + this.lastPlayTime = this.audioElement.currentTime; + this.isPlaying = false; + this.updateButtonState(button, 'paused'); + } catch (pauseError) { + console.warn('Error pausing audio, continuing with state update:', pauseError); + this.isPlaying = false; + this.updateButtonState(button, 'paused'); + } + } else { + console.log('Resuming playback from time:', this.lastPlayTime); + try { + // If we have a last play time, seek to it + if (this.lastPlayTime > 0) { + this.audioElement.currentTime = this.lastPlayTime; + } + await this.audioElement.play(); + this.isPlaying = true; + this.updateButtonState(button, 'playing'); + } catch (playError) { + console.error('Error resuming playback, reloading source:', playError); + // If resume fails, try reloading the source + this.currentUid = null; // Force reload of the source + return this.loadAndPlay(uid, button); + } + } + return; // Exit after handling pause/resume + } catch (error) { + console.error('Error toggling playback:', error); + this.updateButtonState(button, 'error'); + return; + } + } + + // If we get here, we're loading a new stream + this.isLoading = true; + this.currentUid = uid; + this.currentButton = button; + this.isPlaying = true; + this.updateButtonState(button, 'loading'); + + try { + // Only clean up if switching streams + if (this.currentUid !== uid) { + this.cleanup(); + } + + // Store the current button reference + this.currentButton = button; + this.currentUid = uid; + + // Create a new audio element if we don't have one + if (!this.audioElement) { + this.audioElement = new Audio(); + } else if (this.audioElement.readyState > 0) { + // If we already have a loaded source, just play it + try { + await this.audioElement.play(); + this.isPlaying = true; + this.updateButtonState(button, 'playing'); + return; + } catch (playError) { + console.warn('Error playing existing source, will reload:', playError); + // Continue to load a new source + } + } + + // Clear any existing sources + while (this.audioElement.firstChild) { + this.audioElement.removeChild(this.audioElement.firstChild); + } + + // Set the source URL with proper encoding and cache-busting timestamp + // Using the format: /audio/{uid}/stream.opus?t={timestamp} + const timestamp = new Date().getTime(); + this.audioUrl = `/audio/${encodeURIComponent(uid)}/stream.opus?t=${timestamp}`; + console.log('Loading audio from URL:', this.audioUrl); + this.audioElement.src = this.audioUrl; + + // Load the new source (don't await, let canplay handle it) + try { + this.audioElement.load(); + // If load() doesn't throw, we'll wait for canplay event + } catch (e) { + // Ignore abort errors as they're expected during rapid toggling + if (e.name !== 'AbortError') { + console.error('Error loading audio source:', e); + this.isLoading = false; + this.updateButtonState(button, 'error'); + } + } + + // Reset the current time when loading a new source + this.audioElement.currentTime = 0; + this.lastPlayTime = 0; + + // Set up error handling + this.audioElement.onerror = (e) => { + console.error('Audio element error:', e, this.audioElement.error); + this.isLoading = false; + this.updateButtonState(button, 'error'); + }; + + // Handle when audio is ready to play + const onCanPlay = () => { + this.audioElement.removeEventListener('canplay', onCanPlay); + this.isLoading = false; + if (this.lastPlayTime > 0) { + this.audioElement.currentTime = this.lastPlayTime; + } + this.audioElement.play().then(() => { + this.isPlaying = true; + this.updateButtonState(button, 'playing'); + }).catch(e => { + console.error('Error playing after load:', e); + this.updateButtonState(button, 'error'); + }); + }; + + // Define the error handler + const errorHandler = (e) => { + console.error('Audio element error:', e, this.audioElement.error); + this.isLoading = false; + this.updateButtonState(button, 'error'); + }; + + // Define the play handler + const playHandler = () => { + // Clear any pending timeouts + if (this.loadTimeout) { + clearTimeout(this.loadTimeout); + this.loadTimeout = null; + } + + this.audioElement.removeEventListener('canplay', playHandler); + this.isLoading = false; + + if (this.lastPlayTime > 0) { + this.audioElement.currentTime = this.lastPlayTime; + } + + this.audioElement.play().then(() => { + this.isPlaying = true; + this.updateButtonState(button, 'playing'); + }).catch(e => { + console.error('Error playing after load:', e); + this.isPlaying = false; + this.updateButtonState(button, 'error'); + }); + }; + + // Add event listeners + this.audioElement.addEventListener('error', errorHandler, { once: true }); + this.audioElement.addEventListener('canplay', playHandler, { once: true }); + + // Load and play the new source + try { + await this.audioElement.load(); + // Don't await play() here, let the canplay handler handle it + + // Set a timeout to handle cases where canplay doesn't fire + this.loadTimeout = setTimeout(() => { + if (this.isLoading) { + console.warn('Audio loading timed out for UID:', uid); + this.isLoading = false; + this.updateButtonState(button, 'error'); + } + }, 10000); // 10 second timeout + + } catch (e) { + console.error('Error loading audio:', e); + this.isLoading = false; + this.updateButtonState(button, 'error'); + + // Clear any pending timeouts + if (this.loadTimeout) { + clearTimeout(this.loadTimeout); + this.loadTimeout = null; + } + } + + } catch (error) { + console.error('Error in loadAndPlay:', error); + + // Only cleanup and show error if we're still on the same track + if (this.currentUid === uid) { + this.cleanup(); + this.updateButtonState(button, 'error'); + } + } + } + + /** + * Stop playback and clean up resources + */ + stop() { + try { + if (this.audioElement) { + console.log('Stopping audio playback'); + this.audioElement.pause(); + this.lastPlayTime = this.audioElement.currentTime; + this.isPlaying = false; + if (this.currentButton) { + this.updateButtonState(this.currentButton, 'paused'); + } + } + } catch (error) { + console.error('Error stopping audio:', error); + // Don't throw, just log the error + } + } + + /** + * Clean up resources + */ + cleanup() { + // Update button state if we have a reference to the current button + if (this.currentButton) { + this.updateButtonState(this.currentButton, 'paused'); + } + + // Pause the audio and store the current time + if (this.audioElement) { + try { + try { + this.audioElement.pause(); + this.lastPlayTime = this.audioElement.currentTime; + } catch (e) { + console.warn('Error pausing audio during cleanup:', e); + } + + try { + // Clear any existing sources + while (this.audioElement.firstChild) { + this.audioElement.removeChild(this.audioElement.firstChild); + } + + // Clear the source and reset the audio element + this.audioElement.removeAttribute('src'); + try { + this.audioElement.load(); + } catch (e) { + console.warn('Error in audio load during cleanup:', e); + } + } catch (e) { + console.warn('Error cleaning up audio sources:', e); + } + } catch (e) { + console.warn('Error during audio cleanup:', e); + } + } + + // Reset state + this.currentUid = null; + this.currentButton = null; + this.audioUrl = ''; + this.isPlaying = false; + } + + /** + * Update the state of a play/pause button + * @param {HTMLElement} button - The button to update + * @param {string} state - The state to set ('playing', 'paused', 'loading', 'error') + */ + updateButtonState(button, state) { + if (!button) return; + + // Only update the current button's state + if (state === 'playing') { + // If this button is now playing, update all buttons + document.querySelectorAll('.play-pause-btn').forEach(btn => { + btn.classList.remove('playing', 'paused', 'loading', 'error'); + if (btn === button) { + btn.classList.add('playing'); + } else { + btn.classList.add('paused'); + } + }); + } else { + // For other states, just update the target button + button.classList.remove('playing', 'paused', 'loading', 'error'); + if (state) { + button.classList.add(state); + } + } + + // Update button icon and aria-label for the target button + const icon = button.querySelector('i'); + if (icon) { + if (state === 'playing') { + icon.className = 'fas fa-pause'; + button.setAttribute('aria-label', 'Pause'); + } else { + icon.className = 'fas fa-play'; + button.setAttribute('aria-label', 'Play'); + } + } + } +} + +// Create a singleton instance +export const audioPlayer = new AudioPlayer(); + +// Export utility functions for direct use +export function initAudioPlayer(container = document) { + // Set up event delegation for play/pause buttons + container.addEventListener('click', (e) => { + const playButton = e.target.closest('.play-pause-btn'); + if (!playButton) return; + + e.preventDefault(); + e.stopPropagation(); + + const uid = playButton.dataset.uid; + if (!uid) return; + + audioPlayer.loadAndPlay(uid, playButton); + }); + + // Set up event delegation for stop buttons if they exist + container.addEventListener('click', (e) => { + const stopButton = e.target.closest('.stop-btn'); + if (!stopButton) return; + + e.preventDefault(); + e.stopPropagation(); + + audioPlayer.stop(); + }); +} + +// Auto-initialize if this is the main module +if (typeof document !== 'undefined') { + document.addEventListener('DOMContentLoaded', () => { + initAudioPlayer(); + }); +} diff --git a/static/test-audio-player.html b/static/test-audio-player.html deleted file mode 100644 index 8976547..0000000 --- a/static/test-audio-player.html +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - Audio Player Test - - - -

Audio Player Test

- -
-

Test 1: Direct Audio Element

-
- -
-
- - -
-
- -
-

Test 2: Dynamic Audio Element

-
- -
-
- -
-

Test 3: Using loadProfileStream

-
- -
Not started
-
- -
-
-
- -
-

Browser Audio Support

-
Testing codec support...
-
- -
-

Console Log

-
- -
- - - - diff --git a/static/test-audio.html b/static/test-audio.html deleted file mode 100644 index 60b781a..0000000 --- a/static/test-audio.html +++ /dev/null @@ -1,210 +0,0 @@ - - - - - - Audio Player Test - - - -

Audio Player Test

- -
-

Test 1: Basic Audio Element

- -
- - -
-
- -
-

Test 2: Dynamic Audio Element

-
- -
-
- -
-

Test 3: Using loadProfileStream

-
- -
Not started
-
-
- -
-

Browser Audio Support

-
Testing codec support...
-
- -
-

Console Log

-
- -
- - - - diff --git a/static/test-audio.opus b/static/test-audio.opus deleted file mode 100644 index e133232b29d028234ffc4c7615fb7a93f2d228ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 395 zcmeZIPY-5bVt|6%CeK%am^%L-@G%Pe7nBxzq$Z{?GFmV;>;^0G2PuaGka9*KuXO3* zc1Amp@{q*zVqTz#Phwe`simHgxt^hc0Z54~5U1uP=cl9=*#ae#fs)2xNrjxuq { - const entries = list.getEntries(); - for (const entry of entries) { - // Track any task longer than 50ms as a long task - if (entry.duration > 50) { - authProfile.longTasks.push({ - startTime: entry.startTime, - duration: entry.duration, - name: entry.name || 'unknown', - type: entry.entryType - }); - } - } - }); - - // Try to observe supported entry types - try { - // Check for longtask support (not available in all browsers) - if (supportedEntryTypes.includes('longtask')) { - perfObserver.observe({ entryTypes: ['longtask'] }); - } - - // Always try to observe paint timing - try { - if (supportedEntryTypes.includes('paint')) { - perfObserver.observe({ entryTypes: ['paint'] }); - } else { - // Fallback to buffered paint observation - perfObserver.observe({ type: 'paint', buffered: true }); - } - } catch (e) { - console.debug('Paint timing not supported:', e.message); - } - - } catch (e) { - console.debug('Performance observation not supported:', e.message); - } - } - - // Instrument fetch API - window.fetch = async function(...args) { - const url = typeof args[0] === 'string' ? args[0] : args[0].url; - const isAuthRelated = isAuthOperation(url); - - if (isAuthRelated) { - const start = performance.now(); - authProfile.fetchCount++; - - try { - const response = await originalFetch.apply(this, args); - const duration = performance.now() - start; - - authProfile.operations.push({ - type: 'fetch', - url, - duration, - timestamp: new Date().toISOString(), - status: response.status, - size: response.headers.get('content-length') || 'unknown' - }); - - return response; - } catch (error) { - const duration = performance.now() - start; - authProfile.operations.push({ - type: 'fetch', - url, - duration, - timestamp: new Date().toISOString(), - error: error.message, - status: 'error' - }); - throw error; - } - } - - return originalFetch.apply(this, args); - }; - - // Instrument XHR - XMLHttpRequest.prototype.open = function(method, url) { - this._authProfile = isAuthOperation(url); - if (this._authProfile) { - this._startTime = performance.now(); - this._url = url; - authProfile.xhrCount++; - } - return originalXHROpen.apply(this, arguments); - }; - - XMLHttpRequest.prototype.send = function(body) { - if (this._authProfile) { - this.addEventListener('load', () => { - const duration = performance.now() - this._startTime; - authProfile.operations.push({ - type: 'xhr', - url: this._url, - duration, - timestamp: new Date().toISOString(), - status: this.status, - size: this.getResponseHeader('content-length') || 'unknown' - }); - }); - - this.addEventListener('error', (error) => { - const duration = performance.now() - this._startTime; - authProfile.operations.push({ - type: 'xhr', - url: this._url, - duration, - timestamp: new Date().toISOString(), - error: error.message, - status: 'error' - }); - }); - } - - return originalXHRSend.apply(this, arguments); - }; - - // Track DOM updates after navigation with more details - const observer = new MutationObserver((mutations) => { - if (document.body.classList.contains('authenticated')) { - const now = performance.now(); - const updateInfo = { - timestamp: now, - mutations: mutations.length, - addedNodes: 0, - removedNodes: 0, - attributeChanges: 0, - characterDataChanges: 0 - }; - - mutations.forEach(mutation => { - updateInfo.addedNodes += mutation.addedNodes.length || 0; - updateInfo.removedNodes += mutation.removedNodes.length || 0; - updateInfo.attributeChanges += mutation.type === 'attributes' ? 1 : 0; - updateInfo.characterDataChanges += mutation.type === 'characterData' ? 1 : 0; - }); - - authProfile.domUpdates += mutations.length; - - // Track memory usage if supported - if (window.performance && window.performance.memory) { - updateInfo.memoryUsed = performance.memory.usedJSHeapSize; - updateInfo.memoryTotal = performance.memory.totalJSHeapSize; - updateInfo.memoryLimit = performance.memory.jsHeapSizeLimit; - - authProfile.memorySamples.push({ - timestamp: now, - memory: performance.memory.usedJSHeapSize - }); - - authProfile.maxMemory = Math.max( - authProfile.maxMemory, - performance.memory.usedJSHeapSize - ); - } - - // Track frame time - requestAnimationFrame(() => { - const frameTime = performance.now() - now; - authProfile.maxFrameTime = Math.max(authProfile.maxFrameTime, frameTime); - }); - } - }); - - // Track authentication state changes - const originalAddClass = DOMTokenList.prototype.add; - const originalRemoveClass = DOMTokenList.prototype.remove; - - DOMTokenList.prototype.add = function(...args) { - if (this === document.body.classList && args[0] === 'authenticated') { - authProfile.authChecks++; - if (!authProfile.startTime) { - authProfile.startTime = performance.now(); - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - characterData: true - }); - } - } - return originalAddClass.apply(this, args); - }; - - DOMTokenList.prototype.remove = function(...args) { - if (this === document.body.classList && args[0] === 'authenticated') { - authProfile.totalTime = performance.now() - (authProfile.startTime || performance.now()); - observer.disconnect(); - } - return originalRemoveClass.apply(this, args); - }; - - // Helper to identify auth-related operations - function isAuthOperation(url) { - if (!url) return false; - const authKeywords = [ - 'auth', 'login', 'session', 'token', 'user', 'profile', - 'me', 'account', 'verify', 'validate', 'check' - ]; - return authKeywords.some(keyword => - url.toLowerCase().includes(keyword) - ); - } - - // Main function to run the profile - window.profileAuthNavigation = async function() { - // Reset profile with all metrics - Object.assign(authProfile, { - startTime: null, - operations: [], - navigationEvents: [], - fetchCount: 0, - xhrCount: 0, - domUpdates: 0, - authChecks: 0, - totalTime: 0, - maxFrameTime: 0, - longTasks: [], - memorySamples: [], - maxMemory: 0, - currentNavigation: null, - navigationStart: null - }); - - // Track navigation events - if (window.performance) { - const navObserver = new PerformanceObserver((list) => { - list.getEntries().forEach(entry => { - if (entry.entryType === 'navigation') { - authProfile.navigationEvents.push({ - type: 'navigation', - name: entry.name, - startTime: entry.startTime, - duration: entry.duration, - domComplete: entry.domComplete, - domContentLoaded: entry.domContentLoadedEventEnd, - load: entry.loadEventEnd - }); - } - }); - }); - - try { - navObserver.observe({ entryTypes: ['navigation'] }); - } catch (e) { - console.warn('Navigation timing not supported:', e); - } - } - - console.log('Starting authentication navigation profile...'); - console.log('1. Navigate to a few pages while logged in'); - console.log('2. Run getAuthProfile() to see the results'); - console.log('3. Run resetAuthProfile() to start over'); - - // Add global accessor - window.getAuthProfile = function() { - const now = performance.now(); - const duration = authProfile.startTime ? (now - authProfile.startTime) / 1000 : 0; - - console.log('%c\n=== AUTHENTICATION PROFILE RESULTS ===', 'font-weight:bold;font-size:1.2em'); - - // Summary - console.log('\n%c--- PERFORMANCE SUMMARY ---', 'font-weight:bold'); - console.log(`Total Monitoring Time: ${duration.toFixed(2)}s`); - console.log(`Authentication Checks: ${authProfile.authChecks}`); - console.log(`Fetch API Calls: ${authProfile.fetchCount}`); - console.log(`XHR Requests: ${authProfile.xhrCount}`); - console.log(`DOM Updates: ${authProfile.domUpdates}`); - console.log(`Max Frame Time: ${authProfile.maxFrameTime.toFixed(2)}ms`); - - // Memory usage - if (authProfile.memorySamples.length > 0) { - const lastMem = authProfile.memorySamples[authProfile.memorySamples.length - 1]; - console.log(`\n%c--- MEMORY USAGE ---`, 'font-weight:bold'); - console.log(`Max Memory Used: ${(authProfile.maxMemory / 1024 / 1024).toFixed(2)} MB`); - console.log(`Current Memory: ${(lastMem.memory / 1024 / 1024).toFixed(2)} MB`); - } - - // Long tasks - if (authProfile.longTasks.length > 0) { - console.log(`\n%c--- LONG TASKS (${authProfile.longTasks.length} > 50ms) ---`, 'font-weight:bold'); - authProfile.longTasks - .sort((a, b) => b.duration - a.duration) - .slice(0, 5) - .forEach((task, i) => { - console.log(`#${i + 1} ${task.name}: ${task.duration.toFixed(2)}ms`); - }); - } - - // Slow operations - if (authProfile.operations.length > 0) { - console.log('\n%c--- SLOW OPERATIONS ---', 'font-weight:bold'); - const sortedOps = [...authProfile.operations].sort((a, b) => b.duration - a.duration); - sortedOps.slice(0, 10).forEach((op, i) => { - const memUsage = op.memory ? ` | +${(op.memory / 1024).toFixed(2)}KB` : ''; - console.log(`#${i + 1} [${op.type.toUpperCase()}] ${op.url || 'unknown'}: ${op.duration.toFixed(2)}ms${memUsage}`); - }); - } - - return authProfile; - }; - - window.resetAuthProfile = function() { - Object.assign(authProfile, { - startTime: null, - operations: [], - fetchCount: 0, - xhrCount: 0, - domUpdates: 0, - authChecks: 0, - totalTime: 0 - }); - console.log('Authentication profile has been reset'); - }; - }; - - console.log('Authentication profiler loaded. Run profileAuthNavigation() to start.'); -})();