feat: layered authorization and the participant-facing redesign #3

Open
cola wants to merge 35 commits from feat/layered-authorization into master
Owner

Summary

This branch grew well past its first commit. It now carries the layered authorization model, the participant-facing redesign built on it, the course and webinar work that preceded that, and a set of rules and fixes found while walking the product as each persona.

Authorization and reporting

  • Replace mutually exclusive account roles with layered actor, organization, capability, and resource authorization (ADR 016); scoped OPD reporting, the JP approval workflow, and versioned certificate templates.
  • Penyelenggara becomes a first-class organization entity with a public page; the public can join a training with an access code, and an entry says whether it is for ASN or open to everyone.

Course, material, and webinar

  • A pelatihan gets the console its webinar already had; curriculum editor, quizzes written beside the material they check, articles and PDFs authored in SINAU, mid-video questions, a post-test under its own clock, and a material page of its own.
  • Webinar console with sessions, early finish, automatic certificates, and one public page.
  • Public glosarium with a rich-text register and pictures, and terms explained where the reader meets them.

Redesign (20 Sep)

  • Dedicated dashboards per role with generative covers, a 4-column catalogue, certificate list/detail/verification pages, "Pelatihan saya" as cards, the admin enrollment page with tabs and an approval queue, and an admin participant profile — with the backend endpoints they need.

Rules and fixes found while walking the product (21 Sep)

  • A training's material stays closed until start_date, server and client: the outline locks every item, the material endpoints refuse a participant, article bodies are withheld from the list, and the course page says when instead of what is next. Documented in docs/training-lifecycle.md.
  • "Pelatihan saya" shows applications awaiting approval and rejected ones; an upcoming webinar or a not-yet-started training is an appointment with a date, not a progress bar, on the list and on the dashboard's "Jadwal saya" (which was a hard-coded empty state).
  • Draft and archived trainings are told apart from published ones on the admin card; the doubled org tag on fallback covers is gone; the create/edit intros name the page that actually exists.
  • Each enrollment row's actions sit behind one menu (including cancelling, an endpoint nothing called); participant names link to the profile, whose Pelatihan tab now reads real data.
  • Participant pages drop a blanket link rule that fought the global style.

Developer experience

  • Demo data seeded on first start, a fresh-database reset from env alone, goose markers restored on migrations 052–053, type coverage restored in the web typecheck, and /pr and /ship skills.

Validation

  • go build ./... && go vet ./... && go test ./... inside nix develop — green (21 Sep).
  • web: tsc --noEmit, eslint, and vitest run (33 files, 171 tests) — green.
  • Browser walk-through as admin, instructor, and the three demo participants on the dev server; API probe confirms a not-yet-started training answers every outline item locked, empty article bodies, and 403 on document and video files.

Commits (116)

