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
This commit is contained in:
12
.gitignore
vendored
12
.gitignore
vendored
@ -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/
|
||||
|
@ -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
|
||||
|
@ -1 +0,0 @@
|
||||
Generic single-database configuration.
|
@ -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()
|
@ -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"}
|
@ -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 ###
|
@ -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 ###
|
@ -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 ###
|
@ -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)
|
@ -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
|
||||
|
40
dev_user.py
40
dev_user.py
@ -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.")
|
@ -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')
|
@ -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')
|
@ -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);
|
||||
});
|
@ -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
|
||||
|
424
static/audio-player.js
Normal file
424
static/audio-player.js
Normal file
@ -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();
|
||||
});
|
||||
}
|
@ -1,240 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audio Player Test</title>
|
||||
<style>
|
||||
:root {
|
||||
--success: #2e8b57;
|
||||
--error: #ff4444;
|
||||
--border: #444;
|
||||
--text-color: #f0f0f0;
|
||||
--surface: #2a2a2a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
background: #1a1a1a;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.test-case {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.success { color: var(--success); }
|
||||
.error { color: var(--error); }
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
background: #4a6fa5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
background: #3a5a8c;
|
||||
}
|
||||
#log {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.audio-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
audio {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Audio Player Test</h1>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Test 1: Direct Audio Element</h2>
|
||||
<div class="audio-container">
|
||||
<audio id="direct-audio" controls>
|
||||
<source src="/audio/devuser/stream.opus" type="audio/ogg; codecs=opus">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
<div>
|
||||
<button onclick="document.getElementById('direct-audio').play()">Play</button>
|
||||
<button onclick="document.getElementById('direct-audio').pause()">Pause</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Test 2: Dynamic Audio Element</h2>
|
||||
<div id="dynamic-audio-container">
|
||||
<button onclick="setupDynamicAudio()">Initialize Dynamic Audio</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Test 3: Using loadProfileStream</h2>
|
||||
<div id="load-profile-container">
|
||||
<button onclick="testLoadProfileStream()">Test loadProfileStream</button>
|
||||
<div id="test3-status">Not started</div>
|
||||
<div class="audio-container">
|
||||
<audio id="profile-audio" controls></audio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Browser Audio Support</h2>
|
||||
<div id="codec-support">Testing codec support...</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Console Log</h2>
|
||||
<div id="log"></div>
|
||||
<button onclick="document.getElementById('log').innerHTML = ''">Clear Log</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Logging function
|
||||
function log(message, type = 'info') {
|
||||
const logDiv = document.getElementById('log');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = type;
|
||||
entry.textContent = `[${new Date().toISOString()}] ${message}`;
|
||||
logDiv.appendChild(entry);
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
|
||||
// Test 2: Dynamic Audio Element
|
||||
function setupDynamicAudio() {
|
||||
log('Setting up dynamic audio element...');
|
||||
const container = document.getElementById('dynamic-audio-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
try {
|
||||
const audio = document.createElement('audio');
|
||||
audio.controls = true;
|
||||
audio.preload = 'auto';
|
||||
audio.crossOrigin = 'anonymous';
|
||||
|
||||
const source = document.createElement('source');
|
||||
source.src = '/audio/devuser/stream.opus';
|
||||
source.type = 'audio/ogg; codecs=opus';
|
||||
|
||||
audio.appendChild(source);
|
||||
container.appendChild(audio);
|
||||
container.appendChild(document.createElement('br'));
|
||||
|
||||
const playBtn = document.createElement('button');
|
||||
playBtn.textContent = 'Play';
|
||||
playBtn.onclick = () => {
|
||||
audio.play().catch(e => log(`Play error: ${e}`, 'error'));
|
||||
};
|
||||
container.appendChild(playBtn);
|
||||
|
||||
const pauseBtn = document.createElement('button');
|
||||
pauseBtn.textContent = 'Pause';
|
||||
pauseBtn.onclick = () => audio.pause();
|
||||
container.appendChild(pauseBtn);
|
||||
|
||||
log('Dynamic audio element created successfully');
|
||||
} catch (e) {
|
||||
log(`Error creating dynamic audio: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: loadProfileStream
|
||||
async function testLoadProfileStream() {
|
||||
const status = document.getElementById('test3-status');
|
||||
status.textContent = 'Loading...';
|
||||
status.className = '';
|
||||
|
||||
try {
|
||||
// Import the loadProfileStream function from app.js
|
||||
const { loadProfileStream } = await import('./app.js');
|
||||
|
||||
if (typeof loadProfileStream !== 'function') {
|
||||
throw new Error('loadProfileStream function not found');
|
||||
}
|
||||
|
||||
// Call loadProfileStream with test user
|
||||
const audio = await loadProfileStream('devuser');
|
||||
|
||||
if (audio) {
|
||||
status.textContent = 'Audio loaded successfully!';
|
||||
status.className = 'success';
|
||||
log('Audio loaded successfully', 'success');
|
||||
|
||||
// Add the audio element to the page
|
||||
const audioContainer = document.querySelector('#load-profile-container .audio-container');
|
||||
audioContainer.innerHTML = '';
|
||||
audio.controls = true;
|
||||
audioContainer.appendChild(audio);
|
||||
} else {
|
||||
status.textContent = 'No audio available for test user';
|
||||
status.className = '';
|
||||
log('No audio available for test user', 'info');
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = `Error: ${e.message}`;
|
||||
status.className = 'error';
|
||||
log(`Error in loadProfileStream: ${e}`, 'error');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Check browser audio support
|
||||
function checkAudioSupport() {
|
||||
const supportDiv = document.getElementById('codec-support');
|
||||
const audio = document.createElement('audio');
|
||||
|
||||
const codecs = {
|
||||
'audio/ogg; codecs=opus': 'Opus (OGG)',
|
||||
'audio/webm; codecs=opus': 'Opus (WebM)',
|
||||
'audio/mp4; codecs=mp4a.40.2': 'AAC (MP4)',
|
||||
'audio/mpeg': 'MP3'
|
||||
};
|
||||
|
||||
let results = [];
|
||||
|
||||
for (const [type, name] of Object.entries(codecs)) {
|
||||
const canPlay = audio.canPlayType(type);
|
||||
results.push(`${name}: ${canPlay || 'Not supported'}`);
|
||||
}
|
||||
|
||||
supportDiv.innerHTML = results.join('<br>');
|
||||
}
|
||||
|
||||
// Initialize tests
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
log('Test page loaded');
|
||||
checkAudioSupport();
|
||||
|
||||
// Log audio element events for debugging
|
||||
const audioElements = document.getElementsByTagName('audio');
|
||||
Array.from(audioElements).forEach((audio, index) => {
|
||||
['play', 'pause', 'error', 'stalled', 'suspend', 'abort', 'emptied', 'ended'].forEach(event => {
|
||||
audio.addEventListener(event, (e) => {
|
||||
log(`Audio ${index + 1} ${event} event: ${e.type}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,210 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Audio Player Test</title>
|
||||
<style>
|
||||
:root {
|
||||
--success: #2e8b57;
|
||||
--error: #ff4444;
|
||||
--border: #444;
|
||||
--text-color: #f0f0f0;
|
||||
--surface: #2a2a2a;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
background: #1a1a1a;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.test-case {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
background: var(--surface);
|
||||
}
|
||||
.success { color: var(--success); }
|
||||
.error { color: var(--error); }
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
background: #4a6fa5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:hover {
|
||||
background: #3a5a8c;
|
||||
}
|
||||
#log {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Audio Player Test</h1>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Test 1: Basic Audio Element</h2>
|
||||
<audio id="test1" controls>
|
||||
<source src="/static/test-audio.opus" type="audio/ogg; codecs=opus">
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
<div>
|
||||
<button onclick="document.getElementById('test1').play()">Play</button>
|
||||
<button onclick="document.getElementById('test1').pause()">Pause</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Test 2: Dynamic Audio Element</h2>
|
||||
<div id="test2-container">
|
||||
<button onclick="setupTest2()">Initialize Audio</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Test 3: Using loadProfileStream</h2>
|
||||
<div id="test3-container">
|
||||
<button onclick="testLoadProfileStream()">Test loadProfileStream</button>
|
||||
<div id="test3-status">Not started</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Browser Audio Support</h2>
|
||||
<div id="codec-support">Testing codec support...</div>
|
||||
</div>
|
||||
|
||||
<div class="test-case">
|
||||
<h2>Console Log</h2>
|
||||
<div id="log"></div>
|
||||
<button onclick="document.getElementById('log').innerHTML = ''">Clear Log</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Logging function
|
||||
function log(message, type = 'info') {
|
||||
const logDiv = document.getElementById('log');
|
||||
const entry = document.createElement('div');
|
||||
entry.className = type;
|
||||
entry.textContent = `[${new Date().toISOString()}] ${message}`;
|
||||
logDiv.appendChild(entry);
|
||||
logDiv.scrollTop = logDiv.scrollHeight;
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
|
||||
// Test 2: Dynamic Audio Element
|
||||
function setupTest2() {
|
||||
log('Setting up dynamic audio element...');
|
||||
const container = document.getElementById('test2-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
try {
|
||||
const audio = document.createElement('audio');
|
||||
audio.controls = true;
|
||||
audio.preload = 'auto';
|
||||
|
||||
const source = document.createElement('source');
|
||||
source.src = '/static/test-audio.opus';
|
||||
source.type = 'audio/ogg; codecs=opus';
|
||||
|
||||
audio.appendChild(source);
|
||||
container.appendChild(audio);
|
||||
container.appendChild(document.createElement('br'));
|
||||
|
||||
const playBtn = document.createElement('button');
|
||||
playBtn.textContent = 'Play';
|
||||
playBtn.onclick = () => audio.play().catch(e => log(`Play error: ${e}`, 'error'));
|
||||
container.appendChild(playBtn);
|
||||
|
||||
const pauseBtn = document.createElement('button');
|
||||
pauseBtn.textContent = 'Pause';
|
||||
pauseBtn.onclick = () => audio.pause();
|
||||
container.appendChild(pauseBtn);
|
||||
|
||||
log('Dynamic audio element created successfully');
|
||||
} catch (e) {
|
||||
log(`Error creating dynamic audio: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test 3: loadProfileStream
|
||||
async function testLoadProfileStream() {
|
||||
const status = document.getElementById('test3-status');
|
||||
status.textContent = 'Loading...';
|
||||
status.className = '';
|
||||
|
||||
try {
|
||||
// Create a test user ID
|
||||
const testUid = 'test-user-' + Math.random().toString(36).substr(2, 8);
|
||||
log(`Testing with user: ${testUid}`);
|
||||
|
||||
// Call loadProfileStream
|
||||
const audio = await window.loadProfileStream(testUid);
|
||||
|
||||
if (audio) {
|
||||
status.textContent = 'Audio loaded successfully!';
|
||||
status.className = 'success';
|
||||
log('Audio loaded successfully', 'success');
|
||||
} else {
|
||||
status.textContent = 'No audio available for test user';
|
||||
status.className = '';
|
||||
log('No audio available for test user', 'info');
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = `Error: ${e.message}`;
|
||||
status.className = 'error';
|
||||
log(`Error in loadProfileStream: ${e}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Check browser audio support
|
||||
function checkAudioSupport() {
|
||||
const supportDiv = document.getElementById('codec-support');
|
||||
const audio = document.createElement('audio');
|
||||
|
||||
const codecs = {
|
||||
'audio/ogg; codecs=opus': 'Opus (OGG)',
|
||||
'audio/webm; codecs=opus': 'Opus (WebM)',
|
||||
'audio/mp4; codecs=mp4a.40.2': 'AAC (MP4)',
|
||||
'audio/mpeg': 'MP3'
|
||||
};
|
||||
|
||||
let results = [];
|
||||
|
||||
for (const [type, name] of Object.entries(codecs)) {
|
||||
const canPlay = audio.canPlayType(type);
|
||||
results.push(`${name}: ${canPlay || 'Not supported'}`);
|
||||
}
|
||||
|
||||
supportDiv.innerHTML = results.join('<br>');
|
||||
}
|
||||
|
||||
// Initialize tests
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
log('Test page loaded');
|
||||
checkAudioSupport();
|
||||
|
||||
// Expose loadProfileStream for testing
|
||||
if (!window.loadProfileStream) {
|
||||
log('Warning: loadProfileStream not found in global scope', 'warning');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
11
testmail.py
11
testmail.py
@ -1,11 +0,0 @@
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["From"] = "test@keisanki.net"
|
||||
msg["To"] = "oib@bubuit.net"
|
||||
msg["Subject"] = "Test"
|
||||
msg.set_content("Hello world")
|
||||
|
||||
with smtplib.SMTP("localhost") as smtp:
|
||||
smtp.send_message(msg)
|
@ -1,374 +0,0 @@
|
||||
/**
|
||||
* Authentication Profiling Tool for dicta2stream
|
||||
*
|
||||
* This script profiles the authentication-related operations during navigation
|
||||
* to identify performance bottlenecks in the logged-in experience.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Open browser console (F12 → Console)
|
||||
* 2. Copy and paste this entire script
|
||||
* 3. Run: profileAuthNavigation()
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Store original methods we want to profile
|
||||
const originalFetch = window.fetch;
|
||||
const originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
const originalXHRSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
// Track authentication-related operations with detailed metrics
|
||||
const authProfile = {
|
||||
// Core metrics
|
||||
startTime: null,
|
||||
operations: [],
|
||||
navigationEvents: [],
|
||||
|
||||
// Counters
|
||||
fetchCount: 0,
|
||||
xhrCount: 0,
|
||||
domUpdates: 0,
|
||||
authChecks: 0,
|
||||
|
||||
// Performance metrics
|
||||
totalTime: 0,
|
||||
maxFrameTime: 0,
|
||||
longTasks: [],
|
||||
|
||||
// Memory tracking
|
||||
memorySamples: [],
|
||||
maxMemory: 0,
|
||||
|
||||
// Navigation tracking
|
||||
currentNavigation: null,
|
||||
navigationStart: null
|
||||
};
|
||||
|
||||
// Track long tasks and performance metrics
|
||||
if (window.PerformanceObserver) {
|
||||
// Check which entry types are supported
|
||||
const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
|
||||
|
||||
// Only set up observers for supported types
|
||||
const perfObserver = new PerformanceObserver((list) => {
|
||||
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.');
|
||||
})();
|
Reference in New Issue
Block a user