Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions .github/ISSUE_TEMPLATE/bug_report.md

This file was deleted.

17 changes: 0 additions & 17 deletions .github/ISSUE_TEMPLATE/feature_request.md

This file was deleted.

19 changes: 0 additions & 19 deletions .github/PULL_REQUEST_TEMPLATE.md

This file was deleted.

4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ All of these run in CI on every PR — a failing check won't get merged.

## Style

- New features go in `src/features/`, following the existing structure (`api.py`, `services.py`, `dto.py`).
- New features go in `src/features/`, following the existing structure; add `api.py`, `services.py`, or `dto.py` only when the feature needs them.
- Templates live in `src/templates/`, JavaScript in `src/static/js/`.
- Keep template-linked CSS/JS versioned with `asset_version(...)` so browser caches invalidate on file changes.
- Keep template-linked static assets referenced through `static_url(request, ...)` so browser caches invalidate on file changes and URLs respect ASGI `root_path`.
- The terminal bar is a navigation layer, not a shell — keep commands simple and purposeful.
- Form validation belongs in DTOs (`dto.py`), not in route handlers.
- Tests are expected for new Python logic; JS tests for anything in `src/static/js/`.
Expand Down
19 changes: 11 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ echo_ is a personal milestone log — a small web app built with FastAPI, server
| Web framework | FastAPI |
| Templates | Jinja2 |
| ORM | SQLAlchemy 2.x (Mapped API) |
| Database | SQLite (`echo.db` in project root) |
| Database | SQLite (`user_data/echo.db` locally by default; configurable via `DATABASE_URL`) |
| Migrations | Alembic |
| Form validation | Pydantic v2 |
| Configuration | Pydantic Settings |
Expand All @@ -35,7 +35,7 @@ src/
migrations/ — env.py, script.py.mako, versions/

web/
templates.py — shared Jinja2Templates, asset_version, settings in context
templates.py — shared Jinja2Templates, static_url, asset_version, settings in context

features/
auth/
Expand All @@ -51,7 +51,6 @@ src/

tags/
api.py — /tags, /tags/{tag_name}
services.py — get_milestones_for_tag

terminal/
api.py — /help, /random, /search, /terminal/commands, /tree
Expand All @@ -69,6 +68,7 @@ src/
help.html, search.html

