Initial MinerOps working checkpoint

This commit is contained in:
MinerOps Admin committed 2026-07-22 20:56:41 +00:00
commit 4c854374e0
103 files changed
+2400932

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
# Secrets / environment
.env
.env.*
*.env
# Backups / dumps / generated data
backups/
data/
logs/
imports/
*.sql
*.sql.gz
*.dump
*.bak
*.csv
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.mypy_cache/
# Docker/runtime
*.log
tmp/
temp/
# OS/editor
.DS_Store
Thumbs.db
.vscode/
.idea/
+19
View File
@@ -0,0 +1,19 @@
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r /app/requirements.txt
COPY app /app/app
COPY alembic.ini /app/alembic.ini
COPY alembic /app/alembic
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
+40
View File
@@ -0,0 +1,40 @@
[alembic]
script_location = alembic
prepend_sys_path = .
sqlalchemy.url = postgresql+psycopg://minerops:dummy@postgres:5432/minerops
[post_write_hooks]
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+54
View File
@@ -0,0 +1,54 @@
from logging.config import fileConfig
from alembic import context
from app.database import DATABASE_URL, Base
config = context.config
config.set_main_option("sqlalchemy.url", DATABASE_URL)
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline() -> None:
context.configure(
url=DATABASE_URL,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
from sqlalchemy import engine_from_config
from sqlalchemy import pool
configuration = config.get_section(config.config_ini_section, {})
configuration["sqlalchemy.url"] = DATABASE_URL
connectable = engine_from_config(
configuration,
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()
+173
View File
@@ -0,0 +1,173 @@
"""create initial foundation tables
Revision ID: 0001_foundation
Revises:
Create Date: 2026-07-08
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0001_foundation"
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:
op.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto";')
op.create_table(
"roles",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("name", sa.Text(), nullable=False, unique=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_table(
"permissions",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("key", sa.Text(), nullable=False, unique=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_table(
"role_permissions",
sa.Column("role_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("roles.id", ondelete="CASCADE"), nullable=False),
sa.Column("permission_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("permissions.id", ondelete="CASCADE"), nullable=False),
sa.PrimaryKeyConstraint("role_id", "permission_id"),
)
op.create_table(
"users",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("username", sa.Text(), nullable=False, unique=True),
sa.Column("display_name", sa.Text(), nullable=False),
sa.Column("email", sa.Text(), nullable=True),
sa.Column("password_hash", sa.Text(), nullable=False),
sa.Column("role_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("roles.id"), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("last_login_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_index("idx_users_role_id", "users", ["role_id"])
op.create_index("idx_users_is_active", "users", ["is_active"])
op.create_table(
"sites",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("name", sa.Text(), nullable=False, unique=True),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_table(
"rooms",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("site_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("sites.id", ondelete="CASCADE"), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.Column("sort_order", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.UniqueConstraint("site_id", "name", name="uq_rooms_site_name"),
)
op.create_index("idx_rooms_site_id", "rooms", ["site_id"])
op.create_table(
"app_settings",
sa.Column("key", sa.Text(), primary_key=True),
sa.Column("value_json", postgresql.JSONB(), nullable=False),
sa.Column("is_sensitive", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("updated_by_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.execute("""
INSERT INTO roles (name, description) VALUES
('Admin', 'Full system administrator'),
('Technician', 'Technician user for repair and operational tasks'),
('Viewer', 'Read-only dashboard and miner viewer'),
('Auditor', 'Read-only audit and reporting user');
""")
op.execute("""
INSERT INTO permissions (key, description) VALUES
('admin.access', 'Access administrator features'),
('users.manage', 'Create and manage users'),
('miners.view', 'View miners'),
('miners.create', 'Create miners'),
('miners.update', 'Update miners'),
('miners.delete', 'Delete miners'),
('scan_ranges.manage', 'Manage scan ranges'),
('scans.run', 'Run scans'),
('credentials.manage', 'Manage miner credentials'),
('backups.create', 'Create backups'),
('exports.create', 'Create exports'),
('audit.view', 'View audit logs');
""")
op.execute("""
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
CROSS JOIN permissions p
WHERE r.name = 'Admin';
""")
op.execute("""
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.key IN (
'miners.view',
'miners.update',
'scans.run',
'exports.create'
)
WHERE r.name = 'Technician';
""")
op.execute("""
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.key IN ('miners.view')
WHERE r.name = 'Viewer';
""")
op.execute("""
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.key IN ('miners.view', 'audit.view', 'exports.create')
WHERE r.name = 'Auditor';
""")
op.execute("""
INSERT INTO sites (name, description)
VALUES ('Main Mining Site', 'Default mining site');
""")
def downgrade() -> None:
op.drop_table("app_settings")
op.drop_index("idx_rooms_site_id", table_name="rooms")
op.drop_table("rooms")
op.drop_table("sites")
op.drop_index("idx_users_is_active", table_name="users")
op.drop_index("idx_users_role_id", table_name="users")
op.drop_table("users")
op.drop_table("role_permissions")
op.drop_table("permissions")
op.drop_table("roles")
@@ -0,0 +1,50 @@
"""create audit logs table
Revision ID: 0002_audit_logs
Revises: 0001_foundation
Create Date: 2026-07-08
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0002_audit_logs"
down_revision: Union[str, None] = "0001_foundation"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"audit_logs",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("actor_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("action", sa.Text(), nullable=False),
sa.Column("entity_type", sa.Text(), nullable=False),
sa.Column("entity_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("old_value_json", postgresql.JSONB(), nullable=True),
sa.Column("new_value_json", postgresql.JSONB(), nullable=True),
sa.Column("related_miner_asset_id", postgresql.UUID(as_uuid=True), nullable=True),
sa.Column("ip_address", postgresql.INET(), nullable=True),
sa.Column("user_agent", sa.Text(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_index("idx_audit_logs_actor_user_id", "audit_logs", ["actor_user_id"])
op.create_index("idx_audit_logs_action", "audit_logs", ["action"])
op.create_index("idx_audit_logs_entity", "audit_logs", ["entity_type", "entity_id"])
op.create_index("idx_audit_logs_related_miner", "audit_logs", ["related_miner_asset_id"])
op.create_index("idx_audit_logs_created_at", "audit_logs", ["created_at"])
def downgrade() -> None:
op.drop_index("idx_audit_logs_created_at", table_name="audit_logs")
op.drop_index("idx_audit_logs_related_miner", table_name="audit_logs")
op.drop_index("idx_audit_logs_entity", table_name="audit_logs")
op.drop_index("idx_audit_logs_action", table_name="audit_logs")
op.drop_index("idx_audit_logs_actor_user_id", table_name="audit_logs")
op.drop_table("audit_logs")
@@ -0,0 +1,62 @@
"""create scan ranges table
Revision ID: 0003_scan_ranges
Revises: 0002_audit_logs
Create Date: 2026-07-08
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0003_scan_ranges"
down_revision: Union[str, None] = "0002_audit_logs"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"scan_ranges",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("room_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("rooms.id", ondelete="SET NULL"), nullable=True),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("range_type", sa.Text(), nullable=False, server_default=sa.text("'cidr'")),
sa.Column("cidr", sa.Text(), nullable=True),
sa.Column("start_ip", postgresql.INET(), nullable=True),
sa.Column("end_ip", postgresql.INET(), nullable=True),
sa.Column("enabled", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("scan_interval_seconds", sa.Integer(), nullable=False, server_default=sa.text("900")),
sa.Column("timeout_ms", sa.Integer(), nullable=False, server_default=sa.text("3000")),
sa.Column("retry_limit", sa.Integer(), nullable=False, server_default=sa.text("1")),
sa.Column("parallelism_limit", sa.Integer(), nullable=False, server_default=sa.text("10")),
sa.Column("stagger_ms", sa.Integer(), nullable=False, server_default=sa.text("250")),
sa.Column("api_port", sa.Integer(), nullable=False, server_default=sa.text("4028")),
sa.Column("last_scan_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("next_scan_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_by_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.CheckConstraint(
"""
(range_type = 'cidr' AND cidr IS NOT NULL)
OR
(range_type = 'start_end' AND start_ip IS NOT NULL AND end_ip IS NOT NULL)
""",
name="scan_range_has_valid_range",
),
)
op.create_index("idx_scan_ranges_room_id", "scan_ranges", ["room_id"])
op.create_index("idx_scan_ranges_enabled", "scan_ranges", ["enabled"])
op.create_index("idx_scan_ranges_next_scan_at", "scan_ranges", ["next_scan_at"])
def downgrade() -> None:
op.drop_index("idx_scan_ranges_next_scan_at", table_name="scan_ranges")
op.drop_index("idx_scan_ranges_enabled", table_name="scan_ranges")
op.drop_index("idx_scan_ranges_room_id", table_name="scan_ranges")
op.drop_table("scan_ranges")
@@ -0,0 +1,78 @@
"""create scan run and scan result tables
Revision ID: 0004_scan_runs_results
Revises: 0003_scan_ranges
Create Date: 2026-07-08
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0004_scan_runs_results"
down_revision: Union[str, None] = "0003_scan_ranges"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"scan_runs",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("scan_range_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("scan_ranges.id", ondelete="SET NULL"), nullable=True),
sa.Column("triggered_by", sa.Text(), nullable=False, server_default=sa.text("'manual'")),
sa.Column("triggered_by_user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("status", sa.Text(), nullable=False, server_default=sa.text("'running'")),
sa.Column("total_ips", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("scanned_ips", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("miners_found", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("errors_count", sa.Integer(), nullable=False, server_default=sa.text("0")),
sa.Column("notes", sa.Text(), nullable=True),
)
op.create_index("idx_scan_runs_scan_range_id", "scan_runs", ["scan_range_id"])
op.create_index("idx_scan_runs_started_at", "scan_runs", ["started_at"])
op.create_index("idx_scan_runs_status", "scan_runs", ["status"])
op.create_table(
"scan_results",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("scan_run_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("scan_runs.id", ondelete="CASCADE"), nullable=False),
sa.Column("ip_address", postgresql.INET(), nullable=False),
sa.Column("port", sa.Integer(), nullable=False, server_default=sa.text("4028")),
sa.Column("detected", sa.Boolean(), nullable=False, server_default=sa.text("false")),
sa.Column("mac_address", sa.Text(), nullable=True),
sa.Column("response_ms", sa.Integer(), nullable=True),
sa.Column("status", sa.Text(), nullable=False, server_default=sa.text("'unknown'")),
sa.Column("error_message", sa.Text(), nullable=True),
sa.Column("raw_summary_json", postgresql.JSONB(), nullable=True),
sa.Column("raw_version_json", postgresql.JSONB(), nullable=True),
sa.Column("raw_pools_json", postgresql.JSONB(), nullable=True),
sa.Column("raw_error_json", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_index("idx_scan_results_scan_run_id", "scan_results", ["scan_run_id"])
op.create_index("idx_scan_results_ip_address", "scan_results", ["ip_address"])
op.create_index("idx_scan_results_detected", "scan_results", ["detected"])
op.create_index("idx_scan_results_mac_address", "scan_results", [sa.text("lower(mac_address)")])
op.create_index("idx_scan_results_created_at", "scan_results", ["created_at"])
def downgrade() -> None:
op.drop_index("idx_scan_results_created_at", table_name="scan_results")
op.drop_index("idx_scan_results_mac_address", table_name="scan_results")
op.drop_index("idx_scan_results_detected", table_name="scan_results")
op.drop_index("idx_scan_results_ip_address", table_name="scan_results")
op.drop_index("idx_scan_results_scan_run_id", table_name="scan_results")
op.drop_table("scan_results")
op.drop_index("idx_scan_runs_status", table_name="scan_runs")
op.drop_index("idx_scan_runs_started_at", table_name="scan_runs")
op.drop_index("idx_scan_runs_scan_range_id", table_name="scan_runs")
op.drop_table("scan_runs")
@@ -0,0 +1,155 @@
"""create miner inventory tables
Revision ID: 0005_miner_inventory
Revises: 0004_scan_runs_results
Create Date: 2026-07-08
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0005_miner_inventory"
down_revision: Union[str, None] = "0004_scan_runs_results"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"miners",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("site_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("sites.id", ondelete="SET NULL"), nullable=True),
sa.Column("room_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("rooms.id", ondelete="SET NULL"), nullable=True),
sa.Column("asset_tag", sa.Text(), nullable=True),
sa.Column("current_mac", sa.Text(), nullable=True),
sa.Column("current_ip", postgresql.INET(), nullable=True),
sa.Column("hostname", sa.Text(), nullable=True),
sa.Column("miner_sn", sa.Text(), nullable=True),
sa.Column("power_sn", sa.Text(), nullable=True),
sa.Column("model", sa.Text(), nullable=True),
sa.Column("miner_type", sa.Text(), nullable=True),
sa.Column("api_version", sa.Text(), nullable=True),
sa.Column("firmware_version", sa.Text(), nullable=True),
sa.Column("platform", sa.Text(), nullable=True),
sa.Column("chip", sa.Text(), nullable=True),
sa.Column("status", sa.Text(), nullable=False, server_default=sa.text("'discovered'")),
sa.Column("notes", sa.Text(), nullable=True),
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_index("idx_miners_site_id", "miners", ["site_id"])
op.create_index("idx_miners_room_id", "miners", ["room_id"])
op.create_index("idx_miners_current_ip", "miners", ["current_ip"])
op.create_index("idx_miners_current_mac", "miners", ["current_mac"])
op.create_index("idx_miners_miner_sn", "miners", ["miner_sn"])
op.create_index("idx_miners_status", "miners", ["status"])
op.create_index("idx_miners_last_seen_at", "miners", ["last_seen_at"])
op.create_table(
"miner_network_identities",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("miner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("miners.id", ondelete="CASCADE"), nullable=False),
sa.Column("mac_address", sa.Text(), nullable=False),
sa.Column("ip_address", postgresql.INET(), nullable=True),
sa.Column("hostname", sa.Text(), nullable=True),
sa.Column("source", sa.Text(), nullable=False, server_default=sa.text("'scanner'")),
sa.Column("is_current", sa.Boolean(), nullable=False, server_default=sa.text("true")),
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_index("idx_miner_network_identities_miner_id", "miner_network_identities", ["miner_id"])
op.create_index("idx_miner_network_identities_mac_address", "miner_network_identities", ["mac_address"])
op.create_index("idx_miner_network_identities_ip_address", "miner_network_identities", ["ip_address"])
op.create_index("idx_miner_network_identities_is_current", "miner_network_identities", ["is_current"])
op.create_table(
"miner_hashboards",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("miner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("miners.id", ondelete="CASCADE"), nullable=False),
sa.Column("slot", sa.Integer(), nullable=False),
sa.Column("pcb_sn", sa.Text(), nullable=True),
sa.Column("chip_data", sa.Text(), nullable=True),
sa.Column("temperature_c", sa.Numeric(), nullable=True),
sa.Column("chip_frequency", sa.Numeric(), nullable=True),
sa.Column("mhs_av", sa.Numeric(), nullable=True),
sa.Column("mhs_1m", sa.Numeric(), nullable=True),
sa.Column("mhs_15m", sa.Numeric(), nullable=True),
sa.Column("hs_rt", sa.Numeric(), nullable=True),
sa.Column("factory_ghs", sa.Numeric(), nullable=True),
sa.Column("effective_chips", sa.Integer(), nullable=True),
sa.Column("upfreq_complete", sa.Integer(), nullable=True),
sa.Column("raw_json", postgresql.JSONB(), nullable=True),
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.UniqueConstraint("miner_id", "slot", name="uq_miner_hashboards_miner_slot"),
)
op.create_index("idx_miner_hashboards_miner_id", "miner_hashboards", ["miner_id"])
op.create_index("idx_miner_hashboards_pcb_sn", "miner_hashboards", ["pcb_sn"])
op.create_table(
"miner_status_snapshots",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
sa.Column("miner_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("miners.id", ondelete="CASCADE"), nullable=False),
sa.Column("observed_ip", postgresql.INET(), nullable=True),
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
sa.Column("mhs_av", sa.Numeric(), nullable=True),
sa.Column("mhs_1m", sa.Numeric(), nullable=True),
sa.Column("mhs_15m", sa.Numeric(), nullable=True),
sa.Column("hs_rt", sa.Numeric(), nullable=True),
sa.Column("power_w", sa.Numeric(), nullable=True),
sa.Column("power_rate_w_per_th", sa.Numeric(), nullable=True),
sa.Column("env_temp_c", sa.Numeric(), nullable=True),
sa.Column("chip_temp_min_c", sa.Numeric(), nullable=True),
sa.Column("chip_temp_max_c", sa.Numeric(), nullable=True),
sa.Column("chip_temp_avg_c", sa.Numeric(), nullable=True),
sa.Column("fan_speed_in_rpm", sa.Integer(), nullable=True),
sa.Column("fan_speed_out_rpm", sa.Integer(), nullable=True),
sa.Column("uptime_seconds", sa.BigInteger(), nullable=True),
sa.Column("hash_stable", sa.Boolean(), nullable=True),
sa.Column("pool_rejected_percent", sa.Numeric(), nullable=True),
sa.Column("raw_summary_json", postgresql.JSONB(), nullable=True),
sa.Column("raw_version_json", postgresql.JSONB(), nullable=True),
sa.Column("raw_miner_info_json", postgresql.JSONB(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
)
op.create_index("idx_miner_status_snapshots_miner_id", "miner_status_snapshots", ["miner_id"])
op.create_index("idx_miner_status_snapshots_observed_at", "miner_status_snapshots", ["observed_at"])
def downgrade() -> None:
op.drop_index("idx_miner_status_snapshots_observed_at", table_name="miner_status_snapshots")
op.drop_index("idx_miner_status_snapshots_miner_id", table_name="miner_status_snapshots")
op.drop_table("miner_status_snapshots")
op.drop_index("idx_miner_hashboards_pcb_sn", table_name="miner_hashboards")
op.drop_index("idx_miner_hashboards_miner_id", table_name="miner_hashboards")
op.drop_table("miner_hashboards")
op.drop_index("idx_miner_network_identities_is_current", table_name="miner_network_identities")
op.drop_index("idx_miner_network_identities_ip_address", table_name="miner_network_identities")
op.drop_index("idx_miner_network_identities_mac_address", table_name="miner_network_identities")
op.drop_index("idx_miner_network_identities_miner_id", table_name="miner_network_identities")
op.drop_table("miner_network_identities")
op.drop_index("idx_miners_last_seen_at", table_name="miners")
op.drop_index("idx_miners_status", table_name="miners")
op.drop_index("idx_miners_miner_sn", table_name="miners")
op.drop_index("idx_miners_current_mac", table_name="miners")
op.drop_index("idx_miners_current_ip", table_name="miners")
op.drop_index("idx_miners_room_id", table_name="miners")
op.drop_index("idx_miners_site_id", table_name="miners")
op.drop_table("miners")
View File
Whitespace-only changes.
+26
View File
@@ -0,0 +1,26 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "MinerOps Console"
app_env: str = "development"
app_timezone: str = "America/Winnipeg"
postgres_db: str = "minerops"
postgres_user: str = "minerops"
postgres_password: str
postgres_host: str = "postgres"
postgres_port: int = 5432
redis_url: str = "redis://redis:6379/0"
jwt_secret_key: str
jwt_access_token_minutes: int = 30
jwt_refresh_token_days: int = 14
class Config:
env_file = ".env"
extra = "ignore"
settings = Settings()
+35
View File
@@ -0,0 +1,35 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from app.config import settings
def build_database_url() -> str:
return (
f"postgresql+psycopg://{settings.postgres_user}:"
f"{settings.postgres_password}@{settings.postgres_host}:"
f"{settings.postgres_port}/{settings.postgres_db}"
)
DATABASE_URL = build_database_url()
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
+35893
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Loaded 100 of 103 files, more files were not shown because too many files have changed in this diff. Show more