vibetuner 2.6.1__tar.gz → 2.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vibetuner might be problematic. Click here for more details.

Files changed (83) hide show
  1. {vibetuner-2.6.1 → vibetuner-2.7.1}/PKG-INFO +2 -1
  2. {vibetuner-2.6.1 → vibetuner-2.7.1}/pyproject.toml +2 -1
  3. vibetuner-2.7.1/src/vibetuner/__init__.py +2 -0
  4. vibetuner-2.7.1/src/vibetuner/__main__.py +4 -0
  5. vibetuner-2.7.1/src/vibetuner/cli/__init__.py +68 -0
  6. vibetuner-2.7.1/src/vibetuner/cli/run.py +161 -0
  7. vibetuner-2.7.1/src/vibetuner/config.py +128 -0
  8. vibetuner-2.7.1/src/vibetuner/context.py +25 -0
  9. vibetuner-2.7.1/src/vibetuner/frontend/AGENTS.md +113 -0
  10. vibetuner-2.7.1/src/vibetuner/frontend/CLAUDE.md +113 -0
  11. vibetuner-2.7.1/src/vibetuner/frontend/__init__.py +94 -0
  12. vibetuner-2.7.1/src/vibetuner/frontend/context.py +10 -0
  13. vibetuner-2.7.1/src/vibetuner/frontend/deps.py +41 -0
  14. vibetuner-2.7.1/src/vibetuner/frontend/email.py +45 -0
  15. vibetuner-2.7.1/src/vibetuner/frontend/hotreload.py +13 -0
  16. vibetuner-2.7.1/src/vibetuner/frontend/lifespan.py +26 -0
  17. vibetuner-2.7.1/src/vibetuner/frontend/middleware.py +151 -0
  18. vibetuner-2.7.1/src/vibetuner/frontend/oauth.py +196 -0
  19. vibetuner-2.7.1/src/vibetuner/frontend/routes/__init__.py +12 -0
  20. vibetuner-2.7.1/src/vibetuner/frontend/routes/auth.py +150 -0
  21. vibetuner-2.7.1/src/vibetuner/frontend/routes/debug.py +414 -0
  22. vibetuner-2.7.1/src/vibetuner/frontend/routes/health.py +33 -0
  23. vibetuner-2.7.1/src/vibetuner/frontend/routes/language.py +43 -0
  24. vibetuner-2.7.1/src/vibetuner/frontend/routes/meta.py +55 -0
  25. vibetuner-2.7.1/src/vibetuner/frontend/routes/user.py +94 -0
  26. vibetuner-2.7.1/src/vibetuner/frontend/templates.py +176 -0
  27. vibetuner-2.7.1/src/vibetuner/logging.py +87 -0
  28. vibetuner-2.7.1/src/vibetuner/models/AGENTS.md +165 -0
  29. vibetuner-2.7.1/src/vibetuner/models/CLAUDE.md +165 -0
  30. vibetuner-2.7.1/src/vibetuner/models/__init__.py +14 -0
  31. vibetuner-2.7.1/src/vibetuner/models/blob.py +89 -0
  32. vibetuner-2.7.1/src/vibetuner/models/email_verification.py +84 -0
  33. vibetuner-2.7.1/src/vibetuner/models/mixins.py +76 -0
  34. vibetuner-2.7.1/src/vibetuner/models/oauth.py +57 -0
  35. vibetuner-2.7.1/src/vibetuner/models/registry.py +15 -0
  36. vibetuner-2.7.1/src/vibetuner/models/types.py +16 -0
  37. vibetuner-2.7.1/src/vibetuner/models/user.py +91 -0
  38. vibetuner-2.7.1/src/vibetuner/mongo.py +18 -0
  39. vibetuner-2.7.1/src/vibetuner/paths.py +112 -0
  40. vibetuner-2.7.1/src/vibetuner/services/AGENTS.md +104 -0
  41. vibetuner-2.7.1/src/vibetuner/services/CLAUDE.md +104 -0
  42. vibetuner-2.7.1/src/vibetuner/services/blob.py +175 -0
  43. vibetuner-2.7.1/src/vibetuner/services/email.py +50 -0
  44. vibetuner-2.7.1/src/vibetuner/tasks/AGENTS.md +98 -0
  45. vibetuner-2.7.1/src/vibetuner/tasks/CLAUDE.md +98 -0
  46. vibetuner-2.7.1/src/vibetuner/tasks/__init__.py +2 -0
  47. vibetuner-2.7.1/src/vibetuner/tasks/context.py +34 -0
  48. vibetuner-2.7.1/src/vibetuner/tasks/worker.py +18 -0
  49. vibetuner-2.7.1/src/vibetuner/templates/email/AGENTS.md +48 -0
  50. vibetuner-2.7.1/src/vibetuner/templates/email/CLAUDE.md +48 -0
  51. vibetuner-2.7.1/src/vibetuner/templates/email/default/magic_link.html.jinja +16 -0
  52. vibetuner-2.7.1/src/vibetuner/templates/email/default/magic_link.txt.jinja +5 -0
  53. vibetuner-2.7.1/src/vibetuner/templates/frontend/AGENTS.md +74 -0
  54. vibetuner-2.7.1/src/vibetuner/templates/frontend/CLAUDE.md +74 -0
  55. vibetuner-2.7.1/src/vibetuner/templates/frontend/base/favicons.html.jinja +1 -0
  56. vibetuner-2.7.1/src/vibetuner/templates/frontend/base/footer.html.jinja +3 -0
  57. vibetuner-2.7.1/src/vibetuner/templates/frontend/base/header.html.jinja +0 -0
  58. vibetuner-2.7.1/src/vibetuner/templates/frontend/base/opengraph.html.jinja +7 -0
  59. vibetuner-2.7.1/src/vibetuner/templates/frontend/base/skeleton.html.jinja +42 -0
  60. vibetuner-2.7.1/src/vibetuner/templates/frontend/debug/collections.html.jinja +103 -0
  61. vibetuner-2.7.1/src/vibetuner/templates/frontend/debug/components/debug_nav.html.jinja +55 -0
  62. vibetuner-2.7.1/src/vibetuner/templates/frontend/debug/index.html.jinja +83 -0
  63. vibetuner-2.7.1/src/vibetuner/templates/frontend/debug/info.html.jinja +256 -0
  64. vibetuner-2.7.1/src/vibetuner/templates/frontend/debug/users.html.jinja +137 -0
  65. vibetuner-2.7.1/src/vibetuner/templates/frontend/debug/version.html.jinja +53 -0
  66. vibetuner-2.7.1/src/vibetuner/templates/frontend/email/magic_link.txt.jinja +5 -0
  67. vibetuner-2.7.1/src/vibetuner/templates/frontend/email_sent.html.jinja +82 -0
  68. vibetuner-2.7.1/src/vibetuner/templates/frontend/index.html.jinja +19 -0
  69. vibetuner-2.7.1/src/vibetuner/templates/frontend/lang/select.html.jinja +4 -0
  70. vibetuner-2.7.1/src/vibetuner/templates/frontend/login.html.jinja +84 -0
  71. vibetuner-2.7.1/src/vibetuner/templates/frontend/meta/browserconfig.xml.jinja +10 -0
  72. vibetuner-2.7.1/src/vibetuner/templates/frontend/meta/robots.txt.jinja +3 -0
  73. vibetuner-2.7.1/src/vibetuner/templates/frontend/meta/site.webmanifest.jinja +7 -0
  74. vibetuner-2.7.1/src/vibetuner/templates/frontend/meta/sitemap.xml.jinja +6 -0
  75. vibetuner-2.7.1/src/vibetuner/templates/frontend/user/edit.html.jinja +85 -0
  76. vibetuner-2.7.1/src/vibetuner/templates/frontend/user/profile.html.jinja +156 -0
  77. vibetuner-2.7.1/src/vibetuner/templates/markdown/.placeholder +0 -0
  78. vibetuner-2.7.1/src/vibetuner/templates/markdown/AGENTS.md +29 -0
  79. vibetuner-2.7.1/src/vibetuner/templates/markdown/CLAUDE.md +29 -0
  80. vibetuner-2.7.1/src/vibetuner/templates.py +152 -0
  81. vibetuner-2.7.1/src/vibetuner/time.py +57 -0
  82. vibetuner-2.7.1/src/vibetuner/versioning.py +8 -0
  83. {vibetuner-2.6.1/src/vibetuner → vibetuner-2.7.1/src/vibetuner/services}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: vibetuner
