Compare commits

..

9 Commits

Author SHA1 Message Date
giles
5053448ee2 fix: remove existing bp dir before symlinking in Dockerfile
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 50s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 01:08:05 +00:00
giles
7b384bd335 feat: add markets and payments nav buttons, remove features panel
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 40s
- Add cross-app nav buttons for markets and payments (events app)
- Remove page features panel and markets panel from blog admin
  (calendars, markets, and payments are now managed in the events app)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:45:00 +00:00
giles
8fad1ecc7d fix: auto-create PageConfig when enabling page features
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 36s
When toggling market/calendar features or saving SumUp settings,
create a PageConfig row if one doesn't exist yet instead of returning
a 404 error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:34:18 +00:00
giles
af260872d1 fix: build editor in Docker and restore admin panel content
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 49s
The editor.js/editor.css were gitignored and never built during docker
build, causing 404s on the edit page. Add a Node multi-stage build step
to compile them. Also replace "nowt" placeholder in admin templates with
the actual main panel include.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 23:31:35 +00:00
giles
f90d0f2e21 chore: move repo to ~/rose-ash/ and add configurable CI paths
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 44s
REPO_DIR points to /root/rose-ash/blog, COOP_DIR to /root/coop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 22:05:41 +00:00
giles
218849552d chore: update shared_lib submodule to Phase 4
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 39s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 21:47:44 +00:00
giles
6bb520cb0a feat: show page cart badge in blog post header (Phase 4)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s
Fetches page-scoped cart count from /internal/cart/summary?page_slug=
for page posts and displays a cart icon with count in the post header
that links to the page cart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 21:45:41 +00:00
giles
9c047b46b5 chore: update shared_lib submodule to Phase 3
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 44s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:54:12 +00:00
giles
d859be217a feat: per-page SumUp admin UI and update_sumup route (Phase 3)
- Replace SumUp placeholder with real form for merchant code, API key, prefix
- Add PUT /admin/sumup/ route to save per-page SumUp credentials
- Pass sumup_configured/merchant_code/checkout_prefix to template context
- SumUp form only shown when calendar or market feature is enabled

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 20:49:30 +00:00
11 changed files with 189 additions and 23 deletions

View File

@@ -7,7 +7,8 @@ on:
env: env:
REGISTRY: registry.rose-ash.com:5000 REGISTRY: registry.rose-ash.com:5000
IMAGE: blog IMAGE: blog
REPO_DIR: /root/blog REPO_DIR: /root/rose-ash/blog
COOP_DIR: /root/coop
jobs: jobs:
build-and-deploy: build-and-deploy:
@@ -58,7 +59,7 @@ jobs:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
run: | run: |
ssh "root@$DEPLOY_HOST" " ssh "root@$DEPLOY_HOST" "
cd /root/coop cd ${{ env.COOP_DIR }}
source .env source .env
docker stack deploy -c docker-compose.yml coop docker stack deploy -c docker-compose.yml coop
echo 'Waiting for services to update...' echo 'Waiting for services to update...'

View File

@@ -1,5 +1,14 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ---------- Stage 1: Build editor JS/CSS ----------
FROM node:20-slim AS editor-build
WORKDIR /build
COPY shared_lib/editor/package.json shared_lib/editor/package-lock.json* ./
RUN npm ci --ignore-scripts 2>/dev/null || npm install
COPY shared_lib/editor/ ./
RUN NODE_ENV=production node build.mjs
# ---------- Stage 2: Python runtime ----------
FROM python:3.11-slim AS base FROM python:3.11-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
@@ -21,8 +30,11 @@ RUN pip install -r requirements.txt
COPY . . COPY . .
# Copy built editor assets from stage 1
COPY --from=editor-build /static/scripts/editor.js /static/scripts/editor.css shared_lib/static/scripts/
# Link app blueprints into the shared library's namespace # Link app blueprints into the shared library's namespace
RUN ln -s /app/bp /app/shared_lib/suma_browser/app/bp RUN rm -rf /app/shared_lib/suma_browser/app/bp && ln -s /app/bp /app/shared_lib/suma_browser/app/bp
# ---------- Runtime setup ---------- # ---------- Runtime setup ----------
COPY entrypoint.sh /usr/local/bin/entrypoint.sh COPY entrypoint.sh /usr/local/bin/entrypoint.sh

View File

