vibetuner 2.6.1__py3-none-any.whl → 2.7.0__py3-none-any.whl
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.
- vibetuner/__init__.py +2 -0
- vibetuner/__main__.py +4 -0
- vibetuner/cli/__init__.py +68 -0
- vibetuner/cli/run.py +161 -0
- vibetuner/config.py +128 -0
- vibetuner/context.py +25 -0
- vibetuner/frontend/AGENTS.md +113 -0
- vibetuner/frontend/CLAUDE.md +113 -0
- vibetuner/frontend/__init__.py +94 -0
- vibetuner/frontend/context.py +10 -0
- vibetuner/frontend/deps.py +41 -0
- vibetuner/frontend/email.py +45 -0
- vibetuner/frontend/hotreload.py +13 -0
- vibetuner/frontend/lifespan.py +26 -0
- vibetuner/frontend/middleware.py +151 -0
- vibetuner/frontend/oauth.py +196 -0
- vibetuner/frontend/routes/__init__.py +12 -0
- vibetuner/frontend/routes/auth.py +150 -0
- vibetuner/frontend/routes/debug.py +414 -0
- vibetuner/frontend/routes/health.py +33 -0
- vibetuner/frontend/routes/language.py +43 -0
- vibetuner/frontend/routes/meta.py +55 -0
- vibetuner/frontend/routes/user.py +94 -0
- vibetuner/frontend/templates.py +176 -0
- vibetuner/logging.py +87 -0
- vibetuner/models/AGENTS.md +165 -0
- vibetuner/models/CLAUDE.md +165 -0
- vibetuner/models/__init__.py +14 -0
- vibetuner/models/blob.py +89 -0
- vibetuner/models/email_verification.py +84 -0
- vibetuner/models/mixins.py +76 -0
- vibetuner/models/oauth.py +57 -0
- vibetuner/models/registry.py +15 -0
- vibetuner/models/types.py +16 -0
- vibetuner/models/user.py +91 -0
- vibetuner/mongo.py +18 -0
- vibetuner/paths.py +112 -0
- vibetuner/services/AGENTS.md +104 -0
- vibetuner/services/CLAUDE.md +104 -0
- vibetuner/services/__init__.py +0 -0
- vibetuner/services/blob.py +175 -0
- vibetuner/services/email.py +50 -0
- vibetuner/tasks/AGENTS.md +98 -0
- vibetuner/tasks/CLAUDE.md +98 -0
- vibetuner/tasks/__init__.py +2 -0
- vibetuner/tasks/context.py +34 -0
- vibetuner/tasks/worker.py +18 -0
- vibetuner/templates/email/AGENTS.md +48 -0
- vibetuner/templates/email/CLAUDE.md +48 -0
- vibetuner/templates/email/default/magic_link.html.jinja +16 -0
- vibetuner/templates/email/default/magic_link.txt.jinja +5 -0
- vibetuner/templates/frontend/AGENTS.md +74 -0
- vibetuner/templates/frontend/CLAUDE.md +74 -0
- vibetuner/templates/frontend/base/favicons.html.jinja +1 -0
- vibetuner/templates/frontend/base/footer.html.jinja +3 -0
- vibetuner/templates/frontend/base/header.html.jinja +0 -0
- vibetuner/templates/frontend/base/opengraph.html.jinja +7 -0
- vibetuner/templates/frontend/base/skeleton.html.jinja +42 -0
- vibetuner/templates/frontend/debug/collections.html.jinja +103 -0
- vibetuner/templates/frontend/debug/components/debug_nav.html.jinja +55 -0
- vibetuner/templates/frontend/debug/index.html.jinja +83 -0
- vibetuner/templates/frontend/debug/info.html.jinja +256 -0
- vibetuner/templates/frontend/debug/users.html.jinja +137 -0
- vibetuner/templates/frontend/debug/version.html.jinja +53 -0
- vibetuner/templates/frontend/email/magic_link.txt.jinja +5 -0
- vibetuner/templates/frontend/email_sent.html.jinja +82 -0
- vibetuner/templates/frontend/index.html.jinja +19 -0
- vibetuner/templates/frontend/lang/select.html.jinja +4 -0
- vibetuner/templates/frontend/login.html.jinja +84 -0
- vibetuner/templates/frontend/meta/browserconfig.xml.jinja +10 -0
- vibetuner/templates/frontend/meta/robots.txt.jinja +3 -0
- vibetuner/templates/frontend/meta/site.webmanifest.jinja +7 -0
- vibetuner/templates/frontend/meta/sitemap.xml.jinja +6 -0
- vibetuner/templates/frontend/user/edit.html.jinja +85 -0
- vibetuner/templates/frontend/user/profile.html.jinja +156 -0
- vibetuner/templates/markdown/.placeholder +0 -0
- vibetuner/templates/markdown/AGENTS.md +29 -0
- vibetuner/templates/markdown/CLAUDE.md +29 -0
- vibetuner/templates.py +152 -0
- vibetuner/time.py +57 -0
- vibetuner/versioning.py +8 -0
- {vibetuner-2.6.1.dist-info → vibetuner-2.7.0.dist-info}/METADATA +2 -1
- vibetuner-2.7.0.dist-info/RECORD +84 -0
- vibetuner-2.6.1.dist-info/RECORD +0 -4
- {vibetuner-2.6.1.dist-info → vibetuner-2.7.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{% extends "base/skeleton.html.jinja" %}
|
|
2
|
+
{% block title %}
|
|
3
|
+
{{ _("Sign In") }}
|
|
4
|
+
{% endblock title %}
|
|
5
|
+
{% block body %}
|
|
6
|
+
<div class="min-h-screen bg-gradient-to-br from-primary to-secondary flex items-center justify-center p-4">
|
|
7
|
+
<div class="card w-full max-w-md bg-base-100 shadow-2xl backdrop-blur-sm bg-opacity-95">
|
|
8
|
+
<div class="card-body">
|
|
9
|
+
<!-- Header -->
|
|
10
|
+
<div class="text-center mb-8">
|
|
11
|
+
<h1 class="text-3xl font-bold text-base-content mb-2">{{ _("Welcome Back") }}</h1>
|
|
12
|
+
<p class="text-base-content/70">{{ _("Choose your preferred sign-in method") }}</p>
|
|
13
|
+
</div>
|
|
14
|
+
<!-- OAuth Providers -->
|
|
15
|
+
{% if has_oauth and providers %}
|
|
16
|
+
<div class="space-y-3 mb-6">
|
|
17
|
+
{% for provider in providers %}
|
|
18
|
+
<a href="{{ url_for('login_with_' + provider) }}{%- if next %}?next={{ next }}{%- endif %}"
|
|
19
|
+
class="btn btn-outline btn-block justify-start gap-3 hover:btn-primary group transition-all duration-300 hover:scale-[1.02]">
|
|
20
|
+
<span>{{ _("Continue with %(provider)s") | format(provider=provider.title()) }}</span>
|
|
21
|
+
</a>
|
|
22
|
+
{% endfor %}
|
|
23
|
+
</div>
|
|
24
|
+
{% endif %}
|
|
25
|
+
<!-- Divider -->
|
|
26
|
+
{% if has_oauth and providers and has_email %}<div class="divider text-base-content/50">{{ _("OR") }}</div>{% endif %}
|
|
27
|
+
<!-- Email Magic Link -->
|
|
28
|
+
{% if has_email %}
|
|
29
|
+
<div class="bg-base-200/50 rounded-2xl p-6 border border-base-300/50">
|
|
30
|
+
<form method="post"
|
|
31
|
+
class="space-y-4"
|
|
32
|
+
action="{{ url_for("send_magic_link") }}">
|
|
33
|
+
{% if next %}<input type="hidden" name="next" value="{{ next }}" />{% endif %}
|
|
34
|
+
<div class="form-control">
|
|
35
|
+
<label class="label" for="email">
|
|
36
|
+
<span class="label-text font-medium">{{ _("Email Address") }}</span>
|
|
37
|
+
</label>
|
|
38
|
+
<div class="relative">
|
|
39
|
+
<input type="email"
|
|
40
|
+
id="email"
|
|
41
|
+
name="email"
|
|
42
|
+
class="input input-bordered w-full pr-12 focus:input-primary transition-all duration-300"
|
|
43
|
+
placeholder="{{ _("Enter your email address") }}"
|
|
44
|
+
required
|
|
45
|
+
autocomplete="email" />
|
|
46
|
+
<div class="absolute inset-y-0 right-0 flex items-center pr-3">
|
|
47
|
+
<svg class="w-5 h-5 text-base-content/40"
|
|
48
|
+
fill="none"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
viewBox="0 0 24 24">
|
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
52
|
+
</svg>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<button type="submit"
|
|
57
|
+
class="btn btn-primary btn-block gap-2 group hover:scale-[1.02] transition-all duration-300">
|
|
58
|
+
<svg class="w-5 h-5 group-hover:rotate-12 transition-transform"
|
|
59
|
+
fill="none"
|
|
60
|
+
stroke="currentColor"
|
|
61
|
+
viewBox="0 0 24 24">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
63
|
+
</svg>
|
|
64
|
+
<span>{{ _("Send Magic Link") }}</span>
|
|
65
|
+
</button>
|
|
66
|
+
<div class="alert alert-info bg-info/10 border-info/20">
|
|
67
|
+
<svg class="w-5 h-5 text-info shrink-0"
|
|
68
|
+
fill="none"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
viewBox="0 0 24 24">
|
|
71
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
72
|
+
</svg>
|
|
73
|
+
<div class="text-sm">
|
|
74
|
+
<p class="font-medium text-info">{{ _("No password required!") }}</p>
|
|
75
|
+
<p class="text-info/80">{{ _("We'll send you a secure link to sign in instantly.") }}</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</form>
|
|
79
|
+
</div>
|
|
80
|
+
{% endif %}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
{% endblock body %}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{% extends "base/skeleton.html.jinja" %}
|
|
2
|
+
{% block title %}
|
|
3
|
+
{{ _("Edit Profile") }}
|
|
4
|
+
{% endblock title %}
|
|
5
|
+
{% block body %}
|
|
6
|
+
<div class="min-h-screen bg-gray-50">
|
|
7
|
+
<div class="container mx-auto px-4 py-8">
|
|
8
|
+
<div class="max-w-2xl mx-auto">
|
|
9
|
+
<!-- Page Header -->
|
|
10
|
+
<div class="mb-8">
|
|
11
|
+
<h1 class="text-3xl font-bold text-gray-900">{{ _("Edit Profile") }}</h1>
|
|
12
|
+
<p class="text-gray-600 mt-2">{{ _("Update your account information and preferences") }}</p>
|
|
13
|
+
</div>
|
|
14
|
+
<!-- Edit Form -->
|
|
15
|
+
<div class="bg-white shadow-lg rounded-lg p-6">
|
|
16
|
+
<form method="post" action="{{ url_for("user_edit_submit") }}">
|
|
17
|
+
<div class="space-y-6">
|
|
18
|
+
<!-- Name Field -->
|
|
19
|
+
<div>
|
|
20
|
+
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">{{ _("Display Name") }}</label>
|
|
21
|
+
<input type="text"
|
|
22
|
+
id="name"
|
|
23
|
+
name="name"
|
|
24
|
+
value="{{ user.name or '' }}"
|
|
25
|
+
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
26
|
+
required />
|
|
27
|
+
<p class="text-sm text-gray-600 mt-1">{{ _("This name will be displayed throughout the application") }}</p>
|
|
28
|
+
</div>
|
|
29
|
+
<!-- Language Preference -->
|
|
30
|
+
<div>
|
|
31
|
+
<label for="language" class="block text-sm font-medium text-gray-700 mb-2">{{ _("Preferred Language") }}</label>
|
|
32
|
+
<select id="language"
|
|
33
|
+
name="language"
|
|
34
|
+
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
35
|
+
<option value="">{{ _("Use browser default") }}</option>
|
|
36
|
+
{% for lang_code, lang_name in locale_names.items() %}
|
|
37
|
+
<option value="{{ lang_code }}"
|
|
38
|
+
{% if current_language and current_language == lang_code %}selected{% endif %}>
|
|
39
|
+
{{ lang_name }}
|
|
40
|
+
</option>
|
|
41
|
+
{% endfor %}
|
|
42
|
+
</select>
|
|
43
|
+
<p class="text-sm text-gray-600 mt-1">{{ _("This will override browser language detection") }}</p>
|
|
44
|
+
</div>
|
|
45
|
+
<!-- Email (read-only) -->
|
|
46
|
+
<div>
|
|
47
|
+
<label for="email" class="block text-sm font-medium text-gray-700 mb-2">{{ _("Email Address") }}</label>
|
|
48
|
+
<input type="email"
|
|
49
|
+
id="email"
|
|
50
|
+
value="{{ user.email or '' }}"
|
|
51
|
+
class="w-full px-4 py-2 bg-gray-50 border border-gray-300 rounded-md text-gray-600"
|
|
52
|
+
readonly />
|
|
53
|
+
<p class="text-sm text-gray-600 mt-1">{{ _("Email cannot be changed and is managed through your OAuth provider") }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<!-- Form Actions -->
|
|
57
|
+
<div class="flex justify-between items-center mt-8 pt-6 border-t border-gray-200">
|
|
58
|
+
<a href="{{ url_for("user_profile") }}" class="btn btn-ghost">{{ _("Cancel") }}</a>
|
|
59
|
+
<button type="submit" class="btn btn-primary">{{ _("Save Changes") }}</button>
|
|
60
|
+
</div>
|
|
61
|
+
</form>
|
|
62
|
+
</div>
|
|
63
|
+
<!-- Additional Info Card -->
|
|
64
|
+
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
65
|
+
<div class="flex items-start">
|
|
66
|
+
<div class="flex-shrink-0">
|
|
67
|
+
<svg class="h-5 w-5 text-blue-600"
|
|
68
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
69
|
+
viewBox="0 0 20 20"
|
|
70
|
+
fill="currentColor">
|
|
71
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
|
72
|
+
</svg>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="ml-3">
|
|
75
|
+
<h3 class="text-sm font-medium text-blue-900">{{ _("About Language Preferences") }}</h3>
|
|
76
|
+
<p class="mt-1 text-sm text-blue-700">
|
|
77
|
+
{{ _("Your language preference will take priority over URL parameters, cookies, and browser settings. Leave empty to use automatic detection.") }}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
{% endblock body %}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
{% extends "base/skeleton.html.jinja" %}
|
|
2
|
+
{% block title %}
|
|
3
|
+
{{ _("User Profile") }}
|
|
4
|
+
{% endblock title %}
|
|
5
|
+
{% block body %}
|
|
6
|
+
<div class="min-h-screen bg-gray-50">
|
|
7
|
+
<div class="container mx-auto px-4 py-8">
|
|
8
|
+
<div class="max-w-4xl mx-auto">
|
|
9
|
+
<!-- Page Header -->
|
|
10
|
+
<div class="mb-8">
|
|
11
|
+
<h1 class="text-3xl font-bold text-gray-900">{{ _("User Profile") }}</h1>
|
|
12
|
+
<p class="text-gray-600 mt-2">{{ _("Manage your account information and settings") }}</p>
|
|
13
|
+
</div>
|
|
14
|
+
<!-- Profile Card -->
|
|
15
|
+
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
|
|
16
|
+
<!-- Profile Header -->
|
|
17
|
+
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 px-6 py-8">
|
|
18
|
+
<div class="flex items-center space-x-4">
|
|
19
|
+
<!-- Avatar -->
|
|
20
|
+
{% if user.picture or (user.oauth_accounts and user.oauth_accounts[0].picture) %}
|
|
21
|
+
<img src="{{ user.picture or user.oauth_accounts[0].picture }}"
|
|
22
|
+
alt="{{ user.email }}"
|
|
23
|
+
width="80"
|
|
24
|
+
height="80"
|
|
25
|
+
class="w-20 h-20 rounded-full object-cover border-4 border-white shadow-lg" />
|
|
26
|
+
{% else %}
|
|
27
|
+
<div class="w-20 h-20 bg-white rounded-full flex items-center justify-center text-3xl font-bold text-gray-700 shadow-lg">
|
|
28
|
+
{{ user.email[0].upper() if user.email }}
|
|
29
|
+
</div>
|
|
30
|
+
{% endif %}
|
|
31
|
+
<!-- User Info -->
|
|
32
|
+
<div class="text-white">
|
|
33
|
+
<h2 class="text-2xl font-semibold">{{ user.email }}</h2>
|
|
34
|
+
<p class="text-blue-100">{{ _("Member since") }} {{ user.db_insert_dt | timeago }}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<!-- Profile Content -->
|
|
39
|
+
<div class="p-6">
|
|
40
|
+
<!-- Account Information Section -->
|
|
41
|
+
<div class="mb-8">
|
|
42
|
+
<h3 class="text-xl font-semibold text-gray-800 mb-4">{{ _("Account Information") }}</h3>
|
|
43
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
44
|
+
<!-- Email -->
|
|
45
|
+
<div>
|
|
46
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("Email Address") }}</label>
|
|
47
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
48
|
+
<p class="text-gray-900">{{ user.email }}</p>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<!-- User ID -->
|
|
52
|
+
<div>
|
|
53
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("User ID") }}</label>
|
|
54
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
55
|
+
<p class="text-gray-900 font-mono text-sm">{{ user.id }}</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<!-- Created Date -->
|
|
59
|
+
<div>
|
|
60
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("Account Created") }}</label>
|
|
61
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
62
|
+
<p class="text-gray-900">{{ user.db_insert_dt | format_datetime }}</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<!-- Last Updated -->
|
|
66
|
+
<div>
|
|
67
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("Last Updated") }}</label>
|
|
68
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
69
|
+
<p class="text-gray-900">{{ user.db_update_dt | format_datetime }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<!-- OAuth Accounts Section -->
|
|
75
|
+
{% if user.oauth_accounts %}
|
|
76
|
+
<div class="mb-8">
|
|
77
|
+
<h3 class="text-xl font-semibold text-gray-800 mb-4">{{ _("Connected Accounts") }}</h3>
|
|
78
|
+
<div class="space-y-3">
|
|
79
|
+
{% for account in user.oauth_accounts %}
|
|
80
|
+
<div class="flex items-center justify-between bg-gray-50 px-4 py-3 rounded-md">
|
|
81
|
+
<div class="flex items-center space-x-3">
|
|
82
|
+
{% if account.picture %}
|
|
83
|
+
<img src="{{ account.picture }}"
|
|
84
|
+
alt="{{ account.provider }}"
|
|
85
|
+
width="40"
|
|
86
|
+
height="40"
|
|
87
|
+
class="w-10 h-10 rounded-full object-cover" />
|
|
88
|
+
{% else %}
|
|
89
|
+
<div class="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center">
|
|
90
|
+
<span class="text-gray-600 font-medium">{{ account.provider[0].upper() }}</span>
|
|
91
|
+
</div>
|
|
92
|
+
{% endif %}
|
|
93
|
+
<div>
|
|
94
|
+
<p class="font-medium text-gray-900">{{ account.provider|title }}</p>
|
|
95
|
+
<p class="text-sm text-gray-600">{{ account.email or _("No email") }}</p>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<span class="text-sm text-green-600 font-medium">{{ _("Connected") }}</span>
|
|
99
|
+
</div>
|
|
100
|
+
{% endfor %}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
{% endif %}
|
|
104
|
+
<!-- Actions -->
|
|
105
|
+
<div class="border-t pt-6">
|
|
106
|
+
<div class="flex flex-col sm:flex-row gap-4">
|
|
107
|
+
<button class="btn btn-primary"
|
|
108
|
+
hx-get="/user/edit"
|
|
109
|
+
hx-target="#main-content"
|
|
110
|
+
hx-swap="innerHTML">{{ _("Edit Profile") }}</button>
|
|
111
|
+
<a href="{{ url_for("auth_logout") }}" class="btn btn-outline btn-error">{{ _("Sign Out") }}</a>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
<!-- Additional Settings Card -->
|
|
117
|
+
<div class="mt-6 bg-white shadow-lg rounded-lg p-6">
|
|
118
|
+
<h3 class="text-xl font-semibold text-gray-800 mb-4">{{ _("Account Settings") }}</h3>
|
|
119
|
+
<div class="space-y-4">
|
|
120
|
+
<!-- Language Setting -->
|
|
121
|
+
<div class="flex items-center justify-between">
|
|
122
|
+
<div>
|
|
123
|
+
<p class="font-medium text-gray-900">{{ _("Preferred Language") }}</p>
|
|
124
|
+
<p class="text-sm text-gray-600">{{ _("Your preferred language for the interface") }}</p>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="text-gray-900">
|
|
127
|
+
{% if user.user_settings.language %}
|
|
128
|
+
<span class="badge badge-primary">{{ user.user_settings.language|upper }}</span>
|
|
129
|
+
{% else %}
|
|
130
|
+
<span class="text-gray-500">{{ _("Not set") }}</span>
|
|
131
|
+
{% endif %}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<!-- Additional settings can be added here as the UserSettings model is extended -->
|
|
135
|
+
<!-- Email Notifications -->
|
|
136
|
+
<div class="flex items-center justify-between">
|
|
137
|
+
<div>
|
|
138
|
+
<p class="font-medium text-gray-900">{{ _("Email Notifications") }}</p>
|
|
139
|
+
<p class="text-sm text-gray-600">{{ _("Receive updates about your account") }}</p>
|
|
140
|
+
</div>
|
|
141
|
+
<input type="checkbox" class="toggle toggle-primary" checked />
|
|
142
|
+
</div>
|
|
143
|
+
<!-- Privacy Settings -->
|
|
144
|
+
<div class="flex items-center justify-between">
|
|
145
|
+
<div>
|
|
146
|
+
<p class="font-medium text-gray-900">{{ _("Profile Visibility") }}</p>
|
|
147
|
+
<p class="text-sm text-gray-600">{{ _("Make your profile visible to others") }}</p>
|
|
148
|
+
</div>
|
|
149
|
+
<input type="checkbox" class="toggle toggle-primary" />
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
{% endblock body %}
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Core Markdown Templates - DO NOT MODIFY
|
|
2
|
+
|
|
3
|
+
**⚠️ IMPORTANT**: Package-managed files. Changes will be lost on package updates.
|
|
4
|
+
|
|
5
|
+
## How to Override
|
|
6
|
+
|
|
7
|
+
**NEVER modify files in this directory!** Instead:
|
|
8
|
+
|
|
9
|
+
1. Copy template to your project's `templates/markdown/`
|
|
10
|
+
2. Maintain the same directory structure
|
|
11
|
+
3. Your version overrides automatically
|
|
12
|
+
|
|
13
|
+
### Example
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Core template (if exists, bundled in vibetuner package):
|
|
17
|
+
vibetuner/templates/markdown/default/template.md.jinja
|
|
18
|
+
|
|
19
|
+
# Your override (CREATE THIS in your project):
|
|
20
|
+
templates/markdown/default/template.md.jinja
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Template Structure
|
|
24
|
+
|
|
25
|
+
Currently, no core markdown templates are provided by the package.
|
|
26
|
+
|
|
27
|
+
This directory is a placeholder for potential future package markdown templates.
|
|
28
|
+
|
|
29
|
+
All markdown templates should be created in `templates/markdown/`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Core Markdown Templates - DO NOT MODIFY
|
|
2
|
+
|
|
3
|
+
**⚠️ IMPORTANT**: Package-managed files. Changes will be lost on package updates.
|
|
4
|
+
|
|
5
|
+
## How to Override
|
|
6
|
+
|
|
7
|
+
**NEVER modify files in this directory!** Instead:
|
|
8
|
+
|
|
9
|
+
1. Copy template to your project's `templates/markdown/`
|
|
10
|
+
2. Maintain the same directory structure
|
|
11
|
+
3. Your version overrides automatically
|
|
12
|
+
|
|
13
|
+
### Example
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Core template (if exists, bundled in vibetuner package):
|
|
17
|
+
vibetuner/templates/markdown/default/template.md.jinja
|
|
18
|
+
|
|
19
|
+
# Your override (CREATE THIS in your project):
|
|
20
|
+
templates/markdown/default/template.md.jinja
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Template Structure
|
|
24
|
+
|
|
25
|
+
Currently, no core markdown templates are provided by the package.
|
|
26
|
+
|
|
27
|
+
This directory is a placeholder for potential future package markdown templates.
|
|
28
|
+
|
|
29
|
+
All markdown templates should be created in `templates/markdown/`.
|
vibetuner/templates.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
|
5
|
+
|
|
6
|
+
from .paths import (
|
|
7
|
+
app_templates,
|
|
8
|
+
core_templates,
|
|
9
|
+
email_templates,
|
|
10
|
+
frontend_templates,
|
|
11
|
+
markdown_templates,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_base_paths_for_namespace(
|
|
16
|
+
namespace: str | None,
|
|
17
|
+
template_path: Path | list[Path] | None,
|
|
18
|
+
) -> list[Path]:
|
|
19
|
+
"""Get base template paths based on namespace and template_path."""
|
|
20
|
+
if template_path is not None:
|
|
21
|
+
return [template_path] if isinstance(template_path, Path) else template_path
|
|
22
|
+
|
|
23
|
+
# Map known namespaces to their predefined paths
|
|
24
|
+
if namespace == "email":
|
|
25
|
+
return email_templates
|
|
26
|
+
if namespace == "markdown":
|
|
27
|
+
return markdown_templates
|
|
28
|
+
if namespace == "frontend":
|
|
29
|
+
return frontend_templates
|
|
30
|
+
|
|
31
|
+
# Default for unknown or None namespace
|
|
32
|
+
# Only include app_templates if project root has been set
|
|
33
|
+
paths = []
|
|
34
|
+
if app_templates is not None:
|
|
35
|
+
paths.append(app_templates)
|
|
36
|
+
paths.append(core_templates)
|
|
37
|
+
return paths
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _build_search_paths(
|
|
41
|
+
base_paths: list[Path],
|
|
42
|
+
namespace: str | None,
|
|
43
|
+
template_path: Path | list[Path] | None,
|
|
44
|
+
) -> list[Path]:
|
|
45
|
+
"""Build list of directories to search for templates."""
|
|
46
|
+
search_paths: list[Path] = []
|
|
47
|
+
known_namespaces = ("email", "markdown", "frontend")
|
|
48
|
+
|
|
49
|
+
for base_path in base_paths:
|
|
50
|
+
# If namespace is known and we're using default paths, they already include it
|
|
51
|
+
if namespace in known_namespaces and template_path is None:
|
|
52
|
+
if base_path.is_dir():
|
|
53
|
+
search_paths.append(base_path)
|
|
54
|
+
elif namespace:
|
|
55
|
+
# Append namespace to path for custom namespaces or explicit paths
|
|
56
|
+
ns_path = base_path / namespace
|
|
57
|
+
if ns_path.is_dir():
|
|
58
|
+
search_paths.append(ns_path)
|
|
59
|
+
else:
|
|
60
|
+
if base_path.is_dir():
|
|
61
|
+
search_paths.append(base_path)
|
|
62
|
+
|
|
63
|
+
return search_paths
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _render_template_with_env(
|
|
67
|
+
env: Environment,
|
|
68
|
+
jinja_template_name: str,
|
|
69
|
+
lang: str | None,
|
|
70
|
+
context: dict[str, Any],
|
|
71
|
+
) -> str:
|
|
72
|
+
"""Render template using Jinja environment with language fallback."""
|
|
73
|
+
# Try language-specific folder first
|
|
74
|
+
if lang:
|
|
75
|
+
try:
|
|
76
|
+
template = env.get_template(f"{lang}/{jinja_template_name}")
|
|
77
|
+
return template.render(**context)
|
|
78
|
+
except TemplateNotFound:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# Fallback to default folder
|
|
82
|
+
template = env.get_template(f"default/{jinja_template_name}")
|
|
83
|
+
return template.render(**context)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def render_static_template(
|
|
87
|
+
template_name: str,
|
|
88
|
+
*,
|
|
89
|
+
template_path: Path | list[Path] | None = None,
|
|
90
|
+
namespace: Optional[str] = None,
|
|
91
|
+
context: Optional[Dict[str, Any]] = None,
|
|
92
|
+
lang: Optional[str] = None,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Render a Jinja template with optional i18n and namespace support.
|
|
95
|
+
|
|
96
|
+
This simplified functional helper replaces the old ``TemplateRenderer``
|
|
97
|
+
class while adding **namespace** awareness:
|
|
98
|
+
|
|
99
|
+
1. Optionally switch to *template_path / namespace* if that directory
|
|
100
|
+
exists, letting you segment templates per tenant, brand, or feature
|
|
101
|
+
module without changing call‑sites.
|
|
102
|
+
2. Within the selected base directory attempt ``<lang>/<name>.jinja``
|
|
103
|
+
when *lang* is provided.
|
|
104
|
+
3. Fallback to ``default/<name>.jinja``.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
template_name: Base filename without extension (e.g. ``"invoice"``).
|
|
108
|
+
template_path: Root directory or list of directories containing template
|
|
109
|
+
collections. When a list is provided, searches in order (project templates
|
|
110
|
+
override package templates). Defaults to the package's bundled templates.
|
|
111
|
+
namespace: Optional subfolder under *template_path* to confine the
|
|
112
|
+
lookup. Ignored when the directory does not exist.
|
|
113
|
+
context: Variables passed to the template while rendering.
|
|
114
|
+
lang: Language code such as ``"en"`` or ``"es"`` for localized
|
|
115
|
+
templates.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The rendered template as a string.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
TemplateNotFound: When no suitable template could be located after all
|
|
122
|
+
fallbacks.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
context = context or {}
|
|
126
|
+
|
|
127
|
+
# Determine base paths from namespace and template_path
|
|
128
|
+
base_paths = _get_base_paths_for_namespace(namespace, template_path)
|
|
129
|
+
|
|
130
|
+
# Build search paths from base paths
|
|
131
|
+
search_paths = _build_search_paths(base_paths, namespace, template_path)
|
|
132
|
+
|
|
133
|
+
if not search_paths:
|
|
134
|
+
raise TemplateNotFound(
|
|
135
|
+
f"No valid template paths found for namespace '{namespace}'"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Create Jinja environment with search paths
|
|
139
|
+
env = Environment( # noqa: S701
|
|
140
|
+
loader=FileSystemLoader(search_paths),
|
|
141
|
+
trim_blocks=True,
|
|
142
|
+
lstrip_blocks=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Render template with language fallback
|
|
146
|
+
jinja_template_name = f"{template_name}.jinja"
|
|
147
|
+
try:
|
|
148
|
+
return _render_template_with_env(env, jinja_template_name, lang, context)
|
|
149
|
+
except TemplateNotFound as err:
|
|
150
|
+
raise TemplateNotFound(
|
|
151
|
+
f"Template '{jinja_template_name}' not found under '{search_paths}'."
|
|
152
|
+
) from err
|
vibetuner/time.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from datetime import (
|
|
2
|
+
UTC,
|
|
3
|
+
datetime,
|
|
4
|
+
timedelta,
|
|
5
|
+
)
|
|
6
|
+
from enum import StrEnum, auto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Unit(StrEnum):
|
|
10
|
+
"""Return units for `.age_in()`."""
|
|
11
|
+
|
|
12
|
+
SECONDS = auto()
|
|
13
|
+
MINUTES = auto()
|
|
14
|
+
HOURS = auto()
|
|
15
|
+
DAYS = auto()
|
|
16
|
+
WEEKS = auto()
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def factor(self) -> int:
|
|
20
|
+
return {
|
|
21
|
+
Unit.SECONDS: 1,
|
|
22
|
+
Unit.MINUTES: 60,
|
|
23
|
+
Unit.HOURS: 3_600,
|
|
24
|
+
Unit.DAYS: 86_400,
|
|
25
|
+
Unit.WEEKS: 604_800,
|
|
26
|
+
}[self]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def now() -> datetime:
|
|
30
|
+
return datetime.now(UTC)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def age_in_days(dt: datetime) -> int:
|
|
34
|
+
# Ensure dt is timezone-aware, if it isn't already
|
|
35
|
+
if dt.tzinfo is None:
|
|
36
|
+
dt = dt.replace(tzinfo=UTC)
|
|
37
|
+
|
|
38
|
+
return int((now() - dt).total_seconds() / 60 / 60 / 24)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def age_in_minutes(dt: datetime) -> int:
|
|
42
|
+
# Ensure dt is timezone-aware, if it isn't already
|
|
43
|
+
if dt.tzinfo is None:
|
|
44
|
+
dt = dt.replace(tzinfo=UTC)
|
|
45
|
+
|
|
46
|
+
return int((now() - dt).total_seconds() / 60)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def age_in_timedelta(dt: datetime) -> timedelta:
|
|
50
|
+
# Ensure dt is timezone-aware, if it isn't already
|
|
51
|
+
if dt.tzinfo is None:
|
|
52
|
+
dt = dt.replace(tzinfo=UTC)
|
|
53
|
+
|
|
54
|
+
return now() - dt
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Custom functions below
|
vibetuner/versioning.py
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: vibetuner
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.0
|
|
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'
|