Full list, newest first
4547d19 fix: read a participant's enrolments from the list that exists
5e8a765 feat: put each enrollment row's actions behind one menu
a16cab9 feat: keep a training's material closed until its start date
7a8adad feat: show a participant what is pending, upcoming, and rejected
3368603 fix: add the org tag only over an uploaded cover
999e342 fix: reword the create and edit training intros
f05a6b8 feat: mark a draft or archived training on its card
5c32a6c feat: give the demo trainings their material
4e5d787 feat: let a deployment start over with a fresh database from its env alone
17b5c18 feat: seed a development deployment with demo data on first start
cc7d61b fix: give migrations 052 and 053 the goose markers they were missing
0945932 fix: restore type coverage in web typecheck and refresh stale fixtures
38ef427 feat: add pr and ship skills for workflow automation
433ea51 feat: redesign participant profile and add admin detail page
9fb99e2 feat: add backend endpoints for frontend redesign
6e5f09d feat: redesign admin enrollment page with tabs, summary, and approval queue
0449a4c feat: apply visual upgrades to admin catalog, enrollment, and certificate views
9ba5207 fix: prevent mobile overflow on dashboard bottom cards
646a040 fix: align schedule empty state left and shrink calendar icon on mobile
cef753b fix: prevent mobile overflow on catalog and dashboard
ddc2af9 fix: hide Ber-JP chip on mobile catalog to prevent overflow
a368167 fix: show mobile search row and info line on catalog
19bd6ee fix: use CatalogFacet popover for desktop catalog filters
f6d403c feat: redesign training catalog with 4-column grid and enrollment status
e23e1ac feat: add rich empty state for certificate list page
2b00b47 feat: add rich landing page for certificate verification
e1aa187 fix: shrink mobile certificate thumbnail and JP pill for narrow viewports
4dacad1 fix: remove design-reference variant banners from verification page
39ed263 feat: redesign certificate list, detail, and public verification pages
0e8f0f6 fix: widen pelatihan-saya responsive breakpoint to 1100px
cf7d78f feat: redesign enrollment list as card-based "Pelatihan saya" for participants
96e6d83 feat: add dedicated dashboards per role with generative covers
19d2c0a feat: clarify city dashboard participant scope
39715a8 feat: add BKPSDM city dashboard
c175df3 feat: add layered organization authorization
d2e3f1a docs: tell the run recipe about the settings encryption key
7f508d5 chore: keep the scraped BKN survey dump out of the repo
b41ee72 feat: let the public join a training with an access code instead of an account
99adcb8 feat: let an entry say whether it is addressed to ASN or open to the public
b9121c8 docs: record what a live BKN sharing session showed
8a68bff feat: let admins configure outgoing mail and require email verification at sign-up
e4a2447 docs: correct what the README promises someone setting this up
b0f0c48 docs: let the ADRs say what the schema actually does now
3279810 docs: catch the frontend spec up with the pages that were built
fbd8afe feat: lay the course page out the way the mockup reads it
49d8cf0 feat: ask a question in the middle of the video, not only after it
f93fe10 fix: stop a participant reaching the authoring pages
5e0dfe2 feat: explain a term where the reader met it
6c11c75 feat: follow a question back to the minute or the page it was answered at
b6fd9d7 chore: keep verification screenshots out of the repository
a38a490 fix: point the public prospectus at the endpoint that exists
2622888 fix: sit the post-test with the navigation out of the way
bb18b9d fix: keep the material page inside the order the server enforces
a8c4e17 fix: pick a quiz up where it was left, and score it from the marks
a82935c fix: read a PDF without the page crashing under it
658da94 fix: fit the course, the syllabus and the reader on a phone
278b329 feat: read a course, a material and a post-test on a phone
715f0f5 feat: sit the post-test under its own clock, with the shell out of the way
5fa83e0 feat: read an article and a PDF the way each is actually read
72b127a feat: let a material's quiz be sat one question at a time, and answer back
902b609 feat: give the material a page of its own
20c47da feat: give the course page one answer to where a participant is
63c4f2f refactor: make the participant's step list something both kinds can use
cfa06d8 refactor: move the training's page to /pelatihan/:id, and the player to its own URL
f920414 fix: ask the sequence gate about something it can see
0eae064 fix: draw an article and a PDF as the different things they are
8b53a0e fix: decide the whole material order before moving any of it
672e19d fix: let the type picker change the type, now that all three editors exist
a582cb0 feat: offer to link the glosarium terms an article already mentions
1d042b4 feat: author a PDF and an article where the rest of the material is authored
82b182a feat: write a quiz one question at a time, beside the material it checks
d6cfc5d feat: give the curriculum an editor of its own, saving as it is typed
ebda8dc feat: give a pelatihan the console its webinar already had
2ec2a8b feat: let material carry its own place, and a quiz belong to what it checks
1129903 feat: settle when a webinar's questionnaire opens, and give the participant a phone
f10de00 feat: give a webinar one public page instead of two, after the mockup
c034a36 fix: send only the fields a session may be written with, and lay its card out
15726ac feat: wear the Ruang Sinau logo, and call the product by its name
9d15c4e fix: refuse to delete a session that has already begun, and let one finish early
53105d9 fix: offset the outcome rows and their button from one origin
b7a8930 fix: line the outcome rows up with the button beneath them
341c8c0 fix: let the console delete a session, and say when one has no link
b5b9223 fix: require a link on every session, and show where a dragged row lands
46bdb4e fix: lay the form's fields out the way the mockup reads them
6323f44 fix: keep the asterisk next to the label it marks
882e6e0 fix: close the back door, show the cover, and let a session be deleted
d325540 feat: rebuild the entry form in sections, and derive the window from the sessions
9ecf77e fix: edit a session where a session lives
8c34ae8 fix: make the console's signals mean one thing each
a8b710e feat: group the management sidebar
266d7cd feat: issue certificates automatically, and let a session finish early
879a28c fix: link the certificate from the webinar rail, and stop promising it issues itself
93e9d7b fix: give an enrolled webinar attendee their own rail, and evaluate after
b07f725 fix: drop the locked questionnaire card, and say tanya jawab
cd38203 fix: restore the preview banner's styles
72985af fix: drop bulk import and list the roster instead
2b325f8 fix: lay the open question out as a form, not a pile
d375d18 feat: collapse the questionnaire into one row per question
56b91fc fix: state Pendaftaran and Pelaksanaan even when they are unset
39d5aff fix: give the console its breadcrumb and the gutter every other page has
222daa5 feat: give the webinar console its own page, after the mockup
1df00f0 feat: give a webinar's organiser a console instead of a scroll
604a16a docs: say why the webinar's two windows are opposite, and show the dates
eb594aa feat: sweep pictures no entry ever came to reference
021df2e feat: let a training's reading material be an article written in SINAU
ac9f47c feat: put pictures in glosarium explanations
c05297a feat: write glosarium explanations in a rich text editor
ff9b764 feat: make the glosarium a register rather than a list of definitions
dd575da feat: open a public glosarium of the istilah peserta meet
2549dcf fix: align participant typography rhythm
afdf250 fix: align catalogue cards with design system
1fad36a feat: open training prospectus and refine catalogue
798c2b0 fix: restore a passing make check
f6e67ac feat: let the organization page recognize a signed-in visitor
cae194d feat: make penyelenggara a first-class organization entity
90ddf7f docs: sync documentation with codebase after recent feature additions

