openvibers/openvibe.live Commits

Goals: let the streamer edit the current amount by hand

OP OpenVibers committed 26 Aug 2026 · dfb99c48cc · 4 files, +46 −4

For money that arrives outside the site (cash, an external tip, a
miscount): the edit-goal modal gets a "Current amount" field. The value is
clamped to [0, target]; filling the goal by hand completes it exactly like
a donation would but quietly — no goal-reached celebration for
bookkeeping. Open goal widgets get a live goal-update broadcast. Also
makes reopening a goal actually clear reached_at (the code claimed to but
the column was never in the update allow-list), so reopened goals no
longer sit stuck in the celebration window.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017AyeSFu7qdLdR64Zv99QNm

Changes

ddiff --git a/public/js/app.js b/public/js/app.js
iindex a8663b68..3bb8ca36 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -7826,6 +7826,12 @@ function addGoalModal() {
<label>Target (Vibes)</label>
<input type="number" id="modal-goal-target" class="form-input" placeholder="500" min="1" value="${g ? g.target_amount : ''}">
</div>
++ ${g ? `
++ <div class="form-group">
++ <label>Current amount (Vibes)</label>
++ <input type="number" id="modal-goal-current" class="form-input" min="0" value="${Number(g.current_amount) || 0}">
++ <div class="muted" style="font-size:0.78rem;margin-top:4px">Manual correction — e.g. someone sent money outside the site. Setting it to the target completes the goal (without the celebration).</div>
++ </div>` : ''}
<div class="form-group">
<label>Image / Video (optional)</label>
<div id="goal-media-preview">${_goalMediaPreviewHTML()}</div>
@@@ -7871,6 +7877,16 @@ async function saveGoal() {
const target = parseInt(document.getElementById('modal-goal-target').value, 10);
if (!title || !target) return toast('Fill in title and target', 'error');
const body = { title, target_amount: target, image_url: _goalMediaUrl || null, media_type: _goalMediaType || null };
++ // Edit mode only: send the manual progress correction when the streamer changed it.
++ if (window._editingGoal) {
++ const curEl = document.getElementById('modal-goal-current');
++ if (curEl && curEl.value !== '') {
++ const cur = parseInt(curEl.value, 10);
++ if (Number.isFinite(cur) && cur >= 0 && cur !== Number(window._editingGoal.current_amount)) {
++ body.current_amount = cur;
++ }
++ }
++ }
const btn = document.getElementById('goal-save-btn'); if (btn) btn.disabled = true;
try {
if (window._editingGoal) await api(`/funds/goals/${window._editingGoal.id}`, { method: 'PUT', body });
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 663d3f25..599b870b 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -7561,7 +7561,7 @@ function createDonationGoal(userId, { title, target_amount, image_url = null, me
[userId, title, target_amount, image_url, media_type, r ? r.n : 0]);
}
function updateDonationGoal(id, userId, fields) {
-- const allow = ['title', 'target_amount', 'image_url', 'media_type', 'is_active', 'sort_order', 'current_amount'];
++ const allow = ['title', 'target_amount', 'image_url', 'media_type', 'is_active', 'sort_order', 'current_amount', 'reached_at'];
const sets = [], params = [];
for (const k of allow) if (fields[k] !== undefined) { sets.push(`${k} = ?`); params.push(fields[k]); }
if (!sets.length) return null;
ddiff --git a/server/monetization/routes.js b/server/monetization/routes.js
iindex 390a728f..748a1546 100644
---- a/server/monetization/routes.js
++++ b/server/monetization/routes.js
@@@ -218,7 +218,14 @@ router.post('/goals', requireAuth, (req, res) => {
// ── Update a Donation Goal ───────────────────────────────────
router.put('/goals/:id', requireAuth, (req, res) => {
try {
-- openvibeBucks.updateGoal(parseInt(req.params.id, 10), req.user.id, req.body);
++ const g = openvibeBucks.updateGoal(parseInt(req.params.id, 10), req.user.id, req.body);
++ // A manual progress correction should show up on open goal widgets right away,
++ // same as a donation does (a plain goal-update — no celebration).
++ if (req.body.current_amount !== undefined && g) {
++ try {
++ require('../chat/chat-server').broadcastToChannelRoom(req.user.id, null, { type: 'goal-update', goal: publicGoal(g) });
++ } catch { /* live update is best-effort */ }
++ }
res.json({ goals: openvibeBucks.getManageGoals(req.user.id).map(publicGoal) });
} catch (err) {
res.status(400).json({ error: err.message });
ddiff --git a/server/monetization/vibes.js b/server/monetization/vibes.js
iindex 3aa3c994..5761c8d0 100644
---- a/server/monetization/vibes.js
++++ b/server/monetization/vibes.js
@@@ -315,10 +315,29 @@ class Vibes {
if (patch.media_type !== undefined) fields.media_type = ['image', 'video'].includes(patch.media_type) ? patch.media_type : null;
if (patch.is_active !== undefined) {
fields.is_active = patch.is_active ? 1 : 0;
-- // Re-activating a goal clears its reached_at so it isn't stuck in the celebration window.
-- if (patch.is_active) fields.current_amount = Math.min(g.current_amount, g.target_amount - 1 < 0 ? 0 : g.target_amount);
++ // Re-activating a goal clears its reached_at so it isn't stuck in the
++ // celebration window (the column only became updatable with the manual
++ // current-amount editing — before that this intent silently did nothing).
++ if (patch.is_active) {
++ fields.reached_at = null;
++ fields.current_amount = Math.min(g.current_amount, g.target_amount - 1 < 0 ? 0 : g.target_amount);
++ }
}
if (patch.sort_order !== undefined) fields.sort_order = parseInt(patch.sort_order, 10) || 0;
++ // Manual progress correction — for money that arrived outside the site (cash,
++ // an external tip, a miscount). Clamped to [0, target]; filling the goal by
++ // hand completes it exactly like a donation would, but QUIETLY (no goal-reached
++ // celebration — this is bookkeeping, not a live donation moment).
++ if (patch.current_amount !== undefined) {
++ const target = fields.target_amount !== undefined ? fields.target_amount : g.target_amount;
++ const cur = Math.min(Math.max(0, Math.round(Number(patch.current_amount) || 0)), target);
++ fields.current_amount = cur;
++ const activeAfter = fields.is_active !== undefined ? !!fields.is_active : !!g.is_active;
++ if (cur >= target && target > 0 && activeAfter) {
++ fields.is_active = 0;
++ fields.reached_at = new Date().toISOString().slice(0, 19).replace('T', ' ');
++ }
++ }
db.updateDonationGoal(id, userId, fields);
return db.getDonationGoalById(id);
}