feat: implement Pages as Spaces Phase 1
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 45s

- Add PageConfig model with feature flags (calendar, market)
- Auto-create PageConfig on Ghost page sync
- Add create_page() for Ghost /pages/ API endpoint
- Add /new-page/ route for creating pages
- Add ?type=pages blog filter with Posts|Pages tab toggle
- Add list_pages() to DBClient with PageConfig eager loading
- Add PUT /<slug>/admin/features/ route for feature toggles
- Add feature badges (calendar, market) on page cards
- Add features panel to page admin dashboard
- Update shared_lib submodule with PageConfig model

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-10 14:25:34 +00:00
parent 455a7b1078
commit 8b6320619e
13 changed files with 480 additions and 4 deletions

View File

@@ -22,17 +22,91 @@ def register():
@require_admin
async def admin(slug: str):
from suma_browser.app.utils.htmx import is_htmx_request
from models.page_config import PageConfig
from sqlalchemy import select as sa_select
# Load features for page admin
post = (g.post_data or {}).get("post", {})
features = {}
if post.get("is_page"):
pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.post_id == post["id"])
)).scalar_one_or_none()
if pc:
features = pc.features or {}
ctx = {"features": features}
# Determine which template to use based on request type
if not is_htmx_request():
# Normal browser request: full page with layout
html = await render_template("_types/post/admin/index.html")
html = await render_template("_types/post/admin/index.html", **ctx)
else:
# HTMX request: main panel + OOB elements
html = await render_template("_types/post/admin/_oob_elements.html")
html = await render_template("_types/post/admin/_oob_elements.html", **ctx)
return await make_response(html)
@bp.put("/features/")
@require_admin
async def update_features(slug: str):
"""Update PageConfig.features for a page."""
from models.page_config import PageConfig
from models.ghost_content import Post
from sqlalchemy import select as sa_select
from quart import jsonify
import json
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"]
# Load PageConfig
pc = (await g.s.execute(
sa_select(PageConfig).where(PageConfig.post_id == post_id)
)).scalar_one_or_none()
if pc is None:
return jsonify({"error": "PageConfig not found for this page."}), 404
# Parse request body
body = await request.get_json()
if body is None:
# Fall back to form data
form = await request.form
body = {}
for key in ("calendar", "market"):
val = form.get(key)
if val is not None:
body[key] = val in ("true", "1", "on")
if not isinstance(body, dict):
return jsonify({"error": "Expected JSON object with feature flags."}), 400
# Merge features
features = dict(pc.features or {})
for key, val in body.items():
if isinstance(val, bool):
features[key] = val
elif val in ("true", "1", "on"):
features[key] = True
elif val in ("false", "0", "off", None):
features[key] = False
pc.features = features
from sqlalchemy.orm.attributes import flag_modified
flag_modified(pc, "features")
await g.s.flush()
# Return updated features panel
html = await render_template(
"_types/post/admin/_features_panel.html",
features=features,
post=post,
)
return await make_response(html)
@bp.get("/data/")
@require_admin
async def data(slug: str):