🤖 Generated with Claude Code

## Summary This branch grew well past its first commit. It now carries the layered authorization model, the participant-facing redesign built on it, the course and webinar work that preceded that, and a set of rules and fixes found while walking the product as each persona. **Authorization and reporting** - Replace mutually exclusive account roles with layered actor, organization, capability, and resource authorization (ADR 016); scoped OPD reporting, the JP approval workflow, and versioned certificate templates. - Penyelenggara becomes a first-class organization entity with a public page; the public can join a training with an access code, and an entry says whether it is for ASN or open to everyone. **Course, material, and webinar** - A pelatihan gets the console its webinar already had; curriculum editor, quizzes written beside the material they check, articles and PDFs authored in SINAU, mid-video questions, a post-test under its own clock, and a material page of its own. - Webinar console with sessions, early finish, automatic certificates, and one public page. - Public glosarium with a rich-text register and pictures, and terms explained where the reader meets them. **Redesign (20 Sep)** - Dedicated dashboards per role with generative covers, a 4-column catalogue, certificate list/detail/verification pages, "Pelatihan saya" as cards, the admin enrollment page with tabs and an approval queue, and an admin participant profile — with the backend endpoints they need. **Rules and fixes found while walking the product (21 Sep)** - A training's material stays closed until `start_date`, server and client: the outline locks every item, the material endpoints refuse a participant, article bodies are withheld from the list, and the course page says *when* instead of *what is next*. Documented in `docs/training-lifecycle.md`. - "Pelatihan saya" shows applications awaiting approval and rejected ones; an upcoming webinar or a not-yet-started training is an appointment with a date, not a progress bar, on the list and on the dashboard's "Jadwal saya" (which was a hard-coded empty state). - Draft and archived trainings are told apart from published ones on the admin card; the doubled org tag on fallback covers is gone; the create/edit intros name the page that actually exists. - Each enrollment row's actions sit behind one menu (including cancelling, an endpoint nothing called); participant names link to the profile, whose Pelatihan tab now reads real data. - Participant pages drop a blanket link rule that fought the global style. **Developer experience** - Demo data seeded on first start, a fresh-database reset from env alone, goose markers restored on migrations 052–053, type coverage restored in the web typecheck, and `/pr` and `/ship` skills. ## Validation - `go build ./... && go vet ./... && go test ./...` inside `nix develop` — green (21 Sep). - `web`: `tsc --noEmit`, `eslint`, and `vitest run` (33 files, 171 tests) — green. - Browser walk-through as admin, instructor, and the three demo participants on the dev server; API probe confirms a not-yet-started training answers every outline item locked, empty article bodies, and 403 on document and video files. ## Commits (116) <details> <summary>Full list, newest first</summary> ``` 4547d19 fix: read a participant's enrolments from the list that exists 5e8a765 feat: put each enrollment row's actions behind one menu a16cab9 feat: keep a training's material closed until its start date 7a8adad feat: show a participant what is pending, upcoming, and rejected 3368603 fix: add the org tag only over an uploaded cover 999e342 fix: reword the create and edit training intros f05a6b8 feat: mark a draft or archived training on its card 5c32a6c feat: give the demo trainings their material 4e5d787 feat: let a deployment start over with a fresh database from its env alone 17b5c18 feat: seed a development deployment with demo data on first start cc7d61b fix: give migrations 052 and 053 the goose markers they were missing 0945932 fix: restore type coverage in web typecheck and refresh stale fixtures 38ef427 feat: add pr and ship skills for workflow automation 433ea51 feat: redesign participant profile and add admin detail page 9fb99e2 feat: add backend endpoints for frontend redesign 6e5f09d feat: redesign admin enrollment page with tabs, summary, and approval queue 0449a4c feat: apply visual upgrades to admin catalog, enrollment, and certificate views 9ba5207 fix: prevent mobile overflow on dashboard bottom cards 646a040 fix: align schedule empty state left and shrink calendar icon on mobile cef753b fix: prevent mobile overflow on catalog and dashboard ddc2af9 fix: hide Ber-JP chip on mobile catalog to prevent overflow a368167 fix: show mobile search row and info line on catalog 19bd6ee fix: use CatalogFacet popover for desktop catalog filters f6d403c feat: redesign training catalog with 4-column grid and enrollment status e23e1ac feat: add rich empty state for certificate list page 2b00b47 feat: add rich landing page for certificate verification e1aa187 fix: shrink mobile certificate thumbnail and JP pill for narrow viewports 4dacad1 fix: remove design-reference variant banners from verification page 39ed263 feat: redesign certificate list, detail, and public verification pages 0e8f0f6 fix: widen pelatihan-saya responsive breakpoint to 1100px cf7d78f feat: redesign enrollment list as card-based "Pelatihan saya" for participants 96e6d83 feat: add dedicated dashboards per role with generative covers 19d2c0a feat: clarify city dashboard participant scope 39715a8 feat: add BKPSDM city dashboard c175df3 feat: add layered organization authorization d2e3f1a docs: tell the run recipe about the settings encryption key 7f508d5 chore: keep the scraped BKN survey dump out of the repo b41ee72 feat: let the public join a training with an access code instead of an account 99adcb8 feat: let an entry say whether it is addressed to ASN or open to the public b9121c8 docs: record what a live BKN sharing session showed 8a68bff feat: let admins configure outgoing mail and require email verification at sign-up e4a2447 docs: correct what the README promises someone setting this up b0f0c48 docs: let the ADRs say what the schema actually does now 3279810 docs: catch the frontend spec up with the pages that were built fbd8afe feat: lay the course page out the way the mockup reads it 49d8cf0 feat: ask a question in the middle of the video, not only after it f93fe10 fix: stop a participant reaching the authoring pages 5e0dfe2 feat: explain a term where the reader met it 6c11c75 feat: follow a question back to the minute or the page it was answered at b6fd9d7 chore: keep verification screenshots out of the repository a38a490 fix: point the public prospectus at the endpoint that exists 2622888 fix: sit the post-test with the navigation out of the way bb18b9d fix: keep the material page inside the order the server enforces a8c4e17 fix: pick a quiz up where it was left, and score it from the marks a82935c fix: read a PDF without the page crashing under it 658da94 fix: fit the course, the syllabus and the reader on a phone 278b329 feat: read a course, a material and a post-test on a phone 715f0f5 feat: sit the post-test under its own clock, with the shell out of the way 5fa83e0 feat: read an article and a PDF the way each is actually read 72b127a feat: let a material's quiz be sat one question at a time, and answer back 902b609 feat: give the material a page of its own 20c47da feat: give the course page one answer to where a participant is 63c4f2f refactor: make the participant's step list something both kinds can use cfa06d8 refactor: move the training's page to /pelatihan/:id, and the player to its own URL f920414 fix: ask the sequence gate about something it can see 0eae064 fix: draw an article and a PDF as the different things they are 8b53a0e fix: decide the whole material order before moving any of it 672e19d fix: let the type picker change the type, now that all three editors exist a582cb0 feat: offer to link the glosarium terms an article already mentions 1d042b4 feat: author a PDF and an article where the rest of the material is authored 82b182a feat: write a quiz one question at a time, beside the material it checks d6cfc5d feat: give the curriculum an editor of its own, saving as it is typed ebda8dc feat: give a pelatihan the console its webinar already had 2ec2a8b feat: let material carry its own place, and a quiz belong to what it checks 1129903 feat: settle when a webinar's questionnaire opens, and give the participant a phone f10de00 feat: give a webinar one public page instead of two, after the mockup c034a36 fix: send only the fields a session may be written with, and lay its card out 15726ac feat: wear the Ruang Sinau logo, and call the product by its name 9d15c4e fix: refuse to delete a session that has already begun, and let one finish early 53105d9 fix: offset the outcome rows and their button from one origin b7a8930 fix: line the outcome rows up with the button beneath them 341c8c0 fix: let the console delete a session, and say when one has no link b5b9223 fix: require a link on every session, and show where a dragged row lands 46bdb4e fix: lay the form's fields out the way the mockup reads them 6323f44 fix: keep the asterisk next to the label it marks 882e6e0 fix: close the back door, show the cover, and let a session be deleted d325540 feat: rebuild the entry form in sections, and derive the window from the sessions 9ecf77e fix: edit a session where a session lives 8c34ae8 fix: make the console's signals mean one thing each a8b710e feat: group the management sidebar 266d7cd feat: issue certificates automatically, and let a session finish early 879a28c fix: link the certificate from the webinar rail, and stop promising it issues itself 93e9d7b fix: give an enrolled webinar attendee their own rail, and evaluate after b07f725 fix: drop the locked questionnaire card, and say tanya jawab cd38203 fix: restore the preview banner's styles 72985af fix: drop bulk import and list the roster instead 2b325f8 fix: lay the open question out as a form, not a pile d375d18 feat: collapse the questionnaire into one row per question 56b91fc fix: state Pendaftaran and Pelaksanaan even when they are unset 39d5aff fix: give the console its breadcrumb and the gutter every other page has 222daa5 feat: give the webinar console its own page, after the mockup 1df00f0 feat: give a webinar's organiser a console instead of a scroll 604a16a docs: say why the webinar's two windows are opposite, and show the dates eb594aa feat: sweep pictures no entry ever came to reference 021df2e feat: let a training's reading material be an article written in SINAU ac9f47c feat: put pictures in glosarium explanations c05297a feat: write glosarium explanations in a rich text editor ff9b764 feat: make the glosarium a register rather than a list of definitions dd575da feat: open a public glosarium of the istilah peserta meet 2549dcf fix: align participant typography rhythm afdf250 fix: align catalogue cards with design system 1fad36a feat: open training prospectus and refine catalogue 798c2b0 fix: restore a passing make check f6e67ac feat: let the organization page recognize a signed-in visitor cae194d feat: make penyelenggara a first-class organization entity 90ddf7f docs: sync documentation with codebase after recent feature additions ``` </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: add layered organization authorization
All checks were successful
check / check (pull_request) Successful in 3m27s
c175df3107
feat: add BKPSDM city dashboard
All checks were successful
check / check (pull_request) Successful in 3m30s
39715a8b4e
cola added 20 commits 2026-09-21 05:33:32 +07:00
Replace the generic ManagementDashboard with role-specific dashboards
that follow the mockup designs for each workspace:

