Skip to content

Commit 93744e2

Browse files
committed
Say when a plugin is enabled but not configured, and cap between-post ads
A board owner turned every ad placement off except "between posts", saw nothing, and reasonably concluded the ads were broken. Three separate things were wrong, and each one hid the next: 1. topic:between_posts was declared in the plugin API and never rendered by the server. Fixed in the previous commit. 2. Saving the plugin settings form blanked the ad slot ID, and an enabled plugin with a blank required setting looks identical to a broken one: enabled, no error, renders nothing. The admin panel now says "Not configured — this plugin renders nothing" against the specific setting. SettingSpec gained a `required` flag to make that expressible rather than special-cased. 3. The ads plugin took every gap between posts, so a long thread would have carried an ad break each screenful. It takes only the first now. 108 tests, 0 type errors.
1 parent a904295 commit 93744e2

4 files changed

Lines changed: 79 additions & 1 deletion

File tree

‎apps/server/src/routes/admin.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,14 @@ export function adminRoutes(services: Services) {
376376
${row.last_error
377377
? Alert(row.last_error, { variant: 'destructive', title: 'This plugin reported a problem' })
378378
: ''}
379+
${missingRequired(manifest, registry.config(row.slug))
380+
.map((spec) =>
381+
Alert(
382+
html`<strong>${spec.label}</strong> is empty, so this plugin renders nothing.
383+
${spec.help ?? ''}`,
384+
{ variant: 'warning', title: 'Not configured' },
385+
),
386+
)}
379387
${on && manifest?.settings?.length
380388
? html`<form method="post" action="/admin/plugins/${row.slug}/settings">
381389
${manifest.settings.map((spec) =>
@@ -594,6 +602,25 @@ export function adminRoutes(services: Services) {
594602
return app;
595603
}
596604

605+
/**
606+
* A plugin can be enabled, error-free and still do nothing because a setting it
607+
* cannot work without is blank — which looks identical to a broken plugin from
608+
* the outside. Saying so on the page is the difference between a two-minute fix
609+
* and an afternoon.
610+
*/
611+
function missingRequired(
612+
manifest: { settings?: SettingSpec[] } | null | undefined,
613+
config: Record<string, unknown>,
614+
): SettingSpec[] {
615+
return (manifest?.settings ?? []).filter(
616+
(spec) =>
617+
spec.type === 'string' &&
618+
'required' in spec &&
619+
spec.required === true &&
620+
!String(config[spec.key] ?? spec.default ?? '').trim(),
621+
);
622+
}
623+
597624
interface UserRow {
598625
id: number;
599626
username: string;

‎packages/plugin-api/src/manifest.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
/** A setting a plugin exposes in the admin panel. */
22
export type SettingSpec =
3-
| { key: string; label: string; type: 'string'; default?: string; help?: string; placeholder?: string; secret?: boolean }
3+
| {
4+
key: string;
5+
label: string;
6+
type: 'string';
7+
default?: string;
8+
help?: string;
9+
placeholder?: string;
10+
secret?: boolean;
11+
/** The plugin does nothing without it; the admin panel says so when it is blank. */
12+
required?: boolean;
13+
}
414
| { key: string; label: string; type: 'number'; default?: number; help?: string; min?: number; max?: number }
515
| { key: string; label: string; type: 'boolean'; default?: boolean; help?: string }
616
| { key: string; label: string; type: 'select'; default?: string; help?: string; options: { value: string; label: string }[] }

‎plugins/crawlproof-ads/src/index.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export default definePlugin({
109109
key: 'slotId',
110110
label: 'Ad slot ID',
111111
type: 'string',
112+
required: true,
112113
default: '',
113114
placeholder: 'from crawlproof.com/ads/slots',
114115
help:
@@ -228,6 +229,17 @@ export default definePlugin({
228229
(props) => {
229230
if (ctx.settings.get(placement.key) !== true) return null;
230231

232+
/*
233+
* The board offers every gap between two posts; take only the
234+
* first. A unit in every gap turns a long thread into an ad break
235+
* each screenful, which is the one thing this placement sits a
236+
* single bad decision away from.
237+
*/
238+
if (placement.slot === 'topic:between_posts') {
239+
const gap = (props as { index?: number }).index ?? 0;
240+
if (gap !== 0) return null;
241+
}
242+
231243
const viewer = props.viewer;
232244
if (viewer.isAdmin || viewer.isModerator) {
233245
if (ctx.settings.get('hideForStaff') === true) return null;

‎test/admin.test.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,35 @@ describe('the admin panel', () => {
132132
assert.equal(({} as Record<string, unknown>).polluted, undefined, 'and nothing was polluted');
133133
});
134134

135+
it('says so when an enabled plugin is missing a setting it cannot work without', async () => {
136+
// A blank slot ID looks exactly like a broken plugin from the outside: the
137+
// plugin is enabled, reports no error, and renders nothing. Saying so on the
138+
// page is the difference between a two-minute fix and an afternoon — this is
139+
// precisely how the live board ended up serving no ads.
140+
await db.run("UPDATE plugins SET config = ? WHERE slug = 'crawlproof-ads'", [
141+
JSON.stringify({ slotId: '' }),
142+
]);
143+
await registry.reload();
144+
145+
const body = await (await get('/admin/plugins')).text();
146+
assert.ok(body.includes('Not configured'), 'the panel flags it');
147+
assert.ok(body.includes('renders nothing'));
148+
149+
await db.run("UPDATE plugins SET config = ? WHERE slug = 'crawlproof-ads'", [
150+
JSON.stringify({ slotId: 'slot-abc' }),
151+
]);
152+
await registry.reload();
153+
assert.ok(!(await (await get('/admin/plugins')).text()).includes('Not configured'));
154+
});
155+
156+
it('shows one ad between posts, not one in every gap', async () => {
157+
const fs = await import('node:fs');
158+
const source = fs.readFileSync(new URL('../plugins/crawlproof-ads/src/index.ts', import.meta.url), 'utf8');
159+
// A unit in every gap turns a long thread into an ad break each screenful.
160+
assert.ok(source.includes("placement.slot === 'topic:between_posts'"));
161+
assert.ok(source.includes('gap !== 0'));
162+
});
163+
135164
it('enables and disables a plugin from the panel', async () => {
136165
await post('/admin/plugins/hello-world/toggle', { enabled: '1' });
137166
assert.ok(registry.enabled.has('hello-world'));

0 commit comments

Comments
 (0)