3
- Version: 2.6.1
3
+ Version: 2.7.1
4
4
  Summary: Blessed Python dependencies for Vibetuner projects
5
5
  Requires-Dist: aioboto3>=15.5.0
6
6
  Requires-Dist: arel>=0.4.0
@@ -34,6 +34,7 @@ Requires-Dist: gh-bin>=2.81.0 ; extra == 'dev'
34
34
  Requires-Dist: granian[pname,reload]>=2.5.6 ; extra == 'dev'
35
35
  Requires-Dist: just-bin>=1.43.0 ; extra == 'dev'
36
36
  Requires-Dist: pre-commit>=4.3.0 ; extra == 'dev'
37
+ Requires-Dist: pysemver>=0.5.0 ; extra == 'dev'
37
38
  Requires-Dist: ruff>=0.14.3 ; extra == 'dev'
38
39
  Requires-Dist: rumdl>=0.0.170 ; extra == 'dev'
39
40
  Requires-Dist: semver>=3.0.4 ; extra == 'dev'
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "vibetuner"
3
3
  description = "Blessed Python dependencies for Vibetuner projects"
4
- version = "2.6.1"
4
+ version = "2.7.1"
5
5
  requires-python = ">=3.10"
6
6
  dependencies = [
7
7
  # These are the base dependencies of AllTuner's blessed stack
@@ -42,6 +42,7 @@ dev = [
42
42
  "granian[pname,reload]>=2.5.6",
43
43
  "just-bin>=1.43.0",
44
44
  "pre-commit>=4.3.0",
45
+ "pysemver>=0.5.0",
45
46
  "ruff>=0.14.3",
46
47
  "rumdl>=0.0.170",
47
48
  "semver>=3.0.4",
@@ -0,0 +1,2 @@
1
+ # Your code goes here (add your code between this line and the EOF comment)
2
+ # End of file
@@ -0,0 +1,4 @@
1
+ from .cli import app
2
+
3
+
4
+ app()
@@ -0,0 +1,68 @@
1
+ # ABOUTME: Core CLI setup with AsyncTyper wrapper and base configuration
2
+ # ABOUTME: Provides main CLI entry point and logging configuration
3
+ import inspect
4
+ from functools import partial, wraps
5
+ from importlib import import_module
6
+
7
+ import asyncer
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from vibetuner.cli.run import run_app
12
+ from vibetuner.config import settings
13
+ from vibetuner.logging import LogLevel, setup_logging
14
+
15
+
16
+ console = Console()
17
+
18
+
19
+ class AsyncTyper(typer.Typer):
20
+ def __init__(self, *args, **kwargs):
21
+ kwargs.setdefault("no_args_is_help", True)
22
+ super().__init__(*args, **kwargs)
23
+
24
+ @staticmethod
25
+ def maybe_run_async(decorator, f):
26
+ if inspect.iscoroutinefunction(f):
27
+
28
+ @wraps(f)
29
+ def runner(*args, **kwargs):
30
+ return asyncer.runnify(f)(*args, **kwargs)
31
+
32
+ decorator(runner)
33
+ else:
34
+ decorator(f)
35
+ return f
36
+
37
+ def callback(self, *args, **kwargs):
38
+ decorator = super().callback(*args, **kwargs)
39
+ return partial(self.maybe_run_async, decorator)
40
+
41
+ def command(self, *args, **kwargs):
42
+ decorator = super().command(*args, **kwargs)
43
+ return partial(self.maybe_run_async, decorator)
44
+
45
+
46
+ app = AsyncTyper(help=f"{settings.project.project_name.title()} CLI")
47
+
48
+ LOG_LEVEL_OPTION = typer.Option(
49
+ LogLevel.INFO,
50
+ "--log-level",
51
+ "-l",
52
+ case_sensitive=False,
53
+ help="Set the logging level",
54
+ )
55
+
56
+
57
+ @app.callback()
58
+ def callback(log_level: LogLevel | None = LOG_LEVEL_OPTION) -> None:
59
+ """Initialize logging and other global settings."""
60
+ setup_logging(level=log_level)
61
+
62
+
63
+ app.add_typer(run_app, name="run")
64
+
65
+ try:
66
+ import_module("app.cli")
67
+ except (ImportError, ModuleNotFoundError):
68
+ pass
@@ -0,0 +1,161 @@
1
+ # ABOUTME: Run commands for starting the application in different modes
2
+ # ABOUTME: Supports dev/prod modes for frontend and worker services
3
+ import os
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+
10
+ console = Console()
11
+
12
+ run_app = typer.Typer(
13
+ help="Run the application in different modes", no_args_is_help=True
14
+ )
15
+
16
+
17
+ @run_app.command(name="dev")
18
+ def dev(
19
+ service: Annotated[
20
+ str, typer.Argument(help="Service to run: 'frontend' or 'worker'")
21
+ ] = "frontend",
22
+ port: int = typer.Option(
23
+ None, help="Port to run on (8000 for frontend, 11111 for worker)"
24
+ ),
25
+ host: str = typer.Option("0.0.0.0", help="Host to bind to (frontend only)"), # noqa: S104
26
+ workers_count: int = typer.Option(
27
+ 1, "--workers", help="Number of worker processes"
28
+ ),
29
+ ) -> None:
30
+ """Run in development mode with hot reload (frontend or worker)."""
31
+ os.environ["DEBUG"] = "1"
32
+
33
+ if service == "worker":
34
+ # Worker mode
35
+ from streaq.cli import main as streaq_main
36
+
37
+ worker_port = port if port else 11111
38
+ console.print(
39
+ f"[green]Starting worker in dev mode on port {worker_port}[/green]"
40
+ )
41
+ console.print("[dim]Hot reload enabled[/dim]")
42
+
43
+ if workers_count > 1:
44
+ console.print(
45
+ "[yellow]Warning: Multiple workers not supported in dev mode, using 1[/yellow]"
46
+ )
47
+
48
+ # Call streaq programmatically
49
+ streaq_main(
50
+ worker_path="app.tasks.worker.worker",
51
+ workers=1,
52
+ reload=True,
53
+ verbose=True,
54
+ web=True,
55
+ host="0.0.0.0", # noqa: S104
56
+ port=worker_port,
57
+ )
58
+ elif service == "frontend":
59
+ # Frontend mode
60
+ from pathlib import Path
61
+
62
+ from granian import Granian
63
+ from granian.constants import Interfaces
64
+
65
+ frontend_port = port if port else 8000
66
+ console.print(
67
+ f"[green]Starting frontend in dev mode on {host}:{frontend_port}[/green]"
68
+ )
69
+ console.print("[dim]Watching for changes in src/ and templates/[/dim]")
70
+
71
+ # Define paths to watch for changes
72
+ reload_paths = [
73
+ Path("src/vibetuner"),
74
+ Path("src/app"),
75
+ Path("templates/frontend"),
76
+ Path("templates/email"),
77
+ Path("templates/markdown"),
78
+ ]
79
+
80
+ server = Granian(
81
+ target="vibetuner.frontend:app",
82
+ address=host,
83
+ port=frontend_port,
84
+ interface=Interfaces.ASGI,
85
+ workers=workers_count,
86
+ reload=True,
87
+ reload_paths=reload_paths,
88
+ log_level="info",
89
+ log_access=True,
90
+ )
91
+
92
+ server.serve()
93
+ else:
94
+ console.print(f"[red]Error: Unknown service '{service}'[/red]")
95
+ console.print("[yellow]Valid services: 'frontend' or 'worker'[/yellow]")
96
+ raise typer.Exit(code=1)
97
+
98
+
99
+ @run_app.command(name="prod")
100
+ def prod(
101
+ service: Annotated[
102
+ str, typer.Argument(help="Service to run: 'frontend' or 'worker'")
103
+ ] = "frontend",
104
+ port: int = typer.Option(
105
+ None, help="Port to run on (8000 for frontend, 11111 for worker)"
106
+ ),
107
+ host: str = typer.Option("0.0.0.0", help="Host to bind to (frontend only)"), # noqa: S104
108
+ workers_count: int = typer.Option(
109
+ 4, "--workers", help="Number of worker processes"
110
+ ),
111
+ ) -> None:
112
+ """Run in production mode (frontend or worker)."""
113
+ os.environ["ENVIRONMENT"] = "production"
114
+
115
+ if service == "worker":
116
+ # Worker mode
117
+ from streaq.cli import main as streaq_main
118
+
119
+ worker_port = port if port else 11111
120
+ console.print(
121
+ f"[green]Starting worker in prod mode on port {worker_port}[/green]"
122
+ )
123
+ console.print(f"[dim]Workers: {workers_count}[/dim]")
124
+
125
+ # Call streaq programmatically
126
+ streaq_main(
127
+ worker_path="app.tasks.worker.worker",
128
+ workers=workers_count,
129
+ reload=False,
130
+ verbose=False,
131
+ web=True,
132
+ host="0.0.0.0", # noqa: S104
133
+ port=worker_port,
134
+ )
135
+ elif service == "frontend":
136
+ # Frontend mode
137
+ from granian import Granian
138
+ from granian.constants import Interfaces
139
+
140
+ frontend_port = port if port else 8000
141
+ console.print(
142
+ f"[green]Starting frontend in prod mode on {host}:{frontend_port}[/green]"
143
+ )
144
+ console.print(f"[dim]Workers: {workers_count}[/dim]")
145
+
146
+ server = Granian(
147
+ target="vibetuner.frontend:app",
148
+ address=host,
149
+ port=frontend_port,
150
+ interface=Interfaces.ASGI,
151
+ workers=workers_count,
152
+ reload=False,
153
+ log_level="info",
154
+ log_access=True,
155
+ )
156
+
157
+ server.serve()
158
+ else:
159
+ console.print(f"[red]Error: Unknown service '{service}'[/red]")
160
+ console.print("[yellow]Valid services: 'frontend' or 'worker'[/yellow]")
161
+ raise typer.Exit(code=1)
@@ -0,0 +1,128 @@
1
+ import base64
2
+ import hashlib
3
+ from datetime import datetime
4
+ from functools import cached_property
5
+ from typing import Annotated
6
+
7
+ import yaml
8
+ from pydantic import (
9
+ UUID4,
10
+ Field,
11
+ HttpUrl,
12
+ MongoDsn,
13
+ RedisDsn,
14
+ SecretStr,
15
+ computed_field,
16
+ )
17
+ from pydantic_extra_types.language_code import LanguageAlpha2
18
+ from pydantic_settings import BaseSettings, SettingsConfigDict
19
+
20
+ from vibetuner.paths import config_vars as config_vars_path
21
+ from vibetuner.versioning import version
22
+
23
+
24
+ current_year: int = datetime.now().year
25
+
26
+
27
+ def _load_project_config() -> "ProjectConfiguration":
28
+ if not config_vars_path.exists():
29
+ return ProjectConfiguration()
30
+ return ProjectConfiguration(
31
+ **yaml.safe_load(config_vars_path.read_text(encoding="utf-8"))
32
+ )
33
+
34
+
35
+ class ProjectConfiguration(BaseSettings):
36
+ project_slug: str = "default_project"
37
+ project_name: str = "default_project"
38
+
39
+ project_description: str = "A default project description."
40
+
41
+ # Language Related Settings
42
+ supported_languages: set[LanguageAlpha2] | None = None
43
+ default_language: LanguageAlpha2 = LanguageAlpha2("en")
44
+
45
+ mongodb_url: MongoDsn | None = None
46
+ redis_url: RedisDsn | None = None
47
+
48
+ # AWS Parameters
49
+ aws_default_region: str = "eu-central-1"
50
+
51
+ # Company Name
52
+ company_name: str = "Acme Corp"
53
+
54
+ # From Email for transactional emails
55
+ from_email: str = "no-reply@example.com"
56
+
57
+ # Copyright
58
+ copyright_start: Annotated[int, Field(strict=True, gt=1714, lt=2048)] = current_year
59
+
60
+ # Analytics
61
+ umami_website_id: UUID4 | None = None
62
+
63
+ # Fully Qualified Domain Name
64
+ fqdn: str | None = None
65
+
66
+ @cached_property
67
+ def languages(self) -> set[str]:
68
+ if self.supported_languages is None:
69
+ return {self.language}
70
+
71
+ return {
72
+ str(lang) for lang in (*self.supported_languages, self.default_language)
73
+ }
74
+
75
+ @cached_property
76
+ def language(self) -> str:
77
+ return str(self.default_language)
78
+
79
+ @cached_property
80
+ def copyright(self) -> str:
81
+ year_part = (
82
+ f"{self.copyright_start}-{current_year}"
83
+ if self.copyright_start and self.copyright_start != current_year
84
+ else str(current_year)
85
+ )
86
+ return f"© {year_part}{f' {self.company_name}' if self.company_name else ''}"
87
+
88
+ model_config = SettingsConfigDict(extra="ignore")
89
+
90
+
91
+ class CoreConfiguration(BaseSettings):
92
+ project: ProjectConfiguration
93
+
94
+ debug: bool = False
95
+ version: str = version
96
+ session_key: SecretStr = SecretStr("ct-!secret-must-change-me")
97
+
98
+ aws_access_key_id: SecretStr | None = None
99
+ aws_secret_access_key: SecretStr | None = None
100
+
101
+ r2_default_bucket_name: str | None = None
102
+ r2_bucket_endpoint_url: HttpUrl | None = None
103
+ r2_access_key: SecretStr | None = None
104
+ r2_secret_key: SecretStr | None = None
105
+ r2_default_region: str = "auto"
106
+
107
+ @computed_field
108
+ @cached_property
109
+ def v_hash(self) -> str:
110
+ hash_object = hashlib.sha256(self.version.encode("utf-8"))
111
+ hash_bytes = hash_object.digest()
112
+
113
+ b64_hash = base64.urlsafe_b64encode(hash_bytes).decode("utf-8")
114
+
115
+ url_safe_hash = b64_hash.rstrip("=")[:8]
116
+
117
+ return url_safe_hash
118
+
119
+ @cached_property
120
+ def mongo_dbname(self) -> str:
121
+ return self.project.project_slug
122
+
123
+ model_config = SettingsConfigDict(
124
+ case_sensitive=False, extra="ignore", env_file=".env"
125
+ )
126
+
127
+
128
+ settings = CoreConfiguration(project=_load_project_config())
@@ -0,0 +1,25 @@
1
+ from pydantic import UUID4, BaseModel
2
+
3
+ from vibetuner.config import settings
4
+
5
+
6
+ class Context(BaseModel):
7
+ DEBUG: bool = settings.debug
8
+
9
+ project_name: str = settings.project.project_name
10
+ project_slug: str = settings.project.project_slug
11
+ project_description: str = settings.project.project_description
12
+
13
+ version: str = settings.version
14
+ v_hash: str = settings.v_hash
15
+
16
+ copyright: str = settings.project.copyright
17
+
18
+ default_language: str = settings.project.language
19
+ supported_languages: set[str] = settings.project.languages
20
+
21
+ umami_website_id: UUID4 | None = settings.project.umami_website_id
22
+
23
+ fqdn: str | None = settings.project.fqdn
24
+
25
+ model_config = {"arbitrary_types_allowed": True}
@@ -0,0 +1,113 @@
1
+ # Core Frontend Module
2
+
3
+ **IMMUTABLE SCAFFOLDING CODE** - This is the framework's core frontend infrastructure.
4
+
5
+ ## What's Here
6
+
7
+ This module contains the scaffolding's core frontend components:
8
+
9
+ - **routes/** - Essential default routes (auth, health, debug, language, user, meta)
10
+ - **templates.py** - Template rendering with automatic context injection
11
+ - **deps.py** - FastAPI dependencies (authentication, language, etc.)
12
+ - **middleware.py** - Request/response middleware
13
+ - **oauth.py** - OAuth provider integration
14
+ - **email.py** - Magic link email authentication
15
+ - **lifespan.py** - Application startup/shutdown lifecycle
16
+ - **context.py** - Request context management
17
+ - **hotreload.py** - Development hot-reload support
18
+
19
+ ## Important Rules
20
+
21
+ ⚠️ **DO NOT MODIFY** these core frontend components directly.
22
+
23
+ **For changes to core frontend:**
24
+
25
+ - File an issue at `https://github.com/alltuner/scaffolding`
26
+ - Core changes benefit all projects using the scaffolding
27
+
28
+ **For your application routes:**
29
+
30
+ - Create them in `src/app/frontend/routes/` instead
31
+ - Import core components when needed:
32
+ - `from vibetuner.frontend.deps import get_current_user`
33
+ - `from vibetuner.frontend.templates import render_template`
34
+
35
+ ## User Route Pattern (for reference)
36
+
37
+ Your application routes in `src/app/frontend/routes/` should follow this pattern:
38
+
39
+ ```python
40
+ # src/app/frontend/routes/dashboard.py
41
+ from fastapi import APIRouter, Request, Depends
42
+ from vibetuner.frontend.deps import get_current_user
43
+ from vibetuner.frontend.templates import render_template
44
+
45
+ router = APIRouter()
46
+
47
+ @router.get("/dashboard")
48
+ async def dashboard(request: Request, user=Depends(get_current_user)):
49
+ return render_template("dashboard.html.jinja", request, {"user": user})
50
+ ```
51
+
52
+ ## Template Rendering
53
+
54
+ ```python
55
+ # Automatic context in every template:
56
+ {
57
+ "request": request,
58
+ "DEBUG": settings.DEBUG,
59
+ "hotreload": hotreload, # Dev mode
60
+ # ... plus your custom context
61
+ }
62
+ ```
63
+
64
+ ### Template Filters
65
+
66
+ - `{{ datetime | timeago }}` - "2 hours ago"
67
+ - `{{ datetime | format_date }}` - "January 15, 2024"
68
+ - `{{ text | markdown }}` - Convert Markdown to HTML
69
+
70
+ ## Core Dependencies Available
71
+
72
+ Import these from `vibetuner.frontend.deps`:
73
+
74
+ - `get_current_user` - Require authenticated user (raises 403 if not authenticated)
75
+ - `get_current_user_optional` - Optional auth check (returns None if not authenticated)
76
+ - `LangDep` - Current language from cookie/header
77
+ - `MagicCookieDep` - Magic link cookie for authentication
78
+
79
+ ## HTMX Patterns
80
+
81
+ ```html
82
+ <!-- Partial updates -->
83
+ <button hx-post="/api/action" hx-target="#result">Click</button>
84
+
85
+ <!-- Form submission -->
86
+ <form hx-post="/submit" hx-swap="outerHTML">...</form>
87
+
88
+ <!-- Polling -->
89
+ <div hx-get="/status" hx-trigger="every 2s">...</div>
90
+ ```
91
+
92
+ ## Default Routes Provided
93
+
94
+ The following routes are automatically available (DO NOT MODIFY):
95
+
96
+ - **/auth/*** - OAuth and magic link authentication
97
+ - **/health/ping** - Health check endpoint
98
+ - **/debug/** - Debug info (only in DEBUG mode)
99
+ - **/lang/** - Language selection
100
+ - **/user/** - User profile routes
101
+ - **/meta/** - Metadata endpoints
102
+
103
+ ## Development
104
+
105
+ **CRITICAL**: Both processes required:
106
+
107
+ ```bash
108
+ # Terminal 1: Frontend assets
109
+ bun dev
110
+
111
+ # Terminal 2: Backend server
112
+ just local-dev
113
+ ```
@@ -0,0 +1,113 @@
1
+ # Core Frontend Module
2
+
3
+ **IMMUTABLE SCAFFOLDING CODE** - This is the framework's core frontend infrastructure.
4
+
5
+ ## What's Here
6
+
7
+ This module contains the scaffolding's core frontend components:
8
+
9
+ - **routes/** - Essential default routes (auth, health, debug, language, user, meta)
10
+ - **templates.py** - Template rendering with automatic context injection
11
+ - **deps.py** - FastAPI dependencies (authentication, language, etc.)
12
+ - **middleware.py** - Request/response middleware
13
+ - **oauth.py** - OAuth provider integration
14
+ - **email.py** - Magic link email authentication
15
+ - **lifespan.py** - Application startup/shutdown lifecycle
16
+ - **context.py** - Request context management
17
+ - **hotreload.py** - Development hot-reload support
18
+
19
+ ## Important Rules
20
+
21
+ ⚠️ **DO NOT MODIFY** these core frontend components directly.
22
+
23
+ **For changes to core frontend:**
24
+
25
+ - File an issue at `https://github.com/alltuner/scaffolding`
26
+ - Core changes benefit all projects using the scaffolding
27
+
28
+ **For your application routes:**
29
+
30
+ - Create them in `src/app/frontend/routes/` instead
31
+ - Import core components when needed:
32
+ - `from vibetuner.frontend.deps import get_current_user`
33
+ - `from vibetuner.frontend.templates import render_template`
34
+
35
+ ## User Route Pattern (for reference)
36
+
37
+ Your application routes in `src/app/frontend/routes/` should follow this pattern:
38
+
39
+ ```python
40
+ # src/app/frontend/routes/dashboard.py
41
+ from fastapi import APIRouter, Request, Depends
42
+ from vibetuner.frontend.deps import get_current_user
43
+ from vibetuner.frontend.templates import render_template
44
+
45
+ router = APIRouter()
46
+
47
+ @router.get("/dashboard")
48
+ async def dashboard(request: Request, user=Depends(get_current_user)):
49
+ return render_template("dashboard.html.jinja", request, {"user": user})
50
+ ```
51
+
52
+ ## Template Rendering
53
+
54
+ ```python
55
+ # Automatic context in every template:
56
+ {
57
+ "request": request,
58
+ "DEBUG": settings.DEBUG,
59
+ "hotreload": hotreload, # Dev mode
60
+ # ... plus your custom context
61
+ }
62
+ ```
63
+
64
+ ### Template Filters
65
+
66
+ - `{{ datetime | timeago }}` - "2 hours ago"
67
+ - `{{ datetime | format_date }}` - "January 15, 2024"
68
+ - `{{ text | markdown }}` - Convert Markdown to HTML
69
+
70
+ ## Core Dependencies Available
71
+
72
+ Import these from `vibetuner.frontend.deps`:
73
+
74
+ - `get_current_user` - Require authenticated user (raises 403 if not authenticated)
75
+ - `get_current_user_optional` - Optional auth check (returns None if not authenticated)
76
+ - `LangDep` - Current language from cookie/header
77
+ - `MagicCookieDep` - Magic link cookie for authentication
78
+
79
+ ## HTMX Patterns
80
+
81
+ ```html
82
+ <!-- Partial updates -->
83
+ <button hx-post="/api/action" hx-target="#result">Click</button>
84
+
85
+ <!-- Form submission -->
86
+ <form hx-post="/submit" hx-swap="outerHTML">...</form>
87
+
88
+ <!-- Polling -->
89
+ <div hx-get="/status" hx-trigger="every 2s">...</div>
90
+ ```
91
+
92
+ ## Default Routes Provided
93
+
94
+ The following routes are automatically available (DO NOT MODIFY):
95
+
96
+ - **/auth/*** - OAuth and magic link authentication
97
+ - **/health/ping** - Health check endpoint
98
+ - **/debug/** - Debug info (only in DEBUG mode)
99
+ - **/lang/** - Language selection
100
+ - **/user/** - User profile routes
101
+ - **/meta/** - Metadata endpoints
102
+
103
+ ## Development
104
+
105
+ **CRITICAL**: Both processes required:
106
+
107
+ ```bash
108
+ # Terminal 1: Frontend assets
109
+ bun dev
110
+
111
+ # Terminal 2: Backend server
112
+ just local-dev
113
+ ```