ops: alembic baseline, pre-commit, TIGER cleanup, pg_dump scripts
- backend/alembic/* — alembic infra (env.py, script.py.mako). versions/ empty for now; first migration goes in Stage 2a when models are finalized. - backend/Dockerfile: bake alembic.ini + alembic/ into the image so `docker compose exec backend alembic upgrade head` works in prod. - backend/db/init/99_drop_unused_extensions.sql + bind-mount in both compose files: drops postgis-image's TIGER/topology/fuzzystrmatch on fresh volumes. - .pre-commit-config.yaml + pre-commit in dev deps: ruff/prettier on commit to stop CI failures like UP035 from leaking out. - ops/backup.sh, ops/restore.sh: pg_dump cron script with optional Selectel S3 upload. 7-day local retention. Restore guard: requires RESTORE_CONFIRM=yes. - Makefile: new targets `make migration NAME=...`, `make pre-commit-install`. - backend/.env.example: SENTRY_DSN comment with sentry.io reference.
This commit is contained in:
parent
68e95aa1ce
commit
4f034bd86b
16 changed files with 448 additions and 13 deletions
39
.pre-commit-config.yaml
Normal file
39
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Local pre-commit hooks. Install once with:
|
||||||
|
# pip install pre-commit
|
||||||
|
# pre-commit install
|
||||||
|
#
|
||||||
|
# After install: every `git commit` runs these checks against staged files.
|
||||||
|
# Failures abort the commit.
|
||||||
|
|
||||||
|
repos:
|
||||||
|
# Generic hygiene
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.6.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
exclude: ^backend/alembic/script.py.mako$
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: ["--maxkb=512"]
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: detect-private-key
|
||||||
|
|
||||||
|
# Python — ruff (lint + format) on backend/
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.7.4
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix]
|
||||||
|
files: ^backend/
|
||||||
|
- id: ruff-format
|
||||||
|
files: ^backend/
|
||||||
|
|
||||||
|
# Frontend — prettier on TS/TSX/JSON
|
||||||
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
|
rev: v4.0.0-alpha.8
|
||||||
|
hooks:
|
||||||
|
- id: prettier
|
||||||
|
files: ^frontend/.*\.(ts|tsx|js|jsx|json|css|md)$
|
||||||
|
exclude: ^frontend/(node_modules|\.next|package-lock\.json)/
|
||||||
29
Makefile
29
Makefile
|
|
@ -1,15 +1,17 @@
|
||||||
.PHONY: help up down logs backend-shell migrate test lint typecheck codegen
|
.PHONY: help up down logs backend-shell migrate migration test lint typecheck codegen pre-commit-install
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "make up - start all services (Postgres, Redis, backend, frontend)"
|
@echo "make up - start all services (Postgres, Redis, backend, frontend)"
|
||||||
@echo "make down - stop all services"
|
@echo "make down - stop all services"
|
||||||
@echo "make logs - tail logs from all services"
|
@echo "make logs - tail logs from all services"
|
||||||
@echo "make backend-shell - shell into backend container"
|
@echo "make backend-shell - shell into backend container"
|
||||||
@echo "make migrate - run alembic upgrade head"
|
@echo "make migrate - run alembic upgrade head"
|
||||||
@echo "make test - run backend pytest"
|
@echo "make migration NAME=... - autogenerate a new alembic migration"
|
||||||
@echo "make lint - ruff check"
|
@echo "make test - run backend pytest"
|
||||||
@echo "make typecheck - mypy on core modules"
|
@echo "make lint - ruff check"
|
||||||
@echo "make codegen - regenerate frontend TS types from backend OpenAPI"
|
@echo "make typecheck - mypy on core modules"
|
||||||
|
@echo "make codegen - regenerate frontend TS types from backend OpenAPI"
|
||||||
|
@echo "make pre-commit-install - install local git hooks (runs ruff/prettier on commit)"
|
||||||
|
|
||||||
up:
|
up:
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
@ -26,6 +28,10 @@ backend-shell:
|
||||||
migrate:
|
migrate:
|
||||||
docker compose exec backend alembic upgrade head
|
docker compose exec backend alembic upgrade head
|
||||||
|
|
||||||
|
migration:
|
||||||
|
@if [ -z "$(NAME)" ]; then echo "Usage: make migration NAME=add_parcel_table"; exit 1; fi
|
||||||
|
docker compose exec backend alembic revision --autogenerate -m "$(NAME)"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cd backend && uv run pytest -q
|
cd backend && uv run pytest -q
|
||||||
|
|
||||||
|
|
@ -37,3 +43,6 @@ typecheck:
|
||||||
|
|
||||||
codegen:
|
codegen:
|
||||||
cd frontend && npm run codegen
|
cd frontend && npm run codegen
|
||||||
|
|
||||||
|
pre-commit-install:
|
||||||
|
pip install pre-commit && pre-commit install
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
# Hostname-related defaults assume Docker Compose context (this is the recommended way to run).
|
# Hostname-related defaults assume Docker Compose context (recommended).
|
||||||
# For native development without Docker, swap "postgres" → "localhost" and "redis" → "localhost".
|
# For native dev without Docker, swap "postgres" → "localhost" and "redis" → "localhost".
|
||||||
|
|
||||||
DATABASE_URL=postgresql+psycopg://gendesign:gendesign@postgres:5432/gendesign
|
DATABASE_URL=postgresql+psycopg://gendesign:gendesign@postgres:5432/gendesign
|
||||||
REDIS_URL=redis://redis:6379/0
|
REDIS_URL=redis://redis:6379/0
|
||||||
CORS_ORIGINS=["http://localhost:3000"]
|
CORS_ORIGINS=["http://localhost:3000"]
|
||||||
ENVIRONMENT=dev
|
ENVIRONMENT=dev
|
||||||
|
|
||||||
|
# Sentry: create a project at https://sentry.io/, paste the DSN here.
|
||||||
|
# Empty string = Sentry disabled. main.py initializes only if non-empty.
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|
||||||
|
# External APIs (Stage 2)
|
||||||
ROSREESTR_PKK_BASE_URL=https://pkk.rosreestr.ru/api/features/1
|
ROSREESTR_PKK_BASE_URL=https://pkk.rosreestr.ru/api/features/1
|
||||||
OVERPASS_URL=https://overpass-api.de/api/interpreter
|
OVERPASS_URL=https://overpass-api.de/api/interpreter
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ COPY pyproject.toml uv.lock* ./
|
||||||
RUN if [ -f uv.lock ]; then uv sync --frozen --no-dev; else uv sync --no-dev; fi
|
RUN if [ -f uv.lock ]; then uv sync --frozen --no-dev; else uv sync --no-dev; fi
|
||||||
|
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
|
COPY alembic.ini ./
|
||||||
|
COPY alembic ./alembic
|
||||||
|
|
||||||
|
|
||||||
# ---- runner ----
|
# ---- runner ----
|
||||||
|
|
@ -51,6 +53,8 @@ WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder --chown=app:app /app/.venv /app/.venv
|
COPY --from=builder --chown=app:app /app/.venv /app/.venv
|
||||||
COPY --from=builder --chown=app:app /app/app /app/app
|
COPY --from=builder --chown=app:app /app/app /app/app
|
||||||
|
COPY --from=builder --chown=app:app /app/alembic.ini /app/alembic.ini
|
||||||
|
COPY --from=builder --chown=app:app /app/alembic /app/alembic
|
||||||
|
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
|
|
|
||||||
43
backend/alembic.ini
Normal file
43
backend/alembic.ini
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Alembic config.
|
||||||
|
# Database URL is loaded from app.core.config.settings (which reads .env)
|
||||||
|
# in alembic/env.py — do NOT hardcode it here.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
prepend_sys_path = .
|
||||||
|
version_path_separator = os
|
||||||
|
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(slug)s
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
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
|
||||||
62
backend/alembic/env.py
Normal file
62
backend/alembic/env.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
"""Alembic env. Loads DB URL from app settings, registers all SQLAlchemy
|
||||||
|
models so autogenerate can detect schema drift."""
|
||||||
|
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.db import Base
|
||||||
|
|
||||||
|
# Import models so they register on Base.metadata.
|
||||||
|
# Add new model modules here as they appear.
|
||||||
|
from app.models import parcel # noqa: F401
|
||||||
|
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Inject runtime DB URL.
|
||||||
|
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||||
|
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
"""Generate SQL without a live DB connection."""
|
||||||
|
context.configure(
|
||||||
|
url=config.get_main_option("sqlalchemy.url"),
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
compare_type=True,
|
||||||
|
compare_server_default=True,
|
||||||
|
)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations against a live DB."""
|
||||||
|
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,
|
||||||
|
compare_type=True,
|
||||||
|
compare_server_default=True,
|
||||||
|
)
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
26
backend/alembic/script.py.mako
Normal file
26
backend/alembic/script.py.mako
Normal 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
backend/alembic/versions/.gitkeep
Normal file
0
backend/alembic/versions/.gitkeep
Normal file
23
backend/db/init/99_drop_unused_extensions.sql
Normal file
23
backend/db/init/99_drop_unused_extensions.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
-- Drops postgis-image extras that we don't use.
|
||||||
|
--
|
||||||
|
-- The postgis/postgis:16-3.4 base image's own init (10_postgis.sh) installs:
|
||||||
|
-- postgis, postgis_topology, fuzzystrmatch, postgis_tiger_geocoder
|
||||||
|
--
|
||||||
|
-- We only need `postgis` proper. The TIGER geocoder is a US Census Bureau dataset
|
||||||
|
-- (~35 tables, ~10 MB) that's irrelevant for РФ-region work. Topology and
|
||||||
|
-- fuzzystrmatch are unused too.
|
||||||
|
--
|
||||||
|
-- This script lives in /docker-entrypoint-initdb.d/ and runs ONCE on a fresh
|
||||||
|
-- volume, alphabetically AFTER 10_postgis.sh — so it sees a fully-extended DB
|
||||||
|
-- and trims the parts we don't need.
|
||||||
|
--
|
||||||
|
-- IF EXISTS + CASCADE: idempotent, won't fail if extension is already gone.
|
||||||
|
|
||||||
|
DROP EXTENSION IF EXISTS postgis_tiger_geocoder CASCADE;
|
||||||
|
DROP EXTENSION IF EXISTS postgis_topology CASCADE;
|
||||||
|
DROP EXTENSION IF EXISTS fuzzystrmatch CASCADE;
|
||||||
|
|
||||||
|
-- Empty schemas the dropped extensions left behind.
|
||||||
|
DROP SCHEMA IF EXISTS tiger CASCADE;
|
||||||
|
DROP SCHEMA IF EXISTS tiger_data CASCADE;
|
||||||
|
DROP SCHEMA IF EXISTS topology CASCADE;
|
||||||
|
|
@ -34,6 +34,7 @@ dev = [
|
||||||
"ruff>=0.5.0",
|
"ruff>=0.5.0",
|
||||||
"mypy>=1.10.0",
|
"mypy>=1.10.0",
|
||||||
"types-redis>=4.6.0",
|
"types-redis>=4.6.0",
|
||||||
|
"pre-commit>=3.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
100
backend/uv.lock
generated
100
backend/uv.lock
generated
|
|
@ -227,6 +227,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfgv"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.3.3"
|
version = "8.3.3"
|
||||||
|
|
@ -351,6 +360,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
|
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "et-xmlfile"
|
name = "et-xmlfile"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|
@ -407,6 +425,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.29.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fonttools"
|
name = "fonttools"
|
||||||
version = "4.62.1"
|
version = "4.62.1"
|
||||||
|
|
@ -486,6 +513,7 @@ dependencies = [
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
|
{ name = "pre-commit" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
|
|
@ -520,6 +548,7 @@ requires-dist = [
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "mypy", specifier = ">=1.10.0" },
|
{ name = "mypy", specifier = ">=1.10.0" },
|
||||||
|
{ name = "pre-commit", specifier = ">=3.7.0" },
|
||||||
{ name = "pytest", specifier = ">=8.0.0" },
|
{ name = "pytest", specifier = ">=8.0.0" },
|
||||||
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
|
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
|
||||||
{ name = "ruff", specifier = ">=0.5.0" },
|
{ name = "ruff", specifier = ">=0.5.0" },
|
||||||
|
|
@ -661,6 +690,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "identify"
|
||||||
|
version = "2.6.19"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.13"
|
version = "3.13"
|
||||||
|
|
@ -890,6 +928,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "2.4.4"
|
version = "2.4.4"
|
||||||
|
|
@ -1102,6 +1149,15 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.9.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
|
|
@ -1111,6 +1167,22 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pre-commit"
|
||||||
|
version = "4.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cfgv" },
|
||||||
|
{ name = "identify" },
|
||||||
|
{ name = "nodeenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "virtualenv" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prompt-toolkit"
|
name = "prompt-toolkit"
|
||||||
version = "3.0.52"
|
version = "3.0.52"
|
||||||
|
|
@ -1470,6 +1542,19 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-discovery"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "filelock" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|
@ -1997,6 +2082,21 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "21.2.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "distlib" },
|
||||||
|
{ name = "filelock" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "python-discovery" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchfiles"
|
name = "watchfiles"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ services:
|
||||||
- "127.0.0.1:5432:5432"
|
- "127.0.0.1:5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./backend/db/init:/docker-entrypoint-initdb.d:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ services:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./backend/db/init:/docker-entrypoint-initdb.d:ro
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-gendesign}"]
|
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-gendesign}"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
|
|
|
||||||
73
ops/backup.sh
Normal file
73
ops/backup.sh
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Daily Postgres backup script. Designed to run from cron on the prod VM.
|
||||||
|
#
|
||||||
|
# Usage (one-time setup on VM):
|
||||||
|
# sudo cp /opt/gendesign/ops/backup.sh /usr/local/bin/gendesign-backup
|
||||||
|
# sudo chmod +x /usr/local/bin/gendesign-backup
|
||||||
|
# crontab -e
|
||||||
|
# # Run daily at 03:30 Moscow time
|
||||||
|
# 30 3 * * * /usr/local/bin/gendesign-backup >> /var/log/gendesign-backup.log 2>&1
|
||||||
|
#
|
||||||
|
# Optional S3 upload — set these env vars in /etc/default/gendesign-backup
|
||||||
|
# (or as `Environment=` in a systemd timer). Without them, dumps stay local only.
|
||||||
|
#
|
||||||
|
# S3_ENDPOINT=https://s3.ru-1.storage.selcloud.ru # Selectel S3
|
||||||
|
# S3_BUCKET=gendesign-backups
|
||||||
|
# S3_ACCESS_KEY=...
|
||||||
|
# S3_SECRET_KEY=...
|
||||||
|
#
|
||||||
|
# Retention: deletes local dumps older than 7 days. S3 retention policy should
|
||||||
|
# be configured at the bucket level (lifecycle rule), not in this script.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- config ---
|
||||||
|
COMPOSE_DIR=/opt/gendesign
|
||||||
|
COMPOSE_FILE=docker-compose.prod.yml
|
||||||
|
LOCAL_BACKUP_DIR=/opt/gendesign/backups
|
||||||
|
RETENTION_DAYS=7
|
||||||
|
|
||||||
|
# Optional S3 env. Loaded from /etc/default/gendesign-backup if present.
|
||||||
|
[[ -f /etc/default/gendesign-backup ]] && source /etc/default/gendesign-backup
|
||||||
|
|
||||||
|
# --- run ---
|
||||||
|
mkdir -p "$LOCAL_BACKUP_DIR"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
DUMP_FILE="${LOCAL_BACKUP_DIR}/gendesign_${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
|
cd "$COMPOSE_DIR"
|
||||||
|
|
||||||
|
# Read DB user/name from running compose env. Falls back to "gendesign" if env empty.
|
||||||
|
DB_USER=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_USER 2>/dev/null | tr -d '\r' || true)
|
||||||
|
DB_NAME=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_DB 2>/dev/null | tr -d '\r' || true)
|
||||||
|
DB_USER=${DB_USER:-gendesign}
|
||||||
|
DB_NAME=${DB_NAME:-gendesign}
|
||||||
|
|
||||||
|
echo "[$(date -Is)] Dumping ${DB_NAME} as ${DB_USER} → ${DUMP_FILE}"
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" exec -T postgres \
|
||||||
|
pg_dump --clean --if-exists -U "$DB_USER" "$DB_NAME" \
|
||||||
|
| gzip -9 > "$DUMP_FILE"
|
||||||
|
|
||||||
|
DUMP_SIZE=$(stat -c '%s' "$DUMP_FILE")
|
||||||
|
echo "[$(date -Is)] Dump complete: ${DUMP_SIZE} bytes"
|
||||||
|
|
||||||
|
# --- optional S3 upload ---
|
||||||
|
if [[ -n "${S3_ENDPOINT:-}" && -n "${S3_BUCKET:-}" && -n "${S3_ACCESS_KEY:-}" && -n "${S3_SECRET_KEY:-}" ]]; then
|
||||||
|
echo "[$(date -Is)] Uploading to s3://${S3_BUCKET}/"
|
||||||
|
docker run --rm \
|
||||||
|
-e AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY" \
|
||||||
|
-e AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \
|
||||||
|
-v "$LOCAL_BACKUP_DIR":/backup:ro \
|
||||||
|
amazon/aws-cli:latest \
|
||||||
|
--endpoint-url "$S3_ENDPOINT" \
|
||||||
|
s3 cp "/backup/$(basename "$DUMP_FILE")" "s3://${S3_BUCKET}/"
|
||||||
|
echo "[$(date -Is)] S3 upload OK"
|
||||||
|
else
|
||||||
|
echo "[$(date -Is)] S3 vars not set — backup stays local only"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- prune old local dumps ---
|
||||||
|
find "$LOCAL_BACKUP_DIR" -name 'gendesign_*.sql.gz' -mtime "+${RETENTION_DAYS}" -print -delete
|
||||||
|
|
||||||
|
echo "[$(date -Is)] Backup done."
|
||||||
43
ops/restore.sh
Normal file
43
ops/restore.sh
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Restore a Postgres dump produced by backup.sh.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ops/restore.sh /opt/gendesign/backups/gendesign_20260427_033000.sql.gz
|
||||||
|
#
|
||||||
|
# Refuses to run unless RESTORE_CONFIRM=yes is set. This is destructive —
|
||||||
|
# the dump uses --clean --if-exists, so it DROPs existing tables before recreating.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# RESTORE_CONFIRM=yes ops/restore.sh ./gendesign_20260427_033000.sql.gz
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ "${RESTORE_CONFIRM:-no}" != "yes" ]]; then
|
||||||
|
echo "Refusing to restore — set RESTORE_CONFIRM=yes to proceed."
|
||||||
|
echo "This will DROP existing tables and recreate from the dump."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DUMP_FILE="${1:-}"
|
||||||
|
if [[ -z "$DUMP_FILE" || ! -f "$DUMP_FILE" ]]; then
|
||||||
|
echo "Usage: $0 /path/to/dump.sql.gz"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
COMPOSE_DIR=/opt/gendesign
|
||||||
|
COMPOSE_FILE=docker-compose.prod.yml
|
||||||
|
|
||||||
|
cd "$COMPOSE_DIR"
|
||||||
|
|
||||||
|
DB_USER=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_USER 2>/dev/null | tr -d '\r' || true)
|
||||||
|
DB_NAME=$(docker compose -f "$COMPOSE_FILE" exec -T postgres printenv POSTGRES_DB 2>/dev/null | tr -d '\r' || true)
|
||||||
|
DB_USER=${DB_USER:-gendesign}
|
||||||
|
DB_NAME=${DB_NAME:-gendesign}
|
||||||
|
|
||||||
|
echo "Restoring $DUMP_FILE → ${DB_NAME} as ${DB_USER}"
|
||||||
|
|
||||||
|
gunzip -c "$DUMP_FILE" | \
|
||||||
|
docker compose -f "$COMPOSE_FILE" exec -T postgres \
|
||||||
|
psql -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1
|
||||||
|
|
||||||
|
echo "Restore complete."
|
||||||
|
|
@ -50,4 +50,9 @@ pat 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
|
||||||
|
|
||||||
echo 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| docker login ghcr.io -u lekss361 --password-stdin
|
echo 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| docker login ghcr.io -u lekss361 --password-stdin
|
||||||
|
|
||||||
|
ssh -N -L 15432:localhost:5432 gendesign
|
||||||
|
Host: localhost
|
||||||
|
Port: 15432
|
||||||
|
Database: gendesign
|
||||||
|
Username: gendesign
|
||||||
|
Password: 2J2SBPMKuS998fiwhtQqDhMI
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue