splent-feature-settings 0.2.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.
Files changed (28) hide show
  1. splent_feature_settings-0.2.1/MANIFEST.in +8 -0
  2. splent_feature_settings-0.2.1/PKG-INFO +5 -0
  3. splent_feature_settings-0.2.1/pyproject.toml +55 -0
  4. splent_feature_settings-0.2.1/setup.cfg +4 -0
  5. splent_feature_settings-0.2.1/src/splent_feature_settings.egg-info/PKG-INFO +5 -0
  6. splent_feature_settings-0.2.1/src/splent_feature_settings.egg-info/SOURCES.txt +26 -0
  7. splent_feature_settings-0.2.1/src/splent_feature_settings.egg-info/dependency_links.txt +1 -0
  8. splent_feature_settings-0.2.1/src/splent_feature_settings.egg-info/top_level.txt +1 -0
  9. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/__init__.py +22 -0
  10. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/assets/dist/splent_feature_settings.bundle.js +20 -0
  11. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/assets/dist/splent_feature_settings.bundle.js.map +1 -0
  12. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/commands.py +21 -0
  13. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/config.py +19 -0
  14. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/forms.py +6 -0
  15. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/hooks.py +26 -0
  16. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/migrations/alembic.ini +36 -0
  17. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/migrations/env.py +9 -0
  18. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/migrations/script.py.mako +24 -0
  19. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/migrations/versions/settings0001_initial.py +29 -0
  20. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/models.py +16 -0
  21. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/repositories.py +7 -0
  22. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/routes.py +46 -0
  23. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/seeders.py +10 -0
  24. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/services.py +39 -0
  25. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/signals.py +19 -0
  26. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/templates/settings/admin/feature.html +58 -0
  27. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/templates/settings/admin/index.html +25 -0
  28. splent_feature_settings-0.2.1/src/splent_io/splent_feature_settings/templates/settings/index.html +10 -0
@@ -0,0 +1,8 @@
1
+ # Compiled frontend bundles
2
+ recursive-include src/splent_io/splent_feature_settings/assets/dist *.js *.css *.map
3
+
4
+ # Jinja templates (feature views + hook fragments)
5
+ recursive-include src/splent_io/splent_feature_settings/templates *.html
6
+
7
+ # Alembic migration config and scripts
8
+ recursive-include src/splent_io/splent_feature_settings/migrations *.py *.ini *.mako
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_settings
3
+ Version: 0.2.1
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80.3.1", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "splent_feature_settings"
7
+ version = "0.2.1"
8
+ readme = "README.md"
9
+ requires-python = ">=3.13"
10
+
11
+ dependencies = [
12
+
13
+ ]
14
+
15
+ [tool.setuptools]
16
+ package-dir = { "" = "src" }
17
+ include-package-data = true
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+ exclude = ["*.tests", "*.tests.*"]
22
+
23
+ [tool.splent]
24
+ cli_version = "1.11.0"
25
+
26
+ # ── Feature Contract (auto-generated) ────────────────────────────────────────
27
+ # Do not edit manually — re-run `splent feature:contract --write` to refresh.
28
+ # Preserved across refreshes: description, category, tags, env, and
29
+ # requires.features_manual (dependencies invisible to static analysis).
30
+ [tool.splent.contract]
31
+ description = "Key-value site settings storage used by theme and admin"
32
+ archetype = "full"
33
+
34
+ [tool.splent.contract.provides]
35
+ routes = ["/admin/settings", "/admin/settings/<feature>"]
36
+ blueprints = []
37
+ models = ["Setting"]
38
+ commands = ["hello"]
39
+ hooks = ["layout.authenticated_sidebar"]
40
+ services = ["SettingsService"]
41
+ signals = []
42
+ translations = []
43
+ docker = []
44
+
45
+ [tool.splent.contract.requires]
46
+ features = []
47
+ env_vars = []
48
+ signals = []
49
+
50
+ [tool.splent.contract.extensible]
51
+ services = ["SettingsService"]
52
+ templates = ["settings/admin/feature.html", "settings/admin/index.html", "settings/index.html"]
53
+ models = ["Setting"]
54
+ hooks = ["layout.authenticated_sidebar"]
55
+ routes = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: splent_feature_settings
3
+ Version: 0.2.1
4
+ Requires-Python: >=3.13
5
+ Description-Content-Type: text/markdown
@@ -0,0 +1,26 @@
1
+ MANIFEST.in
2
+ pyproject.toml
3
+ src/splent_feature_settings.egg-info/PKG-INFO
4
+ src/splent_feature_settings.egg-info/SOURCES.txt
5
+ src/splent_feature_settings.egg-info/dependency_links.txt
6
+ src/splent_feature_settings.egg-info/top_level.txt
7
+ src/splent_io/splent_feature_settings/__init__.py
8
+ src/splent_io/splent_feature_settings/commands.py
9
+ src/splent_io/splent_feature_settings/config.py
10
+ src/splent_io/splent_feature_settings/forms.py
11
+ src/splent_io/splent_feature_settings/hooks.py
12
+ src/splent_io/splent_feature_settings/models.py
13
+ src/splent_io/splent_feature_settings/repositories.py
14
+ src/splent_io/splent_feature_settings/routes.py
15
+ src/splent_io/splent_feature_settings/seeders.py
16
+ src/splent_io/splent_feature_settings/services.py
17
+ src/splent_io/splent_feature_settings/signals.py
18
+ src/splent_io/splent_feature_settings/assets/dist/splent_feature_settings.bundle.js
19
+ src/splent_io/splent_feature_settings/assets/dist/splent_feature_settings.bundle.js.map
20
+ src/splent_io/splent_feature_settings/migrations/alembic.ini
21
+ src/splent_io/splent_feature_settings/migrations/env.py
22
+ src/splent_io/splent_feature_settings/migrations/script.py.mako
23
+ src/splent_io/splent_feature_settings/migrations/versions/settings0001_initial.py
24
+ src/splent_io/splent_feature_settings/templates/settings/index.html
25
+ src/splent_io/splent_feature_settings/templates/settings/admin/feature.html
26
+ src/splent_io/splent_feature_settings/templates/settings/admin/index.html
@@ -0,0 +1,22 @@
1
+ from splent_framework.blueprints.base_blueprint import create_blueprint
2
+ from splent_framework.services.service_locator import register_service, service_proxy
3
+
4
+ from splent_io.splent_feature_settings.services import SettingsService
5
+
6
+ settings_bp = create_blueprint(__name__)
7
+
8
+
9
+ def init_feature(app):
10
+ register_service(app, "SettingsService", SettingsService)
11
+
12
+
13
+ def inject_context_vars(app):
14
+ # {{ setting('key', default) }} — read a runtime, admin-editable setting,
15
+ # falling back to the given default. Safe to call even mid-request.
16
+ def setting(key, default=""):
17
+ try:
18
+ return service_proxy("SettingsService").get(key, default)
19
+ except Exception:
20
+ return default
21
+
22
+ return {"setting": setting}
@@ -0,0 +1,20 @@
1
+ /******/ (() => { // webpackBootstrap
2
+ /*!*********************************************************************************************!*\
3
+ !*** ../splent_feature_settings/src/splent_io/splent_feature_settings/assets/js/scripts.js ***!
4
+ \*********************************************************************************************/
5
+ // Entry point for splent_feature_settings frontend assets.
6
+ // Add your JavaScript here. Webpack compiles this into assets/dist/splent_feature_settings.bundle.js
7
+ //
8
+ // To load the compiled bundle in the product layout, register it in hooks.py:
9
+ //
10
+ // from splent_framework.hooks.template_hooks import register_template_hook
11
+ // from flask import url_for
12
+ //
13
+ // def settings_scripts():
14
+ // return '<script src="' + url_for("settings.assets", subfolder="dist", filename="splent_feature_settings.bundle.js") + '"></script>'
15
+ //
16
+ // register_template_hook("layout.scripts", settings_scripts)
17
+
18
+ /******/ })()
19
+ ;
20
+ //# sourceMappingURL=splent_feature_settings.bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splent_feature_settings.bundle.js","mappings":";;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://diversolab_app/../splent_feature_settings/src/splent_io/splent_feature_settings/assets/js/scripts.js"],"sourcesContent":["// Entry point for splent_feature_settings frontend assets.\n// Add your JavaScript here. Webpack compiles this into assets/dist/splent_feature_settings.bundle.js\n//\n// To load the compiled bundle in the product layout, register it in hooks.py:\n//\n// from splent_framework.hooks.template_hooks import register_template_hook\n// from flask import url_for\n//\n// def settings_scripts():\n// return '<script src=\"' + url_for(\"settings.assets\", subfolder=\"dist\", filename=\"splent_feature_settings.bundle.js\") + '\"></script>'\n//\n// register_template_hook(\"layout.scripts\", settings_scripts)\n"],"names":[],"sourceRoot":""}
@@ -0,0 +1,21 @@
1
+ """
2
+ CLI commands contributed by splent_feature_settings.
3
+
4
+ These commands are auto-discovered by the framework and exposed in the
5
+ SPLENT CLI under the ``feature:settings`` group.
6
+
7
+ Usage::
8
+
9
+ splent feature:settings hello
10
+ """
11
+
12
+ import click
13
+
14
+
15
+ @click.command("hello")
16
+ def hello():
17
+ """Example command — replace with your own."""
18
+ click.echo(" Hello from splent_feature_settings!")
19
+
20
+
21
+ cli_commands = [hello]
@@ -0,0 +1,19 @@
1
+ """
2
+ settings feature configuration.
3
+
4
+ Injects environment variables into Flask app.config.
5
+ Add your feature's env vars here so the framework can track them.
6
+
7
+ To regenerate from source code: splent feature:inject-config splent_feature_settings
8
+ """
9
+
10
+ import os # noqa: F401 — used when adding env vars below
11
+
12
+
13
+ def inject_config(app):
14
+ app.config.update(
15
+ {
16
+ # Add your feature's env vars here, e.g.:
17
+ # "MY_VAR": os.getenv("MY_VAR", "default_value"),
18
+ }
19
+ )
@@ -0,0 +1,6 @@
1
+ from flask_wtf import FlaskForm
2
+ from wtforms import SubmitField
3
+
4
+
5
+ class SplentFeatureSettingsForm(FlaskForm):
6
+ submit = SubmitField("Save splent_feature_settings")
@@ -0,0 +1,26 @@
1
+ """settings injects a single "Settings" entry into the admin sidebar, which
2
+ lists every feature that declared a settings schema (via register_settings).
3
+ """
4
+
5
+ from flask import request, url_for
6
+
7
+ from splent_framework.hooks.template_hooks import register_template_hook
8
+
9
+
10
+ def settings_admin_link():
11
+ active = (
12
+ "active"
13
+ if request.endpoint and request.endpoint.startswith("settings.admin")
14
+ else ""
15
+ )
16
+ return (
17
+ f'<li class="sidebar-item {active}">'
18
+ f'<a class="sidebar-link" href="{url_for("settings.admin_index")}">'
19
+ '<i class="align-middle" data-feather="settings"></i> '
20
+ '<span class="align-middle">Settings</span>'
21
+ "</a>"
22
+ "</li>"
23
+ )
24
+
25
+
26
+ register_template_hook("layout.authenticated_sidebar", settings_admin_link)
@@ -0,0 +1,36 @@
1
+ [alembic]
2
+ script_location = migrations
3
+
4
+ [loggers]
5
+ keys = root,sqlalchemy,alembic
6
+
7
+ [handlers]
8
+ keys = console
9
+
10
+ [formatters]
11
+ keys = generic
12
+
13
+ [logger_root]
14
+ level = WARN
15
+ handlers = console
16
+ qualname =
17
+
18
+ [logger_sqlalchemy]
19
+ level = WARN
20
+ handlers =
21
+ qualname = sqlalchemy.engine
22
+
23
+ [logger_alembic]
24
+ level = INFO
25
+ handlers =
26
+ qualname = alembic
27
+
28
+ [handler_console]
29
+ class = StreamHandler
30
+ args = (sys.stderr,)
31
+ level = NOTSET
32
+ formatter = generic
33
+
34
+ [formatter_generic]
35
+ format = %(levelname)-5.5s [%(name)s] %(message)s
36
+ datefmt = %H:%M:%S
@@ -0,0 +1,9 @@
1
+ """Alembic migration environment for splent_feature_settings."""
2
+
3
+ from splent_io.splent_feature_settings import models # noqa
4
+ from splent_framework.migrations.feature_env import run_feature_migrations
5
+
6
+ FEATURE_NAME = "splent_feature_settings"
7
+ FEATURE_TABLES = set()
8
+
9
+ run_feature_migrations(FEATURE_NAME, FEATURE_TABLES)
@@ -0,0 +1,24 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade():
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade():
24
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,29 @@
1
+ """setting table (runtime key/value settings).
2
+
3
+ Revision ID: settings0001
4
+ Revises:
5
+ """
6
+
7
+ import sqlalchemy as sa
8
+ from alembic import op
9
+
10
+ revision = "settings0001"
11
+ down_revision = None
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade():
17
+ op.create_table(
18
+ "setting",
19
+ sa.Column("id", sa.Integer(), nullable=False),
20
+ sa.Column("key", sa.String(length=191), nullable=False),
21
+ sa.Column("value", sa.Text(), nullable=True),
22
+ sa.PrimaryKeyConstraint("id"),
23
+ )
24
+ op.create_index(op.f("ix_setting_key"), "setting", ["key"], unique=True)
25
+
26
+
27
+ def downgrade():
28
+ op.drop_index(op.f("ix_setting_key"), table_name="setting")
29
+ op.drop_table("setting")
@@ -0,0 +1,16 @@
1
+ from splent_framework.db import db
2
+
3
+
4
+ class Setting(db.Model):
5
+ """A single runtime, admin-editable key/value setting (the WP-options
6
+ analogue). Features read these via SettingsService / the {{ setting() }}
7
+ template helper, falling back to product config and defaults."""
8
+
9
+ __tablename__ = "setting"
10
+
11
+ id = db.Column(db.Integer, primary_key=True)
12
+ key = db.Column(db.String(191), nullable=False, unique=True, index=True)
13
+ value = db.Column(db.Text, default="")
14
+
15
+ def __repr__(self):
16
+ return f"Setting<{self.key}>"
@@ -0,0 +1,7 @@
1
+ from splent_io.splent_feature_settings.models import Setting
2
+ from splent_framework.repositories.BaseRepository import BaseRepository
3
+
4
+
5
+ class SettingsRepository(BaseRepository):
6
+ def __init__(self):
7
+ super().__init__(Setting)
@@ -0,0 +1,46 @@
1
+ """Generic admin panels for feature settings.
2
+
3
+ Any feature that declares a settings schema (via the framework's
4
+ register_settings) gets an admin panel here automatically — one page per
5
+ feature — that reads/writes through SettingsService. No per-feature panel code.
6
+ """
7
+
8
+ from flask import abort, flash, redirect, render_template, request, url_for
9
+ from flask_login import login_required
10
+
11
+ from splent_io.splent_feature_settings import settings_bp
12
+ from splent_framework.services.service_locator import service_proxy
13
+ from splent_framework.settings.settings_schema import (
14
+ get_config,
15
+ get_schema,
16
+ get_schemas,
17
+ setting_key,
18
+ )
19
+
20
+
21
+ @settings_bp.route("/admin/settings", methods=["GET"])
22
+ @login_required
23
+ def admin_index():
24
+ return render_template("settings/admin/index.html", schemas=get_schemas())
25
+
26
+
27
+ @settings_bp.route("/admin/settings/<feature>", methods=["GET", "POST"])
28
+ @login_required
29
+ def admin_feature(feature):
30
+ schema = get_schema(feature)
31
+ if schema is None:
32
+ abort(404)
33
+ if request.method == "POST":
34
+ values = {}
35
+ for f in schema["fields"]:
36
+ key = setting_key(feature, f["key"])
37
+ if f["type"] == "bool":
38
+ values[key] = "1" if f["key"] in request.form else "0"
39
+ else:
40
+ values[key] = (request.form.get(f["key"]) or "").strip()
41
+ service_proxy("SettingsService").set_many(values)
42
+ flash(f"{schema['title']} settings saved.", "success")
43
+ return redirect(url_for("settings.admin_feature", feature=feature))
44
+ return render_template(
45
+ "settings/admin/feature.html", schema=schema, cfg=get_config(feature)
46
+ )
@@ -0,0 +1,10 @@
1
+ from splent_framework.seeders.BaseSeeder import BaseSeeder
2
+
3
+
4
+ class SplentFeatureSettingsSeeder(BaseSeeder):
5
+ def run(self):
6
+ data = [
7
+ # Create any Model object you want to make seed
8
+ ]
9
+
10
+ self.seed(data)
@@ -0,0 +1,39 @@
1
+ from splent_io.splent_feature_settings.models import Setting
2
+ from splent_io.splent_feature_settings.repositories import SettingsRepository
3
+ from splent_framework.db import db
4
+ from splent_framework.services.BaseService import BaseService
5
+
6
+
7
+ class SettingsService(BaseService):
8
+ """Runtime key/value settings, editable from the admin (WP-options style)."""
9
+
10
+ def __init__(self):
11
+ super().__init__(SettingsRepository())
12
+
13
+ def get(self, key, default=None):
14
+ s = Setting.query.filter_by(key=key).first()
15
+ if s is None or s.value in (None, ""):
16
+ return default
17
+ return s.value
18
+
19
+ def set(self, key, value):
20
+ s = Setting.query.filter_by(key=key).first()
21
+ if s is None:
22
+ s = Setting(key=key, value=value or "")
23
+ db.session.add(s)
24
+ else:
25
+ s.value = value or ""
26
+ db.session.commit()
27
+ return s
28
+
29
+ def set_many(self, mapping):
30
+ for key, value in mapping.items():
31
+ s = Setting.query.filter_by(key=key).first()
32
+ if s is None:
33
+ db.session.add(Setting(key=key, value=value or ""))
34
+ else:
35
+ s.value = value or ""
36
+ db.session.commit()
37
+
38
+ def all_dict(self):
39
+ return {s.key: s.value for s in Setting.query.all()}
@@ -0,0 +1,19 @@
1
+ """
2
+ Signals for splent_feature_settings.
3
+
4
+ Use ``define_signal`` to declare signals this feature emits.
5
+ Use ``connect_signal`` to listen to signals from other features.
6
+
7
+ Examples::
8
+
9
+ # Define a signal (this feature emits it):
10
+ from splent_framework.signals.signal_utils import define_signal
11
+ item_created = define_signal("item-created", "splent_feature_settings")
12
+
13
+ # Connect to a signal from another feature:
14
+ from splent_framework.signals.signal_utils import connect_signal
15
+
16
+ @connect_signal("user-registered", "splent_feature_settings")
17
+ def on_user_registered(sender, user, **kwargs):
18
+ pass
19
+ """
@@ -0,0 +1,58 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}{{ schema.title }} settings{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="d-flex flex-column align-items-start gap-2 mb-4">
7
+ <h1 class="h3 mb-0">{{ schema.title }} <small class="text-muted">settings</small></h1>
8
+ <a href="{{ url_for('settings.admin_index') }}" class="btn btn-sm btn-outline-secondary">All settings</a>
9
+ </div>
10
+
11
+ {% with messages = get_flashed_messages(with_categories=true) %}
12
+ {% for category, message in messages %}
13
+ <div class="alert alert-{{ 'danger' if category == 'danger' else 'success' }} py-2">{{ message }}</div>
14
+ {% endfor %}
15
+ {% endwith %}
16
+
17
+ <form method="POST">
18
+ <div class="card">
19
+ <div class="card-body">
20
+ {% for f in schema.fields %}
21
+ <div class="mb-3">
22
+ {% if f.type == 'bool' %}
23
+ <div class="form-check form-switch">
24
+ <input class="form-check-input" type="checkbox" role="switch"
25
+ name="{{ f.key }}" id="{{ f.key }}" {% if cfg[f.key] %}checked{% endif %}>
26
+ <label class="form-check-label" for="{{ f.key }}">{{ f.label }}</label>
27
+ </div>
28
+ {% elif f.type == 'select' %}
29
+ <label class="form-label" for="{{ f.key }}">{{ f.label }}</label>
30
+ <select class="form-select" name="{{ f.key }}" id="{{ f.key }}">
31
+ {% for value, label in f.options %}
32
+ <option value="{{ value }}" {% if cfg[f.key] == value %}selected{% endif %}>{{ label }}</option>
33
+ {% endfor %}
34
+ </select>
35
+ {% elif f.type == 'color' %}
36
+ <label class="form-label" for="{{ f.key }}">{{ f.label }}</label>
37
+ <input type="color" class="form-control form-control-color"
38
+ name="{{ f.key }}" id="{{ f.key }}" value="{{ cfg[f.key] }}">
39
+ {% elif f.type == 'int' %}
40
+ <label class="form-label" for="{{ f.key }}">{{ f.label }}</label>
41
+ <input type="number" class="form-control"
42
+ name="{{ f.key }}" id="{{ f.key }}" value="{{ cfg[f.key] }}">
43
+ {% else %}
44
+ <label class="form-label" for="{{ f.key }}">{{ f.label }}</label>
45
+ <input type="text" class="form-control"
46
+ name="{{ f.key }}" id="{{ f.key }}" value="{{ cfg[f.key] }}">
47
+ {% endif %}
48
+ {% if f.help %}<small class="text-muted d-block mt-1">{{ f.help }}</small>{% endif %}
49
+ </div>
50
+ {% endfor %}
51
+ </div>
52
+ <div class="card-footer d-flex justify-content-between align-items-center">
53
+ <span class="text-muted small">Changes apply immediately.</span>
54
+ <button class="btn btn-primary" type="submit">Save</button>
55
+ </div>
56
+ </div>
57
+ </form>
58
+ {% endblock %}
@@ -0,0 +1,25 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}Settings{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1 class="h3 mb-2">Settings</h1>
7
+ <p class="text-muted">Configure the behaviour of installed features.</p>
8
+
9
+ {% if not schemas %}
10
+ <p class="text-muted">No configurable features installed.</p>
11
+ {% endif %}
12
+
13
+ <div class="row g-3">
14
+ {% for s in schemas %}
15
+ <div class="col-md-4 col-sm-6">
16
+ <a class="card text-decoration-none h-100" href="{{ url_for('settings.admin_feature', feature=s.feature) }}">
17
+ <div class="card-body d-flex align-items-center gap-2">
18
+ <i class="align-middle" data-feather="{{ s.icon }}"></i>
19
+ <span class="h6 mb-0">{{ s.title }}</span>
20
+ </div>
21
+ </a>
22
+ </div>
23
+ {% endfor %}
24
+ </div>
25
+ {% endblock %}
@@ -0,0 +1,10 @@
1
+ {% extends "base_template.html" %}
2
+
3
+ {% block title %}Settings{% endblock %}
4
+
5
+ {% block content %}
6
+
7
+ {% endblock %}
8
+
9
+ {% block scripts %}
10
+ {% endblock %}