Files
PiCloud/examples/cms-poc/frontend/src/routes/admin/posts/+page.svelte
MechaCat02 813bc46640
Some checks failed
CI / Rust — fmt, clippy, test (push) Failing after 39m53s
CI / Dashboard — check (push) Successful in 10m15s
docs(examples): add the CMS proof-of-concept dogfooding artifact
A WordPress-inspired CMS whose entire backend is PiCloud Rhai scripts
deployed with pic apply, plus a SvelteKit frontend. Built to dogfood the
project tool, CLI, and SDK; exercises docs/kv/files/users, docs+queue+cron
+pubsub triggers, the transactional-outbox notification chain, a docs
before-interceptor, set_if CAS, SSE, per-app CORS, env overlays, and a
durable Workflow (validate -> enrich || seo -> publish -> finalize) started
with workflow::start and polled with workflow::run_status, visualized live
with Svelte Flow.

FINDINGS.md / SECURITY.md capture every gap, surprise, and security issue
found (each tagged [PiCloud] vs [CMS]); the platform fixes for the
actionable ones ship in the preceding commit.

Also folds in the read-only `pic` allowlist entries added to
.claude/settings.json during the session (fewer-permission-prompts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:15:29 +02:00

98 lines
3.4 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { api } from '$lib/api';
let posts = $state<any[]>([]);
let editing = $state<any | null>(null);
let msg = $state('');
const blank = () => ({ id: null, title: '', slug: '', body_md: '', excerpt: '', tags: '', status: 'draft', publish_at: '' });
async function load() { posts = (await api.get('/admin/posts')).posts; }
onMount(load);
function newPost() { editing = blank(); }
function edit(p: any) {
editing = { ...p, tags: (p.tags || []).join(', '), publish_at: p.publish_at || '' };
}
async function save(e: Event) {
e.preventDefault();
msg = '';
const body: any = {
title: editing.title,
slug: editing.slug,
body_md: editing.body_md,
excerpt: editing.excerpt,
status: editing.status,
tags: editing.tags.split(',').map((s: string) => s.trim()).filter(Boolean)
};
if (editing.status === 'scheduled' && editing.publish_at) body.publish_at = editing.publish_at;
try {
if (editing.id) await api.put('/admin/posts/' + editing.id, body);
else await api.post('/admin/posts', body);
msg = 'Saved.';
editing = null;
await load();
} catch (err: any) { msg = 'Error: ' + err.message; }
}
async function del(p: any) {
if (!confirm('Delete "' + p.title + '"?')) return;
try { await api.del('/admin/posts/' + p.id); await load(); }
catch (e: any) { msg = 'Error: ' + e.message; }
}
</script>
<h1>Posts</h1>
<p><button class="btn" onclick={newPost}>+ New post</button> {msg}</p>
{#if editing}
<form class="card" onsubmit={save}>
<h3>{editing.id ? 'Edit' : 'New'} post</h3>
<label>Title</label><input bind:value={editing.title} required />
<label>Slug (optional)</label><input bind:value={editing.slug} />
<label>Excerpt</label><input bind:value={editing.excerpt} />
<label>Tags (comma separated)</label><input bind:value={editing.tags} />
<label>Body (markdown)</label><textarea bind:value={editing.body_md} rows="8"></textarea>
<label>Status</label>
<select bind:value={editing.status}>
<option value="draft">draft</option>
<option value="published">published</option>
<option value="scheduled">scheduled</option>
</select>
{#if editing.status === 'scheduled'}
<label>Publish at (ISO, e.g. 2026-07-18T09:00:00Z)</label>
<input bind:value={editing.publish_at} />
{/if}
<p><button class="btn" type="submit">Save</button> <button type="button" onclick={() => (editing = null)}>Cancel</button></p>
</form>
{/if}
<table>
<thead><tr><th>Title</th><th>Status</th><th>Tags</th><th></th></tr></thead>
<tbody>
{#each posts as p}
<tr>
<td>{p.title}</td>
<td><span class={'badge ' + p.status}>{p.status}</span></td>
<td>{(p.tags || []).join(', ')}</td>
<td>
<button onclick={() => edit(p)}>edit</button>
<button onclick={() => del(p)}>del</button>
</td>
</tr>
{/each}
</tbody>
</table>
<style>
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 0.5rem; border-bottom: 1px solid #eee; font-size: 0.9rem; }
.badge { padding: 0.1rem 0.5rem; border-radius: 999px; font-size: 0.75rem; }
.badge.published { background: #dcfce7; color: #166534; }
.badge.draft { background: #f3f4f6; color: #555; }
.badge.scheduled { background: #fef9c3; color: #854d0e; }
td button { margin-right: 0.3rem; }
</style>