Skip to content

Board contributions (#365, #366) and the OpenIPC Club: sign in, send from the board panel, stars on review - #378

Merged
widgetii merged 13 commits into
masterfrom
board-contributions-365
Oct 2, 2026
Merged

widgetii merged 13 commits into
masterfrom
board-contributions-365

Conversation

@widgetii

@widgetii widgetii commented Oct 2, 2026

Copy link
Copy Markdown
Member

Closes #365, closes #366.

Owners sent a boot log and a U-Boot console as GitHub issues because the board panel's "Have this board?" ask opened one. This lands those two, replaces the ask with a form on the page, and adds the OpenIPC Club: optional sign-in to follow what you sent, keep flash dumps private, and collect stars for what is accepted.

#365 and #366 in the catalogue

The OpenIPC Club

  • internal/club (migration 019): members, identities (Telegram, GitHub, email), sessions, logins. Session cookie path /api/v1/club, HttpOnly, Secure, SameSite=Lax: pages stay static and cached, and a visitor who never signs in gets no cookie. Sign-ins are bound to the browser that started them and used once; cross-site POSTs are refused.
    • Telegram: the page shows a code (QR on desktop), Start in the bot finishes it, the page polls. The bot also reports review decisions; /quiet, /loud, /stop. English, Russian, Chinese.
    • GitHub OAuth (read:org): an active member of CLUB_MAINTAINER_ORG (OpenIPC) is a maintainer.
    • Email magic link through the host's exim.
  • internal/reports keeps everything about reports (deploytest's rule): a send for a known board needs no ipctool output and may carry a programmer's flash image; report_submissions (sender, board named) and the append-only report_stars ledger (migration 020), guarded. Stars are written by a review only: +1 per accepted item, +10 per dump the catalogue did not hold, negated if a published report is rejected later. openipc reports publish|reject takes the same path.
  • A published report's text and photos become a contributed unit on its board (boards.ApplyReportUnits); neither list removes the other's units.
  • Pages: /club (sign-in, then "My submissions" and stars), /club/review (maintainers), the send form in the board panel (and the "what's inside" ask), the member's ★ in the navbar. The nightly purge drops expired sessions and sign-ins.
  • nginx: location ^~ /api/v1/club/ in both vhosts; dev's webhook route skips basic auth (Telegram has no staging password; the bot's secret header guards it).

Validated on dev (dev.openipc.org runs 2b2b231)

  • Real Telegram sign-in (@OpenIPCClubBotDev_bot) and real GitHub sign-in (maintainer recognised) by a maintainer; the email sign-in mail scored 10/10 on mail-tester (SPF, DKIM 2048-bit webber2026, DMARC pass).
  • Click-through as a guest and as a maintainer: send from the MS-J10 panel, private dump on /club, review, +10 ★, a duplicate dump earns 0, a published boot log appears on the board.
  • Tests: Go suite, conformance, nginx check-config.sh and --seam; new club, report-unit and contribution tests; frontend 608 tests, lint, typecheck.

Known gap (not fixed here)

Signing in with Telegram in one browser and GitHub in another makes two accounts; an identity already on one account is not moved to another, so there is no merge yet. Linking from a signed-in browser ("Add another way in") works.

Production steps after merge

  1. openipc-deploy prod <sha> -- migrations 019, 020.
  2. /srv/www/.env.go-prod: TELEGRAM_BOT_TOKEN (@OpenIPCClubBot_bot) and GITHUB_OAUTH_* (OpenIPC Club) are already there; add CLUB_SMTP_ADDR=172.18.0.1:25. CLUB_SITE_URL defaults to https://openipc.org.
  3. deploy/push-nginx.sh --apply for the production vhost (dev's was installed by hand from this branch).
  4. openipc-static prod <sha>.

Host and DNS changes already made (not in this diff): openipc.org SPF fixed (it included itself) and now lists 37.27.251.71; DKIM webber2026._domainkey; _dmarc p=none; webber-eu.openipc.org A + SPF; PTR 37.27.251.71 -> webber-eu.openipc.org; exim HELO and DKIM selector on webber-eu.

AI Dev added 10 commits October 2, 2026 10:01
… the repository (#365, #366)

The board panel asks a visitor with the board for a boot log or a U-Boot
console, and the answer arrives as an issue. The catalogue had nowhere to
put it: migration 003's `contributor` source and `contributed_by` were never
written, and an owner report needs ipctool's YAML.

boards/contributions.yml lists each reviewed contribution as a unit of an
existing model, its text under contributions/<unit>/, and the web role
applies it on every start, as it does contents.yml: an unchanged unit is
left alone, a changed one replaced, a removed one removed with its files,
and an entry whose model is not in the catalogue waits for the import that
brings it. The text is searchable and counted like any other unit's, its
printenv becomes rows, and the panel credits the sender with the issue.

The first entry is Anjoy MS-J10 from sansarus: the boot log (#365) and the
U-Boot console (#366) of one board, IMX307 on SSC335 (the kernel's
INFINITY6B0 SSC009A-S01A) with an 8 MB GD25Q64, stock 3.3.0.2.
…stars on review

Owners sent boot logs and consoles as GitHub issues (#365, #366) because the
board panel's "Have this board?" ask opened one. The ask is now a form on
the panel, and signing in -- optional -- makes what one sends one's own.

internal/club holds accounts and sessions: Telegram (the asking browser
shows a code, a QR code on a desktop; Start in the OpenIPC bot finishes the
sign-in, and the bot later reports each review), GitHub (an active member
of CLUB_MAINTAINER_ORG reviews) and an emailed link, each only when
configured. The session cookie's path is /api/v1/club: pages stay static
and cached, and a visitor who never signs in is sent no cookie. A sign-in
is tied to the browser that started it and used once; POSTs from another
site's page are refused.

Everything about reports stays in internal/reports: a send for a known
board needs no ipctool output and may carry a programmer's flash image;
report_submissions records the sender and the board they named; a
member's reports, their private dumps (to them and maintainers only), the
review queue and the stars ledger are its own, guarded like the rest.
Stars are written by a review only: +1 per accepted item, +10 per dump the
catalogue did not hold, and the negative of each if a published report is
rejected afterwards. `openipc reports publish|reject` takes the same path.

A published report's text and photos become a contributed unit on its
board (boards.ApplyReportUnits), searchable and counted, beside the units
contributions.yml lists; neither list removes the other's.

Pages: /club (sign-in, then the member's reports and stars), /club/review
(maintainers), the send form in the board panel, and the member's stars in
the navbar. The nightly purge drops expired sessions and sign-ins.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add board contributions and the OpenIPC Club review workflow

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Publish reviewed MS-J10 owner material in the searchable board catalogue with sender credit.
• Replace GitHub issue prompts with an on-page form for logs, photos, and private dumps.
• Add optional sign-in, member submissions, maintainer review, and stars awarded on acceptance.
Diagram

sequenceDiagram
    actor Owner
    participant Site as Static site
    participant Club as Club API
    participant Identity as Sign-in provider
    participant Reports as Reports service
    participant DB as PostgreSQL
    participant Boards as Board catalogue
    actor Maintainer
    opt Sign in
        Owner->>Site: Choose sign-in
        Site->>Club: Start login
        Club->>Identity: Verify identity
        Identity-->>Club: Confirm identity
        Club->>DB: Store member and session
    end
    Owner->>Site: Send board material
    Site->>Club: Submit report
    Club->>Reports: Store submission
    Reports->>DB: Save report and sender
    Maintainer->>Club: Publish or reject
    Club->>Reports: Record decision
    Reports->>DB: Write review and stars
    Club->>Boards: Refresh published units
    Boards-->>Site: Show searchable material
Loading
High-Level Assessment

The separation is appropriate: Club owns identities and sessions, while the existing reports service retains ownership of submissions, private files, reviews, and stars. Reusing the board importer for both repository-reviewed and report-derived units avoids a second catalogue path; keeping identity lookups behind the API preserves static page caching.

Files changed (50) +5716 / -48

Enhancement (27) +3569 / -38
SiteHeader.astroMount the signed-in member badge +5/-0

Mount the signed-in member badge

• Adds the Club badge to the site navigation as a client-side island.

frontend/apps/site/src/components/SiteHeader.astro

BoardPanel.tsxReplace board issue links with a send form +39/-22

Replace board issue links with a send form

• Shows sender credit on contributed units and opens an inline submission form from the board and inside-device prompts.

frontend/apps/site/src/components/boards/BoardPanel.tsx

Club.tsxBuild Club sign-in and member pages +330/-0

Build Club sign-in and member pages

• Implements provider sign-in, Telegram QR polling, identity linking, bot preferences, submissions, and stars on the static Club page.

frontend/apps/site/src/components/club/Club.tsx

ClubBadge.tsxShow a member's name and stars in navigation +29/-0

Show a member's name and stars in navigation

• Fetches identity only for browsers with a remembered sign-in and responds to in-page sign-in changes.

frontend/apps/site/src/components/club/ClubBadge.tsx

Review.tsxBuild the maintainer review queue +140/-0

Build the maintainer review queue

• Displays submissions and private-file links, supports status tabs, and submits publish or reject decisions with board selections and notes.

frontend/apps/site/src/components/club/Review.tsx

SendForm.tsxAdd inline board-material submission +117/-0

Add inline board-material submission

• Lets guests or members send text, photos, or flash dumps for a named board, with explicit consent required to publish a dump.

frontend/apps/site/src/components/club/SendForm.tsx

parts.tsxShare Club status and star displays +24/-0

Share Club status and star displays

• Defines status styling and reusable star-count presentation for Club islands.

frontend/apps/site/src/components/club/parts.tsx

Club.astroAdd the static Club page shell +20/-0

Add the static Club page shell

• Renders localized page content around the client-side sign-in and member experience.

frontend/apps/site/src/components/pages/Club.astro

ClubReview.astroAdd the static review page shell +19/-0

Add the static review page shell

• Renders the localized maintainer review page without embedding private queue data in the static page.

frontend/apps/site/src/components/pages/ClubReview.astro

model.tsDerive display credit for contributed units +22/-1

Derive display credit for contributed units

• Formats contributor names and reference labels for issues and report receipts shown in board details.

frontend/apps/site/src/lib/boards/model.ts

club.tsAdd typed Club API client +169/-0

Add typed Club API client

• Defines member and review response types, API calls, a remembered-sign-in hint, and a badge update event.

frontend/apps/site/src/lib/club.ts

page-paths.tsRegister Club page paths +5/-0

Register Club page paths

• Adds localized route metadata for member and maintainer pages.

frontend/apps/site/src/lib/page-paths.ts

pages.tsConnect Club paths to page renderers +4/-0

Connect Club paths to page renderers

• Maps the new member and review routes to their Astro components.

frontend/apps/site/src/lib/pages.ts

boards.goApply owner contributions after board imports +26/-2

Apply owner contributions after board imports

• Applies reviewed contributions at import time so entries waiting for a model appear when that model arrives.

service/cmd/openipc/boards.go

club.goWire sign-in providers and report-unit refresh +89/-0

Wire sign-in providers and report-unit refresh

• Constructs the Club API from configured providers, starts the Telegram bot, and refreshes published report units on boards.

service/cmd/openipc/club.go

main.goRegister Club routes and startup maintenance +37/-2

Register Club routes and startup maintenance

• Wires Club handlers and contribution application into the web role, refreshes published units, and purges expired Club records.

service/cmd/openipc/main.go

contributions.goReconcile contributed board units +315/-0

Reconcile contributed board units

• Validates embedded contribution entries and applies changed units, files, and metadata while keeping repository and report-derived units separate.

service/internal/boards/contributions.go

contributions.ymlRecord the reviewed Anjoy MS-J10 contribution +31/-0

Record the reviewed Anjoy MS-J10 contribution

• Credits sansarus and references issues #365 and #366 for the board's console, boot log, and hardware details.

service/internal/boards/contributions.yml

uboot.txtInclude the MS-J10 U-Boot console +147/-0

Include the MS-J10 U-Boot console

• Adds the reviewed startup console and printenv output for the contributed board unit.

service/internal/boards/contributions/anjoy-ms-j10-c365/uboot.txt

api.goImplement Club sessions and protected endpoints +500/-0

Implement Club sessions and protected endpoints

• Adds browser-bound sign-in completion, scoped cookies, cross-site POST checks, submission endpoints, private-file access, and maintainer-only review.

service/internal/club/api.go

email.goAdd emailed sign-in links +200/-0

Add emailed sign-in links

• Sends localized one-use links through configured SMTP, limits requests, and requires TLS for nonlocal relays.

service/internal/club/email.go

github.goAdd GitHub OAuth and maintainer recognition +166/-0

Add GitHub OAuth and maintainer recognition

• Uses browser-bound OAuth state and checks active organization membership to determine review access.

service/internal/club/github.go

members.goManage Club identities, sessions, and expiry +191/-0

Manage Club identities, sessions, and expiry

• Links identities to members, stores hashed session tokens, reads member totals, and removes expired sessions and logins.

service/internal/club/members.go

telegram.goAdd Telegram bot sign-in and notifications +386/-0

Add Telegram bot sign-in and notifications

• Handles secret-validated webhooks, browser-bound Start codes, localized review notifications, and mute or unlink commands.

service/internal/club/telegram.go

club.goOwn Club-facing reports and review rewards +435/-0

Own Club-facing reports and review rewards

• Provides member submissions, review queues, private-file lookup, duplicate-dump checks, star decisions, and published board material.

service/internal/reports/club.go

handler.goAccept board-specific reports without ipctool output +99/-11

Accept board-specific reports without ipctool output

• Allows known-board uploads and programmer flash images, associates signed-in senders, and serves authorized original files privately.

service/internal/reports/handler.go

store.goPersist report submission ownership +24/-0

Persist report submission ownership

• Stores sender and named-board references with reports and adds a board-existence lookup for submissions.

service/internal/reports/store.go

Refactor (1) +7 / -7
reports.goUse shared report decisions from the CLI +7/-7

Use shared report decisions from the CLI

• Routes publish and reject commands through the same decision path as web review, including stars and board refresh.

service/cmd/openipc/reports.go

Tests (5) +1062 / -1
ClubBadge.test.tsxTest badge changes after sign-in and sign-out +22/-0

Test badge changes after sign-in and sign-out

• Checks that an untouched browser makes no identity request and that the badge responds to member changes without reloading.

frontend/apps/site/src/components/club/ClubBadge.test.tsx

SendForm.test.tsxTest board-form submissions and privacy defaults +92/-0

Test board-form submissions and privacy defaults

• Checks board and file fields, default-private dumps, empty submissions, guest behavior, and member detection.

frontend/apps/site/src/components/club/SendForm.test.tsx

boards.test.tsTest contributor-credit formatting +18/-1

Test contributor-credit formatting

• Covers GitHub issue references, report receipts, other URLs, and noncontributor units.

frontend/apps/site/src/lib/boards/boards.test.ts

contributions_test.goTest contribution reconciliation and isolation +291/-0

Test contribution reconciliation and isolation

• Covers validation, attribution, search, ordering, idempotence, replacement, removal, missing models, and separation from report units.

service/internal/boards/contributions_test.go

api_test.goExercise sign-in, submission, review, and access flows +639/-0

Exercise sign-in, submission, review, and access flows

• Tests provider sign-ins, one-use browser binding, private downloads, star awards and reversal, bot commands, purge, and mail transport.

service/internal/club/api_test.go

Documentation (2) +46 / -2
CLAUDE.mdDocument Club and contribution ownership +19/-2

Document Club and contribution ownership

• Explains the new authentication boundary, report responsibilities, catalogue publication path, and cookie-scoping convention.

CLAUDE.md

README.mdDocument Club operation and mail settings +27/-0

Document Club operation and mail settings

• Describes the static-page sign-in model, stars policy, provider configuration, and email relay requirements.

service/README.md

Other (15) +1032 / -0
boards.en.ymlAdd English Club and board credit copy +121/-0

Add English Club and board credit copy

• Adds page metadata and board-interface messages for sign-in, sending, submissions, stars, review, and contributor credit.

data/locales/boards.en.yml

boards.ru.ymlAdd Russian Club and board credit copy +121/-0

Add Russian Club and board credit copy

• Provides Russian page metadata and interface messages for the new member and maintainer workflows.

data/locales/boards.ru.yml

boards.zh.ymlAdd Chinese Club and board credit copy +121/-0

Add Chinese Club and board credit copy

• Provides Chinese page metadata and interface messages for the new member and maintainer workflows.

data/locales/boards.zh.yml

org.openipcProxy production Club requests +21/-0

Proxy production Club requests

• Adds a noncached Club API proxy location with streamed, larger uploads and rate limiting.

deploy/nginx/sites-available/org.openipc

org.openipc.devProxy development Club and Telegram webhook requests +36/-0

Proxy development Club and Telegram webhook requests

• Adds the Club API proxy location and exempts the Telegram webhook from staging basic auth; the application validates its secret header.

deploy/nginx/sites-available/org.openipc.dev

boards.en.jsonAdd English board and Club UI translations +114/-0

Add English board and Club UI translations

• Supplies compiled interface strings for contributor credit, sending, sign-in, member reports, and review.

frontend/apps/site/src/i18n/boards.en.json

boards.ru.jsonAdd Russian board and Club UI translations +114/-0

Add Russian board and Club UI translations

• Supplies compiled Russian interface strings for the new board and Club features.

frontend/apps/site/src/i18n/boards.ru.json

boards.zh.jsonAdd Chinese board and Club UI translations +114/-0

Add Chinese board and Club UI translations

• Supplies compiled Chinese interface strings for the new board and Club features.

frontend/apps/site/src/i18n/boards.zh.json

en.jsonAdd English Club page metadata +10/-0

Add English Club page metadata

• Adds titles and descriptions for the Club and review pages.

frontend/apps/site/src/i18n/en.json

ru.jsonAdd Russian Club page metadata +10/-0

Add Russian Club page metadata

• Adds Russian titles and descriptions for the Club and review pages.

frontend/apps/site/src/i18n/ru.json

zh.jsonAdd Chinese Club page metadata +10/-0

Add Chinese Club page metadata

• Adds Chinese titles and descriptions for the Club and review pages.

frontend/apps/site/src/i18n/zh.json

config.goLoad Club provider and site settings +22/-0

Load Club provider and site settings

• Adds environment configuration for the Club URL, Telegram, GitHub, maintainer membership, and SMTP.

service/internal/config/config.go

019_club.sqlCreate Club member and login tables +71/-0

Create Club member and login tables

• Adds members, provider identities, hashed sessions, and expiring browser-bound sign-in records.

service/internal/db/migrations/019_club.sql

020_report_submissions.sqlTrack report senders and append-only stars +72/-0

Track report senders and append-only stars

• Adds guarded submission ownership and board references plus an award-and-revocation ledger.

service/internal/db/migrations/020_report_submissions.sql

routes.jsonDeclare Club API routes +75/-0

Declare Club API routes

• Adds sign-in, member report, private-file, and maintainer review endpoints to the web role's route manifest.

service/routes.json

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. A sign-in link can expose private uploads ✓ Resolved
Description
finish accepts an email sign-in link without binding it to the browser that requested it or
confirming the account change. If a visitor opens a link requested for someone else's account, their
browser signs in as that account, and subsequent private reports they send become accessible to its
owner.
Code

service/internal/club/api.go[R331-334]

+	if bound != nil && provider != "email" {
+		http.Redirect(w, r, "/club/?signin=elsewhere", http.StatusSeeOther)
+		return
+	}
Evidence
The emailed URL carries the login code alone; finish exempts email from its browser-binding check
and sets a session in the browser opening the link. The report endpoint uses that session's member
ID, while private-file access allows that member to retrieve its reports.

service/internal/club/email.go[143-158]
service/internal/club/api.go[306-360]
service/internal/club/api.go[363-416]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An email finish link can sign an unrelated browser into the link requester's account, causing later private uploads to be attributed to that account.
## Fix Focus Areas
- service/internal/club/api.go[306-360]
- service/internal/club/email.go[143-158]
## Recommended Fix
Require the initiating browser's login secret for automatic sign-in. If cross-browser email links must work, require an explicit account-identifying confirmation before creating a session, particularly when replacing an existing session.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. One WebP photo stops report units from refreshing ✓ Resolved
Description
refreshReportUnits maps every published photo to photo_other, so Importer.files calls
Thumbnail, which can decode only JPEG and PNG. The send form accepts WebP; one such photo makes
apply() return early, so later reports never get units and units of rejected reports are never
removed.
Code

service/cmd/openipc/club.go[66]

+	kinds := map[string]string{"photo": "photo_other", "boot_log": "boot_log", "uboot_env": "uboot_env", "note": "note"}
Evidence
sniff accepts image/webp, and prepare publishes the original photo bytes. text.go imports
only the JPEG and PNG decoders. files() returns the error from Thumbnail, and apply() returns
before its cleanup DELETE.

service/internal/reports/handler.go[454-458]
service/internal/boards/text.go[3-10]
service/internal/boards/import.go[193-198]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A published WebP photo fails `Thumbnail` (only JPEG and PNG decoders are registered). `apply()` then aborts the whole refresh before the stale-unit DELETE runs.
## Fix Focus Areas
- service/cmd/openipc/club.go[66-78]
- service/internal/boards/contributions.go[182-185]
- service/internal/boards/text.go[3-10]
## Recommended Fix
Register a WebP decoder (`golang.org/x/image/webp`), or log and skip a photo that fails to thumbnail instead of returning. Keep going with the other entries so the cleanup step always runs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. A report linked to several boards keeps only one ✓ Resolved
Description
Before each insert, apply() deletes contributor units whose source_ref equals the new unit's,
and refreshReportUnits gives every model of one report the same receipt URL as its evidence.
Inserting the unit for model B therefore deletes model A's, and each refresh swaps them again,
leaving files on disk with no row.
Code

service/internal/boards/contributions.go[R187-188]

+			if _, err := tx.Exec(ctx, `DELETE FROM board_units WHERE source = $1 AND (id = $2 OR source_ref = $3)`,
+				Contributors, c.Unit, u.SourceRef); err != nil {
Evidence
Decide links every model in models. PublishedTexts returns one entry per (report, model).
Every entry's Evidence is ReceiptMark + t.Report, the same for all models of a report.

service/cmd/openipc/club.go[73-74]
service/internal/reports/club.go[322-326]
service/internal/reports/club.go[428-430]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
All units of one report share one `source_ref`, so the DELETE by `source_ref` removes the sibling units just inserted.
## Fix Focus Areas
- service/internal/boards/contributions.go[187-188]
- service/cmd/openipc/club.go[73-74]
## Recommended Fix
For report units, delete by `id` only, or add the model to the evidence URL (for example `&model=<model>`) so each unit's `source_ref` is unique.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Senders cannot read review explanations ✓ Resolved
Description
memberReport fills its Note field from the sender's original reports.note rather than the note
stored with the latest review. When a maintainer enters a reason for rejection, the member response
omits that reason, and the submissions page does not display a review note.
Code

service/internal/reports/club.go[R107-109]

+	err := s.DB.QueryRow(ctx, `
+		SELECT r.received_at, r.note, r.yaml, trim(r.chip_vendor || ' ' || r.chip_model)
+		FROM reports r WHERE r.id = $1`, id).Scan(&m.ReceivedAt, &m.Note, &yaml, &m.Chip)
Evidence
Review writes the supplied note to report_reviews, whereas the new member query reads r.note and
only retrieves status and timestamp from the reviews. The member ledger renders neither a
review-note field nor the existing note field.

service/internal/reports/store.go[246-259]
service/internal/reports/club.go[104-120]
frontend/apps/site/src/components/club/Club.tsx[277-303]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Maintainer review notes are stored but not returned or shown to report senders.
## Fix Focus Areas
- service/internal/reports/club.go[104-120]
- frontend/apps/site/src/components/club/Club.tsx[277-303]
## Recommended Fix
Read the latest review's note separately from the sender's submission note, include it in the member response, and render it with the report's status on the submissions page.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Re-publishing a rejected report earns no stars ✓ Resolved
Description
Decide allows a previously rejected report to be published again, but the ledger’s unique
(report_id, position, kind) key and ON CONFLICT DO NOTHING prevent it from inserting replacement
award rows. After a publish–reject–publish sequence, the report returns to published status while
its revoked stars remain unrestored, and the member is told the report earns no stars.
Code

service/internal/reports/club.go[R353-355]

+				tag, err := tx.Exec(ctx, `
+					INSERT INTO report_stars (member_id, report_id, position, points, kind, reason)
+					VALUES ($1, $2, $3, $4, 'award', $5) ON CONFLICT DO NOTHING`, owner, id, pos, pts, "published by "+by)
Evidence
The schema permits only one ledger row of each kind per report position. Decide checks the
requested decision without excluding a previously rejected status: publishing inserts awards with
ON CONFLICT DO NOTHING, rejection inserts negative entries, and d.Points counts only awards
actually inserted. The latest review still sets the report’s status, so republishing changes its
status without inserting awards that offset the earlier revocation.

service/internal/db/migrations/020_report_submissions.sql[32-32]
service/internal/reports/club.go[304-307]
service/internal/reports/club.go[341-369]
service/internal/db/migrations/020_report_submissions.sql[21-33]
service/internal/reports/store.go[216-242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Publishing a report after rejection restores its published status but does not restore revoked stars: the ledger permits only one award per position, so the second award is dropped.
## Fix Focus Areas
- service/internal/reports/club.go[304-383]
- service/internal/db/migrations/020_report_submissions.sql[21-33]
## Recommended Fix
Define the supported review transitions and make the ledger represent each transition, including republishing after rejection. Add a sequence or cycle column to the ledger’s unique key; calculate awards and revocations transactionally from the prior state, awarding a position on publish when the net sum of its ledger rows is <= 0. Add a publish–reject–publish regression test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Removed maintainers keep their review rights ✓ Resolved
Description
member() treats someone as a maintainer if any identity has maintainer=true, and that flag is
refreshed only when the person signs in with GitHub again. Someone removed from the org keeps access
to private dumps and to publish/reject for the 90-day session, and indefinitely if they also sign in
through Telegram or email.
Code

service/internal/club/members.go[166]

+		m.Maintainer = m.Maintainer || maint
Evidence
identify updates maintainer only on the identity that is signing in. Sessions last `sessionDays
= 90. queue, decide and file trust m.Maintainer` without re-checking.

service/internal/club/members.go[107-116]
service/internal/club/api.go[404-416]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The maintainer flag stored on a GitHub identity is never re-checked, so it outlives org membership.
## Fix Focus Areas
- service/internal/club/members.go[159-175]
- service/internal/club/github.go[132-138]
## Recommended Fix
Store a `checked_at` time with the flag and ignore the flag once it is older than a short TTL. Alternatively, give maintainer sessions a short lifetime.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (3)
7. A shared Telegram link signs the sender in as the victim ✓ Resolved
Description
telegramStart ties the code to the browser that requested it, and tgStart attaches it to
whichever Telegram user taps Start, with no confirmation step. An attacker can send their start link
to a member; once the member taps Start, the attacker's polling browser gets a 90-day session on the
member's account.
Code

service/internal/club/telegram.go[R260-271]

+		member, err = identify(ctx, tx, who, func() string {
+			if valid {
+				return deref(forMember)
+			}
+			return ""
+		}())
+		if err != nil {
+			return err
+		}
+		if valid {
+			_, err = tx.Exec(ctx, `UPDATE club_logins SET member_id = $2 WHERE code_sha256 = $1`, sha(code), member)
+			done = true
Evidence
tgStart sets club_logins.member_id to the identity that tapped Start. poll checks only the
login cookie of the browser that created the code.

service/internal/club/api.go[239-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A Telegram start link created in one browser signs that browser in as whoever taps Start, without asking them.
## Fix Focus Areas
- service/internal/club/telegram.go[244-280]
## Recommended Fix
Have the bot show a confirmation button that includes the requesting device or IP, or a code also shown on the page. Only set `member_id` on the login after the person confirms.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Email user names appear publicly as report credits ✓ Resolved
Description
An email sign-up stores the part of the address before the @ as the member's name, and
PublishedTexts uses club_members.name as the unit's contributed_by. Every accepted report then
shows that part of the address on the public board page, and there is no endpoint to change the
name.
Code

service/internal/club/api.go[R339-340]

+			id, err = identify(ctx, tx, signIn{Provider: "email", Subject: addr, Handle: addr,
+				Name: strings.SplitN(addr, "@", 2)[0]}, deref(forMember))
Evidence
PublishedTexts selects cm.name. refreshReportUnits passes it as By, which is written to
contributed_by and shown by the board panel as "Sent by <name>".

service/internal/reports/club.go[408-413]
service/cmd/openipc/club.go[69-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The part of an email address before the @ becomes a public credit on the board page.
## Fix Focus Areas
- service/internal/club/api.go[339-340]
- service/internal/reports/club.go[406-416]
## Recommended Fix
Default email members to a neutral display name. Let members choose a public name, and use only that for board credits.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Each signed-in page view runs hundreds of queries ✓ Resolved
Description
member() calls StarsOf to calculate pending stars, which calls Mine and loads up to 200
reports through memberReport, costing about six queries per report plus more per backup. For an
account with many submissions, this work runs when loading member details on /me—requested by the
navbar on every page for a signed-in browser—and on file downloads and review actions, not just when
listing submissions.
Code

service/internal/club/members.go[176]

+	m.Stars, m.Pending, err = a.Reports.Store().StarsOf(ctx, id)
Evidence
member() invokes StarsOf while serving member details; StarsOf calls Mine, which permits up
to 200 report IDs and invokes memberReport separately for each. Each memberReport performs
report, status, file, potential, and ledger queries, with additional queries per backup, so loading
an identity and star total can issue hundreds of queries without listing submissions.

service/internal/reports/club.go[82-102]
service/internal/reports/club.go[81-102]
service/internal/reports/club.go[104-181]
service/internal/reports/club.go[214-225]
service/internal/club/members.go[147-177]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Loading member details calculates pending stars by fully loading up to 200 reports through per-report database queries, even when submissions are not being listed.
## Fix Focus Areas
- service/internal/reports/club.go[81-102]
- service/internal/reports/club.go[214-225]
- service/internal/club/members.go[147-177]
## Recommended Fix
Calculate pending stars with a single SQL aggregate or batched query over the member's pending reports instead of calling `Mine` from `StarsOf`. Alternatively, calculate them only on `/club/reports` rather than in `member()`. Keep full report loading for the submissions endpoint.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

10. Two reviews at once can corrupt board units ✓ Resolved
Description
OnReviewed runs refreshReportUnits inside each review request, and the CLI review runs it in
another process; neither takes the lock the web role holds at startup. Two concurrent refreshes race
on os.RemoveAll, file writes and the delete/insert of the same units, which can cause key
conflicts or unit rows whose files were deleted.
Code

service/cmd/openipc/club.go[26]

+		OnReviewed: func(ctx context.Context) { refreshReportUnits(ctx, cfg, log, pool) },
Evidence
decide calls OnReviewed with no locking. apply() removes the unit directory before its
transaction starts.

service/internal/club/api.go[470-472]
service/internal/boards/contributions.go[179-181]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Concurrent unit refreshes race on the same directories and rows.
## Fix Focus Areas
- service/cmd/openipc/club.go[60-89]
## Recommended Fix
Take a Postgres advisory lock (`pg_advisory_lock`) around `refreshReportUnits`, shared with the startup apply and the CLI review.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread service/internal/club/api.go Outdated
Comment thread service/internal/reports/club.go
Comment thread service/cmd/openipc/club.go
Comment thread service/internal/boards/contributions.go Outdated
Comment thread service/internal/reports/club.go Outdated
Comment thread service/internal/club/members.go
Comment thread service/internal/club/telegram.go Outdated
Comment thread service/internal/club/api.go Outdated
Comment thread service/internal/club/members.go Outdated
Comment thread service/cmd/openipc/club.go
AI Dev added 3 commits October 2, 2026 12:51
- A sign-in link opened in a browser other than the one that asked for it
  (an emailed link, the bot's link after an expired code) signs nothing in
  by itself: /club names the account and waits for a click. Someone could
  otherwise ask for a link to their own address, get it opened elsewhere,
  and collect what that person sends next.
- Telegram's Start only claims a sign-in; the bot asks "Sign in as @you?",
  naming the address and time it was asked from, and the browser is signed
  in on Yes from the same Telegram account. A start link sent by someone
  else no longer hands them a session.
- A GitHub sign-in proves membership of the maintainers' organisation for
  seven days; after that, reviewing waits for the next GitHub sign-in.
- An email sign-up is named "OpenIPC member", never the address, and a
  member can change their name on /club; the boards' credits follow.
- The stars ledger is a net per part of a report: publishing brings each
  part to what it earns, rejecting to zero, so a report published again
  after a rejection earns its stars back (migration 021).
- The sender reads the reviewer's note with the decision.
- /me sums the ledger instead of reading every report; pending stars come
  with the submissions list.
- Report units: WebP photos are thumbnailed (golang.org/x/image/webp), a
  unit whose files cannot be written stays as it was and costs only its own
  report, a report on several boards is a unit on each, and the refresh runs
  under one advisory lock across processes.
@widgetii

widgetii commented Oct 2, 2026

Copy link
Copy Markdown
Member Author

All ten findings from the review are fixed (3e3f7d2, 157c980, 87fca20), each with a test, and validated on dev:

  1. Sign-in link opened elsewhere -- a link opened in a browser other than the one that asked for it signs nothing in by itself; /club shows whose account it is (from the server, GET /api/v1/club/finish/who) and signs in on a click (POST /api/v1/club/finish). Opened in the asking browser, it signs in at once.
  2. WebP photo stops the refresh -- WebP is decoded (golang.org/x/image/webp); every file of a unit is read and decoded before anything is touched, and a unit that cannot be written costs only its own report and stays as it was.
  3. Report on several boards -- each board's unit has its own reference (…?id=r-…&board=<model>).
  4. Review note -- the sender reads the reviewer's note with the decision on /club.
  5. Re-publishing earns nothing -- the ledger is a net per part of a report (migration 021 drops the unique key): publishing brings each part to what it earns, rejecting to zero. A dump is a duplicate only of a published copy received before it, so its first sender keeps it.
  6. Removed maintainers -- a GitHub sign-in proves membership of the maintainers' organisation for seven days.
  7. Shared Telegram link -- Start only claims the sign-in; the bot asks "Sign in as @you?" naming the asking address and time, and only the account that tapped Start can answer Yes.
  8. Email local part as public credit -- email sign-ups are "OpenIPC member"; members rename themselves on /club (an address is refused as a name).
  9. Hundreds of queries per page -- /me is one sum over the ledger (33 ms on dev); pending stars come with the submissions list.
  10. Concurrent refreshes -- one advisory lock across processes, with an in-process queue in front of it so waiters cannot starve a small pool (a test with a two-connection pool deadlocked before the queue was added).

@widgetii
widgetii merged commit 2293f1f into master Oct 2, 2026
4 checks passed
@widgetii
widgetii deleted the board-contributions-365 branch October 2, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Плата MS-J10: новые материалы Плата MS-J10: новые материалы

1 participant