- Admin OPD: action queue, org-scoped metrics, training table, registrations
- Pengajar: greeting, assigned trainings, placeholder webinar/grading cards
- Pimpinan OPD: read-only metrics, org training list, SIMPEG reference
- Peserta ASN: continue-learning hero with generative cover, JP donut ring,
  schedule/certificates/recommendations, "Pelatihan baru bulan ini" strip

Improve the existing city dashboard (PlatformDashboard):
- Empty queue card faded with dashed border
- Inactive OPD rows dimmed in ranking table
- "tanpa JP" badge contrast improved
- JP average hidden when sample too small

Add CoverFallback, an SVG component that generates deterministic training
covers from the org name (palette) and training id (geometric pattern),
used across the participant dashboard cards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the data table with a learning-focused card layout when the
actor is a participant:

- Header with summary stats and filter tabs (Semua / Sedang diikuti / Selesai)
- Active enrollments as cards with generative cover, deadline/status
  pills, progress bar, and contextual CTA (Mulai belajar vs Lanjutkan)
- Completed section with JP pill, Lihat materi and Sertifikat buttons
- Empty state with catalog CTA for new participants
- Mobile responsive: vertical cards, pill tabs, full-width buttons

Move CoverFallback to shared/components since it is now used by both
the dashboard and enrollment features.

