Ideen bekommen Bereiche und einen Thread zum Reden
Nach dem Vorbild von windrose.support: Bereiche zum Einsortieren, Sortierung nach Top / Bewegung / Neu, und eine Zahl daneben, wie viel darueber geredet wird. Der Unterschied zu so einem Dienst ist, wo geredet wird. Zu jedem Wunsch macht der Bot jetzt einen Thread unter dem Post auf. Der Kommentar-Zaehler auf der Roadmap zaehlt die Beitraege darin und verlinkt hinein — es gibt also einen Zaehler wie bei einem Ideen-Board, aber ohne ein zweites Kommentarsystem neben Discord zu stellen. Genau das hatte ich beim letzten Mal als das Falsche bezeichnet, und dabei bleibt es. Eingereicht wird jetzt auch ueber einen Aufruf-Post: /wunsch-setup postet ihn, und mit Bereichen wird daraus ein Auswahlmenue — erst wohin, dann das Formular. Ohne Bereiche bleibt es beim einen Knopf. Derselbe Aufbau wie bei den Ticket-Anliegen, bis hin zur Emoji-Pruefung, die ich mir dort schon geschrieben hatte. Das Anlegen selbst ist nach bot/wishes.js gewandert. /wunsch, der Knopf und die Webseite gehen jetzt denselben Weg — vorher hatte die Webseite ihr eigenes Embed zusammengebaut, das dem aus dem Befehl nur aehnlich sah und keinen Thread bekam. "Bewegung" sind die Stimmen der letzten sieben Tage. Die Rangliste allein zementiert alte Wuensche: was einmal oben steht, bleibt oben, egal ob noch jemand hinschaut. Sortiert wird auf dem Server, weil die Zeitstempel der Stimmen im Browser gar nicht ankommen. Bewusst nicht uebernommen: Gegenstimmen. Windrose hat sie, aber bei einer Community dieser Groesse laden sie zum Nachtreten ein und schrecken vom Einreichen ab — und eine Zahl, die aus zwei Richtungen kommt, sagt am Ende weniger als eine, die nur zaehlt, wer etwas will. Geprueft: Migration erneut durchgespielt (das Schema hat drei Spalten dazubekommen), Routen, SQL gegen das Schema, und im Browser alle drei Filterreihen einzeln und kombiniert, der Sortierwechsel, die Thread-Links und die Bereichs-Pflege im Panel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+83
-21
@@ -15,7 +15,8 @@ import {
|
||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||
saveTemplate, listTemplates, deleteTemplate,
|
||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||
getWish, searchWishes, setWishStatus, mergeWish, deleteWish, wishVoters,
|
||||
getWish, searchWishes, setWishStatus, mergeWish, deleteWish, wishVoters, setWishCategory,
|
||||
listWishCategories, createWishCategory, updateWishCategory, deleteWishCategory,
|
||||
logAudit, listAudit, playerHistory, uptimeBuckets, watchdogBuckets, watchdogDays,
|
||||
heartbeatBuckets, heartbeatFirst,
|
||||
freeAlphaKeys, deleteFreeAlphaKey, removePlaytester,
|
||||
@@ -33,6 +34,7 @@ import { tuningStates, setTuning, tuning, TUNING_GROUPS } from '../tuning.js';
|
||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||
import { watchdogState, watchedServices } from '../bot/watchdog.js';
|
||||
import { listRules, setRuleEnabled } from '../bot/automod.js';
|
||||
import { wunschAnlegen } from '../bot/wishes.js';
|
||||
import { heartbeatSerie } from '../bot/heartbeat.js';
|
||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||
import { sanitizeEmbed } from './api-v1.js';
|
||||
@@ -186,8 +188,19 @@ export function registerApiRoutes(app, client) {
|
||||
app.get('/api/wishes', async (request) => {
|
||||
const user = getSessionUser(request);
|
||||
const mine = user ? new Set(myWishVotes(user.id)) : new Set();
|
||||
const sortierung = String(request.query?.sort ?? 'top');
|
||||
// Den Link zum Thread bauen wir hier — die Guild-ID gehört nicht auf
|
||||
// die öffentliche Seite, nur der fertige Verweis
|
||||
const gid = discordGuildId();
|
||||
return {
|
||||
wishes: topWishes(50).map((w) => ({ ...w, voted: mine.has(w.id) })),
|
||||
wishes: topWishes(50, sortierung).map((w) => ({
|
||||
...w,
|
||||
voted: mine.has(w.id),
|
||||
thread_url: gid && w.thread_id
|
||||
? `https://discord.com/channels/${gid}/${w.thread_id}`
|
||||
: null,
|
||||
})),
|
||||
bereiche: listWishCategories(),
|
||||
canVote: Boolean(user),
|
||||
};
|
||||
});
|
||||
@@ -1925,31 +1938,26 @@ ${rssItems}
|
||||
}
|
||||
});
|
||||
|
||||
// Wunsch einreichen (postet wie /wunsch in den Voting-Kanal)
|
||||
// Wunsch einreichen — läuft durch denselben Weg wie /wunsch, damit ein
|
||||
// Wunsch von hier genauso aussieht: Embed, 👍 und Thread zum Reden
|
||||
app.post('/api/wishes', async (request, reply) => {
|
||||
if (await requireMember(request, reply)) return;
|
||||
const user = getSessionUser(request);
|
||||
const idea = String(request.body?.idea ?? '').trim().slice(0, 500);
|
||||
if (idea.length < 5) return reply.code(400).send({ error: 'Idee zu kurz' });
|
||||
const channelId = votingChannelId();
|
||||
if (!channelId) return reply.code(400).send({ error: 'Feature-Voting ist nicht aktiviert' });
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel?.isTextBased()) return reply.code(500).send({ error: 'Voting-Kanal nicht gefunden' });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(brandColor())
|
||||
.setAuthor({ name: user.username, iconURL: user.avatar ?? undefined })
|
||||
.setTitle('💡 Feature-Wunsch')
|
||||
.setDescription(idea)
|
||||
.setURL(`${publicUrl()}/roadmap`)
|
||||
.setFooter({
|
||||
text: `${brandFooter('VOTING')} • 👍 = will ich!`,
|
||||
iconURL: client.user?.displayAvatarURL?.({ size: 64 }),
|
||||
const categoryId = Number(request.body?.category_id) || null;
|
||||
try {
|
||||
const { id } = await wunschAnlegen(client, {
|
||||
idee: idea,
|
||||
autor: user.username,
|
||||
autorId: user.id,
|
||||
avatar: user.avatar ?? null,
|
||||
categoryId,
|
||||
});
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
await message.react('👍').catch(() => {});
|
||||
const id = saveWish(message.id, user.username, idea, user.id);
|
||||
return { ok: true, id };
|
||||
return { ok: true, id };
|
||||
} catch (error) {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Eine Stimme je Person — ob hier geklickt oder als 👍 in Discord, ist
|
||||
@@ -1969,6 +1977,52 @@ ${rssItems}
|
||||
return { treffer: q.length >= 3 ? searchWishes(q) : [] };
|
||||
});
|
||||
|
||||
// Bereiche, in die ein Wunsch faellt. Ohne Eintrag bleibt die Liste eine
|
||||
// Liste — Schubladen lohnen sich erst ab einer gewissen Zahl.
|
||||
app.get('/api/wish-categories', async (request, reply) => {
|
||||
if (requireAnyScope(request, reply)) return;
|
||||
return { bereiche: listWishCategories() };
|
||||
});
|
||||
|
||||
function bereichBody(request, reply) {
|
||||
const name = String(request.body?.name ?? '').trim();
|
||||
if (!name) {
|
||||
reply.code(400).send({ error: 'Name fehlt.' });
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: name.slice(0, 40),
|
||||
emoji: String(request.body?.emoji ?? '').trim().slice(0, 32),
|
||||
sort: Number(request.body?.sort) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
app.post('/api/wish-categories', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'community')) return;
|
||||
const b = bereichBody(request, reply);
|
||||
if (!b) return;
|
||||
createWishCategory(b);
|
||||
logAudit(getSessionUser(request), 'wunsch-bereich angelegt', b.name);
|
||||
return { ok: true, bereiche: listWishCategories() };
|
||||
});
|
||||
|
||||
app.put('/api/wish-categories/:id', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'community')) return;
|
||||
const b = bereichBody(request, reply);
|
||||
if (!b) return;
|
||||
updateWishCategory({ ...b, id: Number(request.params.id) });
|
||||
logAudit(getSessionUser(request), 'wunsch-bereich geaendert', b.name);
|
||||
return { ok: true, bereiche: listWishCategories() };
|
||||
});
|
||||
|
||||
// Wuensche im geloeschten Bereich bleiben stehen, nur ohne Schild
|
||||
app.delete('/api/wish-categories/:id', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'community')) return;
|
||||
const weg = deleteWishCategory(Number(request.params.id));
|
||||
logAudit(getSessionUser(request), 'wunsch-bereich entfernt', String(request.params.id));
|
||||
return { deleted: weg, bereiche: listWishCategories(), wishes: topWishes(50) };
|
||||
});
|
||||
|
||||
// --- Wünsche verwalten (Team) ---
|
||||
|
||||
const STATUS = {
|
||||
@@ -2034,6 +2088,14 @@ ${rssItems}
|
||||
return { ok: true, benachrichtigt, wishes: topWishes(50) };
|
||||
});
|
||||
|
||||
app.put('/api/wishes/:id/category', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'community')) return;
|
||||
const id = Number(request.params.id);
|
||||
if (!getWish(id)) return reply.code(404).send({ error: 'Wunsch nicht gefunden.' });
|
||||
setWishCategory(id, Number(request.body?.category_id) || null);
|
||||
return { ok: true, wishes: topWishes(50) };
|
||||
});
|
||||
|
||||
// Doppler zusammenführen: Stimmen wandern zum Original, wer für beide
|
||||
// gestimmt hat, zählt dort weiterhin einmal
|
||||
app.post('/api/wishes/:id/merge', async (request, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user