@@ -28,14 +28,25 @@ def register():
# Load features for page admin # Load features for page admin
post = (g.post_data or {}).get("post", {}) post = (g.post_data or {}).get("post", {})
features = {} features = {}
sumup_configured = False
sumup_merchant_code = ""
sumup_checkout_prefix = ""
if post.get("is_page"): if post.get("is_page"):
pc = (await g.s.execute( pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.post_id == post["id"]) sa_select(PageConfig).where(PageConfig.post_id == post["id"])
)).scalar_one_or_none() )).scalar_one_or_none()
if pc: if pc:
features = pc.features or {} features = pc.features or {}
sumup_configured = bool(pc.sumup_api_key)
sumup_merchant_code = pc.sumup_merchant_code or ""
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
ctx = {"features": features} ctx = {
"features": features,
"sumup_configured": sumup_configured,
"sumup_merchant_code": sumup_merchant_code,
"sumup_checkout_prefix": sumup_checkout_prefix,
}
# Determine which template to use based on request type # Determine which template to use based on request type
if not is_htmx_request(): if not is_htmx_request():
@@ -63,12 +74,14 @@ def register():
post_id = post["id"] post_id = post["id"]
# Load PageConfig # Load or create PageConfig
pc = (await g.s.execute( pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.post_id == post_id) sa_select(PageConfig).where(PageConfig.post_id == post_id)
)).scalar_one_or_none() )).scalar_one_or_none()
if pc is None: if pc is None:
return jsonify({"error": "PageConfig not found for this page."}), 404 pc = PageConfig(post_id=post_id, features={})
g.s.add(pc)
await g.s.flush()
# Parse request body # Parse request body
body = await request.get_json() body = await request.get_json()
@@ -104,6 +117,55 @@ def register():
"_types/post/admin/_features_panel.html", "_types/post/admin/_features_panel.html",
features=features, features=features,
post=post, post=post,
sumup_configured=bool(pc.sumup_api_key),
sumup_merchant_code=pc.sumup_merchant_code or "",
sumup_checkout_prefix=pc.sumup_checkout_prefix or "",
)
return await make_response(html)
@bp.put("/admin/sumup/")
@require_admin
async def update_sumup(slug: str):
"""Update PageConfig SumUp credentials for a page."""
from models.page_config import PageConfig
from sqlalchemy import select as sa_select
from quart import jsonify
post = g.post_data.get("post")
if not post or not post.get("is_page"):
return jsonify({"error": "This is not a page."}), 400
post_id = post["id"]
pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.post_id == post_id)
)).scalar_one_or_none()
if pc is None:
pc = PageConfig(post_id=post_id, features={})
g.s.add(pc)
await g.s.flush()
form = await request.form
merchant_code = (form.get("merchant_code") or "").strip()
api_key = (form.get("api_key") or "").strip()
checkout_prefix = (form.get("checkout_prefix") or "").strip()
pc.sumup_merchant_code = merchant_code or None
pc.sumup_checkout_prefix = checkout_prefix or None
# Only update API key if non-empty (allows updating other fields without re-entering key)
if api_key:
pc.sumup_api_key = api_key
await g.s.flush()
features = pc.features or {}
html = await render_template(
"_types/post/admin/_features_panel.html",
features=features,
post=post,
sumup_configured=bool(pc.sumup_api_key),
sumup_merchant_code=pc.sumup_merchant_code or "",
sumup_checkout_prefix=pc.sumup_checkout_prefix or "",
) )
return await make_response(html) return await make_response(html)

View File