The admin enrollment view (approve/reject table) is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sidebar takes ~230px, so at 1024px the content area is only ~794px
which is too narrow for the 3-column horizontal card layout (cover +
info + actions). Raise the breakpoint from 760px to 1100px so tablet
portrait and landscape both get the vertical card layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Certificate list: year-grouped sections with rekap summary cards,
CoverFallback thumbnails, JP pills, copy cert number, and responsive
mobile layout with horizontal scroll rekap and compact tap-to-view rows.

Certificate detail: two-column layout with SVG certificate preview
sketch, sticky data panel (name, org, JP green, date, number with copy),
download/verify/material actions, QR card, and breadcrumb navigation.
Mobile: single column with preview, actions under title, card-style data.

Public verification: status banners (valid/revoked/not-found) with
colored backgrounds and icons, certificate number display, data grid,
info card, and "check another number" search. Mobile-first since most
QR scans happen from phones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The revoked/not-found variant examples were mockup design references,
not user-facing content. The initial state now shows only the intro
text and search input.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
At 360px width the 64px thumbnail + JP pill + chevron left only ~153px
for the title, truncating it to 3 words. Reduce thumbnail to 48×34 and
pill to 10px/600 so the title gets enough room for a readable 2-line
display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The initial state (no token) now shows a focused landing page with:
- Shield icon, serif title, and "Tidak perlu login" subtitle
- Input card with formatted placeholder (SINAU-2026-000000), help text,
  and secondary QR scan option