static/
site.webmanifest
css/
base.css, forms.css
pages/ timeline.css, milestone.css
Expand All @@ -78,7 +78,10 @@ src/
keyboard/ global.js
terminal/ input.js, input-mobile.js, navigation.js, table.js
icons/
favicon.ico, favicon.svg, apple-touch-icon.png
favicon.ico, favicon-16x16.png, favicon-32x32.png
apple-touch-icon-180x180.png
pwa-icon-192x192.png, pwa-icon-256x256.png, pwa-icon-384x384.png, pwa-icon-512x512.png
larger source/reserve icons
```

## Routes
Expand Down Expand Up @@ -114,7 +117,7 @@ Read from `.env` via `pydantic-settings`. Variables:

| Variable | Default | Description |
|----------------------|--------------------------------|----------------------------------------|
| `DATABASE_URL` | `sqlite:///echo.db` | Database URL |
| `DATABASE_URL` | `sqlite:///.../user_data/echo.db` | Database URL |
| `SESSION_SECRET_KEY` | — | Session signing key (required) |
| `ECHO_PASSWORD` | — | Login password (required) |
| `ECHO_USERNAME` | `katrin` | Username |
Expand Down Expand Up @@ -171,7 +174,7 @@ The bottom bar is a navigation layer, not a shell. Commands: `help`, `new`, `tag

- HTML responses should not be cached long-term.
- Versioned static assets should be cached long-term (`Cache-Control: public, max-age=31536000, immutable`).
- All CSS and JS references in templates should include `asset_version(...)` so the URL changes when file contents change.
- All template static references should use `static_url(request, ...)` so URLs include cache versions and respect ASGI `root_path`.

## Timeline

Expand Down Expand Up @@ -203,9 +206,9 @@ poetry run alembic -c src/orm/alembic.ini stamp 4f1b2d9c7a11
## Code quality

Pre-commit hooks: Ruff, MyPy, djLint, Stylelint, ESLint, pytest, JS tests (Vitest), poetry check, alembic check.
Linters (except MyPy and pytest) run only on changed files.
In pre-commit, linters run on changed files where possible; MyPy, pytest, JS tests, poetry check and alembic check run as full-project checks. CI runs full checks.

CI (`.github/workflows/`):
- `quality_gates.yml` — Python style, Frontend style, Python tests, JS tests, Migrations
- `smoke.yml` — runs after quality gates, starts the server and checks `/login`
- `smoke.yml` — runs after quality gates and executes middleware smoke tests
- `release.yml` — semantic-release, publishes a draft release on push to main
7 changes: 6 additions & 1 deletion src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ async def lifespan(app: FastAPI):
async def cache_control(request: Request, call_next):
response = await call_next(request)

if request.url.path.startswith("/static/"):
path = request.scope.get("path", request.url.path)
root_path = request.scope.get("root_path", "")
if root_path and path.startswith(root_path):
path = path.removeprefix(root_path) or "/"

if path.startswith("/static/"):
response.headers.setdefault(
"Cache-Control",
"public, max-age=31536000, immutable",
Expand Down
5 changes: 4 additions & 1 deletion src/features/auth/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@

class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
path = request.url.path
path = request.scope.get("path", request.url.path)
root_path = request.scope.get("root_path", "")
if root_path and path.startswith(root_path):
path = path.removeprefix(root_path) or "/"

if path.startswith("/static/"):
return await call_next(request)
Expand Down
9 changes: 0 additions & 9 deletions src/features/tags/services.py

This file was deleted.

Binary file added src/static/icons/apple-touch-icon-180x180.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed src/static/icons/apple-touch-icon.png
Binary file not shown.
Binary file added src/static/icons/favicon-128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/favicon-48x48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/favicon-64x64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/favicon-96x96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/static/icons/favicon.ico
Binary file not shown.
7 changes: 0 additions & 7 deletions src/static/icons/favicon.svg

This file was deleted.

Binary file added src/static/icons/icon-master-1024x1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/pwa-icon-1024x1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/pwa-icon-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/pwa-icon-256x256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/pwa-icon-384x384.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/static/icons/pwa-icon-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 7 additions & 7 deletions src/static/js/terminal/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ const COMMAND_HANDLERS = {

return `Тег "${tagName}" не найден.`;
},
search: (args) => {
const query = args.trim();
search: (args) => {
const query = args.trim();

if (!query) {
return "Введите поисковый запрос.";
}
if (!query) {
return "Введите поисковый запрос.";
}

return `/search?q=${encodeURIComponent(query)}`;
},
return `/search?q=${encodeURIComponent(query)}`;
},
};

window.TERMINAL_COMMAND_HANDLERS = COMMAND_HANDLERS;
Expand Down
35 changes: 35 additions & 0 deletions src/static/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "Echo",
"short_name": "Echo",
"start_url": ".",
"scope": ".",
"display": "standalone",
"theme_color": "#111111",
"background_color": "#111111",
"icons": [
{
"src": "icons/pwa-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "icons/pwa-icon-256x256.png",
"sizes": "256x256",
"type": "image/png",
"purpose": "any"
},
{
"src": "icons/pwa-icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "any"
},
{
"src": "icons/pwa-icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
]
}
59 changes: 28 additions & 31 deletions src/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,45 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#111111">
<link rel="manifest" href="{{ static_url(request, 'site.webmanifest') }}">
<link rel="icon"
type="image/svg+xml"
href="/static/icons/favicon.svg?v={{ asset_version("icons/favicon.svg") }}">
sizes="16x16"
type="image/png"
href="{{ static_url(request, 'icons/favicon-16x16.png') }}">
<link rel="icon"
sizes="32x32"
type="image/png"
href="{{ static_url(request, 'icons/favicon-32x32.png') }}">
<link rel="icon"
type="image/x-icon"
href="/static/icons/favicon.ico?v={{ asset_version("icons/favicon.ico") }}">
href="{{ static_url(request, 'icons/favicon.ico') }}">
<link rel="apple-touch-icon"
href="/static/icons/apple-touch-icon.png?v={{ asset_version("icons/apple-touch-icon.png") }}">
sizes="180x180"
href="{{ static_url(request, 'icons/apple-touch-icon-180x180.png') }}">
<title>{% block title %}echo_.log{% endblock %}</title>
<link rel="stylesheet" href="{{ static_url(request, 'css/base.css') }}">
<link rel="stylesheet" href="{{ static_url(request, 'css/forms.css') }}">
<link rel="stylesheet"
href="/static/css/base.css?v={{ asset_version("css/base.css") }}">
<link rel="stylesheet"
href="/static/css/forms.css?v={{ asset_version("css/forms.css") }}">
<link rel="stylesheet"
href="/static/css/pages/timeline.css?v={{ asset_version("css/pages/timeline.css") }}">
href="{{ static_url(request, 'css/pages/timeline.css') }}">
<link rel="stylesheet"
href="/static/css/pages/milestone.css?v={{ asset_version("css/pages/milestone.css") }}">
href="{{ static_url(request, 'css/pages/milestone.css') }}">
<link rel="stylesheet"
href="/static/css/components/terminal.css?v={{ asset_version("css/components/terminal.css") }}">
href="{{ static_url(request, 'css/components/terminal.css') }}">
<link rel="stylesheet"
href="/static/css/components/terminal-table.css?v={{ asset_version("css/components/terminal-table.css") }}">
href="{{ static_url(request, 'css/components/terminal-table.css') }}">
<link rel="stylesheet"
href="/static/css/components/terminal-tree.css?v={{ asset_version("css/components/terminal-tree.css") }}">
href="{{ static_url(request, 'css/components/terminal-tree.css') }}">
{% block styles %}{% endblock %}
<script src="/static/js/autocomplete/core.js?v={{ asset_version("js/autocomplete/core.js") }}"
defer></script>
<script src="/static/js/terminal/history.js?v={{ asset_version("js/terminal/history.js") }}"
defer></script>
<script src="/static/js/autocomplete/terminal.js?v={{ asset_version("js/autocomplete/terminal.js") }}"
defer></script>
<script src="/static/js/autocomplete/tags.js?v={{ asset_version("js/autocomplete/tags.js") }}"
defer></script>
<script src="/static/js/terminal/input.js?v={{ asset_version("js/terminal/input.js") }}"
defer></script>
<script src="/static/js/terminal/input-mobile.js?v={{ asset_version("js/terminal/input-mobile.js") }}"
defer></script>
<script src="/static/js/terminal/table.js?v={{ asset_version("js/terminal/table.js") }}"
defer></script>
<script src="/static/js/keyboard/global.js?v={{ asset_version("js/keyboard/global.js") }}"
defer></script>
<script src="/static/js/terminal/navigation.js?v={{ asset_version("js/terminal/navigation.js") }}"
defer></script>
<script src="{{ static_url(request, 'js/autocomplete/core.js') }}" defer></script>
<script src="{{ static_url(request, 'js/terminal/history.js') }}" defer></script>
<script src="{{ static_url(request, 'js/autocomplete/terminal.js') }}" defer></script>
<script src="{{ static_url(request, 'js/autocomplete/tags.js') }}" defer></script>
<script src="{{ static_url(request, 'js/terminal/input.js') }}" defer></script>
<script src="{{ static_url(request, 'js/terminal/input-mobile.js') }}" defer></script>
<script src="{{ static_url(request, 'js/terminal/table.js') }}" defer></script>
<script src="{{ static_url(request, 'js/keyboard/global.js') }}" defer></script>
<script src="{{ static_url(request, 'js/terminal/navigation.js') }}" defer></script>
</head>
<body>
<main>
Expand Down
3 changes: 1 addition & 2 deletions src/templates/milestones/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ <h1>Edit milestone</h1>
</label>
<button type="submit">Save</button>
</form>
<script src="/static/js/milestones/date.js?v={{ asset_version("js/milestones/date.js") }}"
defer></script>
<script src="{{ static_url(request, 'js/milestones/date.js') }}" defer></script>
{% endblock %}
3 changes: 1 addition & 2 deletions src/templates/milestones/new.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ <h1>New milestone</h1>
</datalist>
<button type="submit">Save</button>
</form>
<script src="/static/js/milestones/date.js?v={{ asset_version("js/milestones/date.js") }}"
defer></script>
<script src="{{ static_url(request, 'js/milestones/date.js') }}" defer></script>
{% endblock %}
7 changes: 7 additions & 0 deletions src/web/templates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path

from fastapi import Request
from fastapi.templating import Jinja2Templates

from src.config import settings
Expand All @@ -12,5 +13,11 @@ def _asset_version(rel_path: str) -> int:
return int((_TEMPLATES_DIR.parent / "static" / rel_path).stat().st_mtime)


def _static_url(request: Request, rel_path: str) -> str:
version = _asset_version(rel_path)
return f"{request.url_for('static', path=rel_path)}?v={version}"


templates.env.globals["asset_version"] = _asset_version
templates.env.globals["static_url"] = _static_url
templates.env.globals["settings"] = settings
Loading
Loading