@@ -65,6 +65,7 @@ def register():
p_data = getattr(g, "post_data", None) p_data = getattr(g, "post_data", None)
if p_data: if p_data:
from .services.entry_associations import get_associated_entries from .services.entry_associations import get_associated_entries
from shared.internal_api import get as api_get
db_post_id = (g.post_data.get("post") or {}).get("id") # <-- integer db_post_id = (g.post_data.get("post") or {}).get("id") # <-- integer
calendars = ( calendars = (
@@ -86,13 +87,30 @@ def register():
# Fetch associated entries for nav display # Fetch associated entries for nav display
associated_entries = await get_associated_entries(g.s, db_post_id) associated_entries = await get_associated_entries(g.s, db_post_id)
return { ctx = {
**p_data, **p_data,
"base_title": f"{config()['title']} {p_data['post']['title']}", "base_title": f"{config()['title']} {p_data['post']['title']}",
"calendars": calendars, "calendars": calendars,
"markets": markets, "markets": markets,
"associated_entries": associated_entries, "associated_entries": associated_entries,
} }
# Page cart badge: fetch page-scoped cart count for pages
post_dict = p_data.get("post") or {}
if post_dict.get("is_page"):
page_cart = await api_get(
"cart",
f"/internal/cart/summary?page_slug={post_dict['slug']}",
forward_session=True,
)
if page_cart:
ctx["page_cart_count"] = page_cart.get("count", 0) + page_cart.get("calendar_count", 0)
ctx["page_cart_total"] = page_cart.get("total", 0) + page_cart.get("calendar_total", 0)
else:
ctx["page_cart_count"] = 0
ctx["page_cart_total"] = 0
return ctx
else: else:
return {} return {}

View File

@@ -41,9 +41,72 @@
</label> </label>
</form> </form>
{# Phase 3: SumUp connection placeholder #} {# SumUp credentials — shown when calendar or market is enabled #}
{% if features.get('calendar') or features.get('market') %}
<div class="mt-4 pt-4 border-t border-stone-100"> <div class="mt-4 pt-4 border-t border-stone-100">
<h4 class="text-sm font-medium text-stone-500">SumUp Payment</h4> <h4 class="text-sm font-medium text-stone-700">
<p class="text-xs text-stone-400 mt-1">Payment connection coming soon.</p> <i class="fa fa-credit-card text-purple-600 mr-1"></i>
SumUp Payment
</h4>
<p class="text-xs text-stone-400 mt-1 mb-3">
Configure per-page SumUp credentials. Leave blank to use the global merchant account.
</p>
<form
hx-put="{{ url_for('blog.post.admin.update_sumup', slug=post.slug)|host }}"
hx-target="#features-panel"
hx-swap="outerHTML"
class="space-y-3"
>
<div>
<label class="block text-xs font-medium text-stone-600 mb-1">Merchant Code</label>
<input
type="text"
name="merchant_code"
value="{{ sumup_merchant_code }}"
placeholder="e.g. ME4J6100"
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
>
</div>
<div>
<label class="block text-xs font-medium text-stone-600 mb-1">API Key</label>
<input
type="password"
name="api_key"
value=""
placeholder="{{ '••••••••' if sumup_configured else 'sup_sk_...' }}"
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
>
{% if sumup_configured %}
<p class="text-xs text-stone-400 mt-0.5">Key is set. Leave blank to keep current key.</p>
{% endif %}
</div>
<div>
<label class="block text-xs font-medium text-stone-600 mb-1">Checkout Reference Prefix</label>
<input
type="text"
name="checkout_prefix"
value="{{ sumup_checkout_prefix }}"
placeholder="e.g. ROSE-"
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
>
</div>
<button
type="submit"
class="px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500"
>
Save SumUp Settings
</button>
{% if sumup_configured %}
<span class="ml-2 text-xs text-green-600">
<i class="fa fa-check-circle"></i> Connected
</span>
{% endif %}
</form>
</div> </div>
{% endif %}
</div> </div>

View File

@@ -3,14 +3,5 @@
id="main-panel" id="main-panel"
class="flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport" class="flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
> >
{% if post and post.is_page %}
<div class="max-w-lg mx-auto mt-6 px-4 space-y-6">
{% include "_types/post/admin/_features_panel.html" %}
{% if features.get('market') %}
{% include "_types/post/admin/_markets_panel.html" %}
{% endif %}
</div>
{% endif %}
<div class="pb-8"></div> <div class="pb-8"></div>
</section> </section>

View File

@@ -4,6 +4,16 @@
calendars calendars
</a> </a>
</div> </div>
<div class="relative nav-group">
<a href="{{ events_url('/' + post.slug + '/markets/') }}" class="{{styles.nav_button}}">
markets
</a>
</div>
<div class="relative nav-group">
<a href="{{ events_url('/' + post.slug + '/payments/') }}" class="{{styles.nav_button}}">
payments
</a>
</div>
{% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %} {% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
entries entries
{% endcall %} {% endcall %}

View File

@@ -18,5 +18,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
nowt {% include '_types/post/admin/_main_panel.html' %}
{% endblock %} {% endblock %}

View File

@@ -14,5 +14,5 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
nowt {% include '_types/post/admin/_main_panel.html' %}
{% endblock %} {% endblock %}

View File

@@ -13,6 +13,15 @@
</span> </span>
{% endcall %} {% endcall %}
{% call links.desktop_nav() %} {% call links.desktop_nav() %}
{% if page_cart_count is defined and page_cart_count > 0 %}
<a
href="{{ cart_url('/' + post.slug + '/') }}"
class="relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
<span>{{ page_cart_count }}</span>
</a>
{% endif %}
{% include '_types/post/_nav.html' %} {% include '_types/post/_nav.html' %}
{% endcall %} {% endcall %}
{% endcall %} {% endcall %}