- Three-column explainer (what's shown, what's not, contact)
- Footer with legal basis (Perwal No. 12/2026)

Mobile: QR scan as primary CTA above the input card, stacked explainer,
compact footer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a participant has no certificates yet, show an encouraging empty
state with: document illustration, "Belum ada sertifikat" explanation,
"Paling dekat selesai" card showing the active enrollment closest to
completion (progress bar, remaining modules, JP, deadline), and CTA
buttons to enrollments and catalog.

The rekap area shows "0 JP" with potential JP from active enrollments,
plus an info banner about SIMPEG.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Participant catalog now shows a 4-column card grid (16:9 covers with
CoverFallback, kind/status pills, org tags) with enrollment status
overlay per card: Lanjutkan·N%, Terdaftar, Selesai, Daftar, Tonton.

Controls: kind chips with counts (Semua·N, Pelatihan·N, Webinar·N,
Ber-JP), search, sort (terbaru/deadline/JP), filter button. Active
filters shown as removable chips.

Mobile: horizontal card rows, sticky controls, bottom sheet filter
with sort radios, rumpun/penyelenggara/status chip toggles, and
"Tampilkan N pelatihan" preview count.

Pagination: load-more (12 per page), hidden when total ≤ 12.
Admin catalog ("Kelola pelatihan") unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Desktop filter button now toggles a facet row with CatalogFacet
popovers (Rumpun, Penyelenggara, Status) below the toolbar, matching
the existing admin catalog pattern. Bottom sheet stays for mobile only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The mobile search row and info line were hidden at all viewports by
base display:none rules. Move those to the desktop-only media query
so they appear on mobile as designed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three chips (Semua, Pelatihan, Webinar) fit the 390px viewport; a
fourth causes horizontal overflow. Ber-JP is desktop-only now and
available through the bottom sheet filter on mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Catalog: fix search container to use flex+min-width:0 so input doesn't
overflow its parent. Remove inline paddingLeft style.

Dashboard: add overflow-x:hidden on page, reduce mobile padding to
20px 16px for consistent gutter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The calendar icon and text were centered in a tall card on mobile.
Align them to flex-start and reduce the icon from 72px to 48px so the
empty state reads as a compact note below the heading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reduce card padding from 24px to 16px on mobile, shrink cert/reco
covers and row padding, add overflow:hidden on cards, word-break on
recommendation reason text. Remove the page-level overflow-x:hidden
bandaid now that the actual sources are fixed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Admin catalog (TrainingCard):
- Cover 4:3 → 16:9 with CoverFallback from shared/components
- Remove category eyebrow and crest avatar
- Org name + bold JP in meta line
- 4-column grid at ≥1280px, hover translateY(-2px) + shadow
- Hide empty meta separator (Legacy card bug)

Admin enrollment table:
- CoverFallback thumbnail (48×34) beside training title
- Enrollment progress label below title

Admin certificate gallery:
- Same sertifikat row layout as participant view
- CoverFallback thumbnails, JP pills, org + date meta
- Training data enrichment for JP and org

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two-tab system: "Menunggu persetujuan" queue and "Semua pendaftaran"
table, each with summary cards showing pending count (with oldest wait
time), active enrollments, completions, and certificates.

Pending tab: checkbox selection with bulk approve, training org + JP
from cross-lookup, waiting time in days (accent when >4 days), green
approve and accent reject buttons.

All tab: search + training + status filters, progress bar (72×5) with
n/m count, enrolled date, approve/reject/certificate actions, numbered
pagination (25 per page).

Fields not yet in the API (segment, unit kerja, quota, rejection
reason, last activity) are omitted; the layout accommodates them when
the backend adds them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enrollment enrichment:
- Enrich enrollment response with participant type, email, NIP,
  organization ID and name (joined from participants/users/organizations)
