prod: UI Optimisée mise en production

- Documentation archivée et réorganisée
- Backend: Ajout tests, migrations, library service, rate limiting
- Frontend: Suppression Flutter, focus sur interface web HTML/JS
- Tailwind CSS ajouté pour le style
- Améliorations UX et corrections bugs

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
root
2026-01-20 09:56:39 +00:00
parent bc03225e47
commit 801e6a050b
263 changed files with 33100 additions and 23058 deletions
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+104
View File
@@ -0,0 +1,104 @@
import sys
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
# Add the backend directory to the Python path
sys.path.insert(0, '/opt/audiOhm/backend')
# Load environment variables
from dotenv import load_dotenv
load_dotenv()
# Import settings and models
from app.core.config import settings
from app.core.database import Base
from app.models import ( # noqa: F401
album,
artist,
liked_track,
listening_history,
playlist,
playlist_track,
track,
user,
)
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Override sqlalchemy.url with the value from settings
# Convert async URL to sync URL for Alembic
database_url = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
config.set_main_option("sqlalchemy.url", database_url)
# 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)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+26
View File
@@ -0,0 +1,26 @@
"""${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, 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:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
@@ -0,0 +1,197 @@
"""Add library tables (listening_history, liked_tracks)
Revision ID: 001_add_library_tables
Revises:
Create Date: 2025-01-19 17:51:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '001_add_library_tables'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create listening_history and liked_tracks tables with indexes."""
# Create listening_history table
op.create_table(
'listening_history',
sa.Column(
'id',
postgresql.UUID(as_uuid=True),
primary_key=True,
nullable=False,
server_default=sa.text('gen_random_uuid()')
),
sa.Column(
'user_id',
postgresql.UUID(as_uuid=True),
sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False
),
sa.Column(
'track_id',
postgresql.UUID(as_uuid=True),
sa.ForeignKey('tracks.id', ondelete='CASCADE'),
nullable=False
),
sa.Column(
'played_for',
sa.Integer(),
nullable=False,
server_default='0',
comment='Duration played in seconds'
),
sa.Column(
'completed',
sa.Boolean(),
nullable=False,
server_default='false',
comment='Whether the track was played to completion'
),
sa.Column(
'source',
sa.String(length=50),
nullable=True,
comment='Playback source (library, playlist, search, etc.)'
),
sa.Column(
'played_at',
sa.DateTime(),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP')
),
sa.Column(
'created_at',
sa.DateTime(),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP')
),
comment='Listening history representing user track listening records'
)
# Create indexes for listening_history
op.create_index(
'ix_listening_history_id',
'listening_history',
['id']
)
op.create_index(
'ix_listening_history_user_id',
'listening_history',
['user_id']
)
op.create_index(
'ix_listening_history_track_id',
'listening_history',
['track_id']
)
op.create_index(
'ix_listening_history_played_at',
'listening_history',
['played_at']
)
op.create_index(
'ix_listening_history_user_played',
'listening_history',
['user_id', 'played_at']
)
op.create_index(
'ix_listening_history_user_track',
'listening_history',
['user_id', 'track_id']
)
# Create liked_tracks table
op.create_table(
'liked_tracks',
sa.Column(
'id',
postgresql.UUID(as_uuid=True),
primary_key=True,
nullable=False,
server_default=sa.text('gen_random_uuid()')
),
sa.Column(
'user_id',
postgresql.UUID(as_uuid=True),
sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False
),
sa.Column(
'track_id',
postgresql.UUID(as_uuid=True),
sa.ForeignKey('tracks.id', ondelete='CASCADE'),
nullable=False
),
sa.Column(
'notes',
sa.String(length=1000),
nullable=True,
comment='User notes about the track'
),
sa.Column(
'created_at',
sa.DateTime(),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP')
),
sa.Column(
'updated_at',
sa.DateTime(),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP')
),
comment='Liked tracks representing user favorited tracks'
)
# Create indexes for liked_tracks
op.create_index(
'ix_liked_tracks_id',
'liked_tracks',
['id']
)
op.create_index(
'ix_liked_tracks_user_id',
'liked_tracks',
['user_id']
)
op.create_index(
'ix_liked_tracks_track_id',
'liked_tracks',
['track_id']
)
op.create_index(
'ix_liked_tracks_user_track',
'liked_tracks',
['user_id', 'track_id'],
unique=True
)
def downgrade() -> None:
"""Drop liked_tracks and listening_history tables."""
# Drop liked_tracks table first (no foreign keys depend on it)
op.drop_index('ix_liked_tracks_user_track', table_name='liked_tracks')
op.drop_index('ix_liked_tracks_track_id', table_name='liked_tracks')
op.drop_index('ix_liked_tracks_user_id', table_name='liked_tracks')
op.drop_index('ix_liked_tracks_id', table_name='liked_tracks')
op.drop_table('liked_tracks')
# Drop listening_history table
op.drop_index('ix_listening_history_user_track', table_name='listening_history')
op.drop_index('ix_listening_history_user_played', table_name='listening_history')
op.drop_index('ix_listening_history_played_at', table_name='listening_history')
op.drop_index('ix_listening_history_track_id', table_name='listening_history')
op.drop_index('ix_listening_history_user_id', table_name='listening_history')
op.drop_index('ix_listening_history_id', table_name='listening_history')
op.drop_table('listening_history')