diff --git a/bincio/serve/routers/ideas.py b/bincio/serve/routers/ideas.py
index e864e88..9c17e8e 100644
--- a/bincio/serve/routers/ideas.py
+++ b/bincio/serve/routers/ideas.py
@@ -126,6 +126,28 @@ async def toggle_idea_status(
return JSONResponse({"status": idea["status"]})
+@router.post("/api/ideas/{idea_id}/reopen")
+async def reopen_idea(
+ idea_id: str,
+ bincio_session: Optional[str] = Cookie(default=None),
+) -> JSONResponse:
+ user = deps._require_user(bincio_session)
+ if not user.is_admin:
+ raise HTTPException(403, "Forbidden")
+ dd = deps._get_data_dir()
+ path = _ideas_dir(dd) / f"{idea_id}.json"
+ if not path.exists():
+ raise HTTPException(404, "Not found")
+ with open(path, "r+", encoding="utf-8") as f:
+ _fcntl.flock(f, _fcntl.LOCK_EX)
+ idea = json.load(f)
+ idea["status"] = "open"
+ f.seek(0)
+ f.truncate()
+ json.dump(idea, f, ensure_ascii=False, indent=2)
+ return JSONResponse({"status": "open"})
+
+
@router.post("/api/ideas/{idea_id}/decline")
async def decline_idea(
idea_id: str,
diff --git a/site/src/components/IdeasPage.svelte b/site/src/components/IdeasPage.svelte
index 066118e..975eac5 100644
--- a/site/src/components/IdeasPage.svelte
+++ b/site/src/components/IdeasPage.svelte
@@ -184,6 +184,21 @@
return s === 'awaiting' ? 0 : s === 'done' ? 2 : s === 'declined' ? 3 : 1;
}
+ async function reopenIdea(idea: Idea) {
+ const r = await fetch(`/api/ideas/${idea.id}/reopen`, {
+ method: 'POST',
+ credentials: 'include',
+ });
+ if (r.ok) {
+ idea.status = 'open';
+ ideas = [...ideas].sort((a, b) =>
+ statusOrder(a.status) - statusOrder(b.status) ||
+ b.vote_count - a.vote_count ||
+ b.created_at - a.created_at
+ );
+ }
+ }
+
async function declineIdea(idea: Idea) {
const r = await fetch(`/api/ideas/${idea.id}/decline`, {
method: 'POST',
@@ -408,6 +423,14 @@
style="color: {btnColor}; border: 1px solid {borderColor}"
title="Cycle status"
>{nextLabel}
+ {#if awaiting}
+
+ {/if}