- Add rejection_reason and rejection_note fields to enrollment
- Add last_activity_at timestamp, updated on every MarkActivity call
- PUT /enrollments/:id/reject now accepts {reason, note} body
- POST /enrollments/bulk-approve for batch approval (up to 50)

Certificate and training:
- Add verification_url to certificate response (computed from token)
- Add current_enrollments count to training response

Dashboard:
- Add completed_this_month and completed_last_month to stats
  (enables "+N vs last month" delta in admin summary cards)

Webinar:
- GET /api/webinars/mine returns upcoming sessions for enrolled trainings

Session management:
- DELETE /api/auth/sessions invalidates all existing tokens by setting
  logged_out_all_at; the /auth/me handler checks JWT iat against it
- Actor now carries IssuedAt from the JWT claims

Participant profile:
- PUT /api/participants/:id/avatar for photo upload (JPEG/PNG, 5MB)
- GET /api/participants/:id/avatar to serve the photo
- has_avatar boolean in participant response

Admin notes (new package adminnote):
- GET/POST /api/users/:id/notes, DELETE /api/users/:id/notes/:noteId
- Admin-only, stores free-text notes on participant profiles

Notification preferences (new package notification):
- GET/PUT /api/notifications/preferences
- Per-user email toggles (enrollment, certificate, reminder)

Organization abbreviation:
- Add abbreviation field to organizations table, Organization struct,
  OrgRef, and all queries that return them
- Migration 053 backfills from name (first word when all-caps)

Migrations: 052_backend_enrichment.sql, 053_organization_abbreviation.sql
TypeScript types updated to match all new fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: redesign participant profile and add admin detail page
Some checks failed
check / check (pull_request) Failing after 2m49s
433ea51f5a
Redesign the participant profile page with sectioned navigation
(data, contact, notifications, security, privacy) and add an admin
participant detail page with enrollment history. Link participant
names in the admin list to the new detail view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: add pr and ship skills for workflow automation
Some checks failed
check / check (pull_request) Failing after 3m24s
38ef42742b
Add two new Claude Code skills for streamlined development workflow:
- pr: Create pull requests on Forgejo automatically
- ship: Commit and push changes with safety checks
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
fix: restore type coverage in web typecheck and refresh stale fixtures
All checks were successful
check / check (pull_request) Successful in 3m17s
0945932835
`npm run typecheck` ran `tsc --noEmit` against the root tsconfig, which
declares `files: []` and carries only project references. Non-build mode
does not follow references, so the step compiled nothing and passed
unconditionally. Six test fixtures went stale behind it until `tsc -b`
inside the production build caught them in CI.

Build mode makes the step check the referenced projects again, and the
fixtures gain the fields their response types now require:
completed_this_month and completed_last_month on Stats, has_avatar on
Participant, current_enrollments on Training.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both files were written without `-- +goose Up`, so goose refused to parse
them and no database could be migrated past 051. They also gain a Down
section so they can be rolled back like the rest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat: seed a development deployment with demo data on first start
All checks were successful
check / check (pull_request) Successful in 4m1s
17b5c18156
A development container now fills an empty database with demo
organizations, accounts, participants, trainings, and enrollments so the
deployment is usable the moment it comes up. cmd/seed runs between goose
and the server, only when SEED_DEMO=true, and refuses a database that
already holds an account, so the same CMD is safe for production and a
redeploy over a persistent volume never duplicates data. `make seed-demo`
does the same without Docker.

The account CLI docs in .env.example and the run-app skill still described
ACCOUNT_ROLE, which the layered authorization removed; they now show
ACCOUNT_TYPE, ACCOUNT_ORGANIZATION, and ACCOUNT_ACCESS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat: let a deployment start over with a fresh database from its env alone
All checks were successful
check / check (pull_request) Successful in 4m35s
4e5d7877c2
docker-compose.prod.yml reads the Postgres data directory from
POSTGRES_DATA_DIR (default unchanged). Postgres initialises a database only
into an empty directory, so pointing the variable at a new path gives a
deployment a fresh database while the old directory stays on disk — the
only way to do it when the host is reachable through a compose manager
such as Komodo and not a shell.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat: give the demo trainings their material
All checks were successful
check / check (pull_request) Successful in 3m6s
5c32a6cc9d
The seeded trainings were shells: a title in the catalogue and nothing to
open. Each published pelatihan now carries ordered modules with articles
written as the editor's document, YouTube videos from public government
channels, a quiz attached to the video, and a graded post-test; the webinar
gets a scheduled session and a handout; the draft keeps one half-written
article so the publication checklist has something to flag.

Everything goes through the domain repositories, so positions, the
material-owns-module rule, and quiz grading constraints hold the same way
they do for material authored in the UI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On Kelola pelatihan a draft looked exactly like a published entry, down to
the "Mendatang" phase label, which only means something once the entry is
live. A card that is not on offer now wears its state on the cover, in the
same warm colour the console uses for a draft, has a dimmed cover and a
dashed border, and says "Belum dipublikasikan" or "Tidak tayang" where the
phase would go. Published cards are untouched, and participants never see
these because the catalogue lists published entries alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
"Publikasi dilakukan dari halaman entri" named a page that does not exist;
the Publikasikan button lives on the training's own page. Both intros now
describe the actual path — save the draft, finish it on the edit page, press
Publikasikan on the training page — without the dashes and the borrowed
"draftnya" and "editor".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fallback cover already draws the organisation's abbreviation in its
SVG, and the catalogue card drew the same word over it again, so "BKPSDM"
overlapped itself on every entry without a cover image.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pelatihan saya listed only enrolled, in-progress, and completed rows, so an
application awaiting approval left no trace and a rejected one vanished.
Both now have their own sections, a "Menunggu" tab that appears only when
there is something in them, and a count in the summary line.

A webinar whose session is still ahead, or a training whose start date is,
is an appointment rather than something to continue: its card shows the
date and one "Lihat" button instead of a progress bar and "Mulai belajar",
and the pill counts the days instead of calling the session a deadline.
The same rule keeps them out of the dashboard's "Lanjutkan belajar" and
puts them in "Jadwal saya", which until now was a hard-coded empty state
that never read any data. Cards carry the kind on the cover, as the
catalogue does, so a webinar and a training are told apart.

Both pages also dropped a blanket rule that made every link blue and
underlined on hover, which overrode the global link style and underlined
the titles of card-shaped links. Text links now opt in through one shared
class; cards change their ground like everywhere else.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The execution window gated only what counts as progress — video heartbeats,
read marks, quiz starts — while reading and watching stayed open from the
moment of enrolment. Before start_date that meant a participant could work
through everything and be refused only when the server tried to record it.

Now nothing opens before start_date: module.Service closes every outline
item and refuses UnlockedForMaterial, which the document and video services
call before handing over a file, and the document list withholds article
bodies, which travelled in the same row. Managers still read their own
material at any time, and after end_date an unlocked piece stays readable
as before — what the deadline closes is unchanged.

The course page says when instead of what is next: the material step reads
"Terbuka 5 Okt 2026", the action is that date, and a material reached by a
direct link explains the same rather than "selesaikan materi sebelumnya".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Aksi column on Semua pendaftaran showed a different control per status
— two buttons, a certificate link, or "Buka" — so the eye re-read every row
to find where to click. Each row now has one quiet "⋯" button, and the menu
behind it lists only what would work for that row and that reader: approve
and reject on a pending row, the training, the participant's profile, the
certificate or its issuing, and, set apart in red, cancelling the
enrolment — an endpoint the server had and nothing called.

The participant's name links to their profile for whoever administers
participants, on both tabs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix: read a participant's enrolments from the list that exists
All checks were successful
check / check (pull_request) Successful in 3m37s
4547d19835
The profile's Pelatihan tab asked /api/participants/{id}/enrollments, an
endpoint the server does not have, swallowed the error, and matched on a
status word ("active") the server never sends — so it was always empty and
the stat cards always read zero. It now narrows the admin's enrolment list
to the one participant, fills organisation, JP, and deadline from the
training list, and uses the enrolment's own status words.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cola changed title from feat: add layered organization authorization to feat: layered authorization and the participant-facing redesign 2026-09-21 11:43:17 +07:00
All checks were successful
check / check (pull_request) Successful in 3m37s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/layered-authorization:feat/layered-authorization
git switch feat/layered-authorization

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch master
git merge --no-ff feat/layered-authorization
git switch feat/layered-authorization
git rebase master
git switch master
git merge --ff-only feat/layered-authorization
git switch feat/layered-authorization
git rebase master
git switch master
git merge --no-ff feat/layered-authorization
git switch master
git merge --squash feat/layered-authorization
git switch master
git merge --ff-only feat/layered-authorization
git switch master
git merge feat/layered-authorization
git push origin master
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cola/LMS!3
No description provided.