@bongos/core 1.24.5 → 1.24.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +79 -54
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +8 -1
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0371-auto-pickup-applies-a-submitted-tweak-on-the-runner.md +1 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +8 -0
- package/docs/api/openapi.json +114 -6
- package/docs/api-reference.md +7 -4
- package/docs/copy-inventory.md +52 -43
- package/docs/copy-registry.json +145 -63
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +71 -63
- package/migrations/core_286_credit_log_no_spend.sql +128 -0
- package/modules/copy-desk/auto-close.js +5 -3
- package/modules/copy-desk/page-status.js +7 -4
- package/modules/copy-desk/tests/copy_page_auto_close.mjs +4 -4
- package/modules/hall-ui/public/project-settings.html +4 -4
- package/modules/hall-ui/public/project-settings.js +51 -1
- package/modules/lifecycle/autopickup.js +26 -9
- package/modules/lifecycle/db-claims.js +1 -1
- package/modules/lifecycle/routes/claims.js +7 -1
- package/modules/lifecycle/routes/project-settings.js +71 -10
- package/modules/platform-identity/account-logins.js +131 -0
- package/modules/platform-identity/migrations/platform_identity_035_account_login_email_idx.sql +19 -0
- package/modules/platform-identity/routes/profile.js +29 -1
- package/modules/platform-identity/routes/sso.js +25 -3
- package/modules/public-landing/public/account.html +114 -0
- package/modules/public-landing/public/account.states.json +1 -0
- package/modules/ui-design/kit/serve.js +24 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/src/bongos/auth-github.js +24 -7
- package/src/bongos/project-settings.js +59 -15
- package/src/bongos/routes/auth-google.js +71 -5
- package/src/bongos/routes/auth-link.js +132 -0
- package/src/bongos/routes.js +2 -0
- package/src/bongos/sign-in-pages.js +20 -1
- package/src/module-api.js +1 -1
- package/tests/account_linking.mjs +289 -0
- package/tests/account_logins_db.mjs +56 -0
- package/tests/autopickup_cap.mjs +203 -11
- package/tests/claims_batch_routes.mjs +79 -12
- package/tests/copy_desk_page_release.mjs +20 -0
- package/tests/copy_stuck_page_tweaks.mjs +2 -2
- package/tests/credit_log_no_spend_db.mjs +160 -0
- package/tests/effective_visibility_predicate.mjs +8 -0
- package/tests/hub_account_page.mjs +91 -5
- package/tests/project_settings_page.mjs +8 -5
package/docs/page-readings.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"adr": "0341 D3",
|
|
6
6
|
"reread": "node scripts/bongos/page-reader.js --stale",
|
|
7
7
|
"how": ["Generated by node scripts/bongos/page-reader.js (BV2.TW04, task 1004315; ADR 0341 D3). Do not hand-edit.","One entry per page of docs/page-inventory.json. Lines are in page order: the default state first, and a line only another state shows sits where that state shows it, with its state named.","placement: placed = a docs/copy-registry.json row renders it (file, line, string_id); shared = shell text every page of the surface carries (top bar, sidebar, footer), with its row when the registry has one; unplaced = the registry cannot place it (a rewrite becomes an engineer task, ADR 0341 D4).","text keeps {…} where the registry row has a hole, or where a value the page read from its API was filled in. A string that is only such data is counted under omitted.data, not listed.","reading_hash is over the non-shared lines (key, section, text); files_hash is over the page's inventory files. --check compares files_hash only and never opens a browser.","Rendered through the hall-preview harness (hall) and the ui-design kit stub (landing, status), at 1280x800 with the clock pinned, so a reading is the fixture world's page."],
|
|
8
|
-
"counts": {"pages":48,"lines":
|
|
8
|
+
"counts": {"pages":48,"lines":5464,"placed":1687,"shared":427,"unplaced":3350,"omitted_data":3732},
|
|
9
9
|
"pages": [
|
|
10
10
|
{
|
|
11
11
|
"id": "builders:ai-studio",
|
|
@@ -2595,9 +2595,9 @@
|
|
|
2595
2595
|
"surface": "builders",
|
|
2596
2596
|
"title": "Project settings",
|
|
2597
2597
|
"states_read": ["default"],
|
|
2598
|
-
"files_hash": "
|
|
2599
|
-
"reading_hash": "
|
|
2600
|
-
"counts": {"lines":38,"placed":
|
|
2598
|
+
"files_hash": "39656ae681066cb4",
|
|
2599
|
+
"reading_hash": "4f7a29f2c2f0ad7c",
|
|
2600
|
+
"counts": {"lines":38,"placed":20,"shared":8,"unplaced":10},
|
|
2601
2601
|
"omitted": {"data":31},
|
|
2602
2602
|
"notes": [],
|
|
2603
2603
|
"lines": [
|
|
@@ -2609,7 +2609,7 @@
|
|
|
2609
2609
|
{"key":"L0006","section":"top bar","text":"Artistry","placement":"shared"},
|
|
2610
2610
|
{"key":"L0007","section":"top bar","text":"Ideating","placement":"shared"},
|
|
2611
2611
|
{"key":"L0008","section":"The fundamentals","text":"The fundamentals","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":39,"string_id":"cb7c7879eeba"},
|
|
2612
|
-
{"key":"L0009","section":"The fundamentals","text":"
|
|
2612
|
+
{"key":"L0009","section":"The fundamentals","text":"Seven things every project settles. Four of them are knobs you can turn here; the other three are decided in committed configuration, and each says which file decides it.","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":40,"string_id":"f7da8748453d"},
|
|
2613
2613
|
{"key":"L0010","section":"The fundamentals","text":"{…} and {…}","placement":"placed","file":"modules/hall-ui/public/profile-nudge.js","line":77,"string_id":"56b00d773232","occurrences":9},
|
|
2614
2614
|
{"key":"L0011","section":"The fundamentals","text":"How tasks ship","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":136,"string_id":"76f344dd4727"},
|
|
2615
2615
|
{"key":"L0012","section":"The fundamentals","text":"Save","placement":"unplaced","occurrences":4},
|
|
@@ -2617,14 +2617,14 @@
|
|
|
2617
2617
|
{"key":"L0014","section":"The fundamentals","text":"Raise after","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":115,"string_id":"4de232a05888"},
|
|
2618
2618
|
{"key":"L0015","section":"The fundamentals","text":"days ({…}–{…})","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":115,"string_id":"0e2bef756977"},
|
|
2619
2619
|
{"key":"L0016","section":"The fundamentals","text":"offered","placement":"unplaced","occurrences":2},
|
|
2620
|
-
{"key":"L0017","section":"The fundamentals","text":"New category label","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":
|
|
2621
|
-
{"key":"L0018","section":"The fundamentals","text":"Add","placement":"
|
|
2620
|
+
{"key":"L0017","section":"The fundamentals","text":"New category label","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":193,"string_id":"87f09d983f6c","attr":"placeholder"},
|
|
2621
|
+
{"key":"L0018","section":"The fundamentals","text":"Add","placement":"placed","file":"modules/hall-ui/public/ai-studio.js","line":469,"string_id":"e4da002ae135"},
|
|
2622
2622
|
{"key":"L0019","section":"The fundamentals","text":"committed config","placement":"unplaced"},
|
|
2623
2623
|
{"key":"L0020","section":"The fundamentals","text":"not yet real","placement":"unplaced"},
|
|
2624
2624
|
{"key":"L0021","section":"The fundamentals","text":"not available ({…})","placement":"unplaced"},
|
|
2625
2625
|
{"key":"L0022","section":"Danger zone","text":"Danger zone","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":56,"string_id":"13b22e48d9fb"},
|
|
2626
|
-
{"key":"L0023","section":"Danger zone","text":"This project is hosted on {…}, and that is where it is taken offline or removed. Both links below open this project’s page there.","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":
|
|
2627
|
-
{"key":"L0024","section":"Danger zone","text":"Manage this project on {…}","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":
|
|
2626
|
+
{"key":"L0023","section":"Danger zone","text":"This project is hosted on {…}, and that is where it is taken offline or removed. Both links below open this project’s page there.","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":350,"string_id":"e97b2d549f10"},
|
|
2627
|
+
{"key":"L0024","section":"Danger zone","text":"Manage this project on {…}","placement":"placed","file":"modules/hall-ui/public/project-settings.js","line":351,"string_id":"4e261fd15bd5"},
|
|
2628
2628
|
{"key":"L0025","section":"Danger zone","text":"Take it offline","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":59,"string_id":"da9da6e866bd"},
|
|
2629
2629
|
{"key":"L0026","section":"Danger zone","text":"— stops the project and its nightly backups but keeps everything: its database, its backups and your repository on GitHub. You can bring it back.","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":59,"string_id":"61ebc5d684fd"},
|
|
2630
2630
|
{"key":"L0027","section":"Danger zone","text":"Disconnect from Bongos entirely","placement":"placed","file":"modules/hall-ui/public/project-settings.html","line":60,"string_id":"d3efd513694b"},
|
|
@@ -4568,11 +4568,11 @@
|
|
|
4568
4568
|
"id": "landing:account",
|
|
4569
4569
|
"surface": "landing",
|
|
4570
4570
|
"title": "{{productName}} — Your account",
|
|
4571
|
-
"states_read": ["out","in","in-projects-band","in-profile-band","in-building","in-privacy","in-private"],
|
|
4572
|
-
"files_hash": "
|
|
4573
|
-
"reading_hash": "
|
|
4574
|
-
"counts": {"lines":
|
|
4575
|
-
"omitted": {"data":
|
|
4571
|
+
"states_read": ["out","in","in-projects-band","in-profile-band","in-building","in-ways","in-privacy","in-private"],
|
|
4572
|
+
"files_hash": "62466bd448aefdf7",
|
|
4573
|
+
"reading_hash": "a50818bf29267425",
|
|
4574
|
+
"counts": {"lines":100,"placed":46,"shared":12,"unplaced":42},
|
|
4575
|
+
"omitted": {"data":10},
|
|
4576
4576
|
"notes": [],
|
|
4577
4577
|
"lines": [
|
|
4578
4578
|
{"key":"L0001","section":"top bar","text":"Community","placement":"shared"},
|
|
@@ -4618,55 +4618,63 @@
|
|
|
4618
4618
|
{"key":"L0041","section":"your handle","text":"your-handle","placement":"unplaced","attr":"placeholder","state":"in"},
|
|
4619
4619
|
{"key":"L0042","section":"your handle","text":"Lowercase letters, numbers and hyphens, up to 39. You can change it once a week, and your old handle keeps leading to you until you change it again.","placement":"placed","file":"modules/public-landing/public/account.html","line":211,"string_id":"5ceffa7b9d7e","state":"in"},
|
|
4620
4620
|
{"key":"L0043","section":"your handle","text":"change handle","placement":"placed","file":"modules/public-landing/public/account.html","line":212,"string_id":"0a22c9a537b4","state":"in"},
|
|
4621
|
-
{"key":"L0044","section":"
|
|
4622
|
-
{"key":"L0045","section":"
|
|
4623
|
-
{"key":"L0046","section":"
|
|
4624
|
-
{"key":"L0047","section":"
|
|
4625
|
-
{"key":"L0048","section":"
|
|
4626
|
-
{"key":"L0049","section":"
|
|
4627
|
-
{"key":"L0050","section":"
|
|
4628
|
-
{"key":"L0051","section":"
|
|
4629
|
-
{"key":"L0052","section":"who can find you","text":"
|
|
4630
|
-
{"key":"L0053","section":"who can find you","text":"
|
|
4631
|
-
{"key":"L0054","section":"who can find you","text":"
|
|
4632
|
-
{"key":"L0055","section":"who can find you","text":"
|
|
4633
|
-
{"key":"L0056","section":"who can find you","text":"
|
|
4634
|
-
{"key":"L0057","section":"who can find you","text":"
|
|
4635
|
-
{"key":"L0058","section":"who can find you","text":"
|
|
4636
|
-
{"key":"L0059","section":"who can find you","text":"
|
|
4637
|
-
{"key":"L0060","section":"who can find you","text":"
|
|
4638
|
-
{"key":"L0061","section":"who can find you","text":"
|
|
4639
|
-
{"key":"L0062","section":"who can find you","text":"
|
|
4640
|
-
{"key":"L0063","section":"who can find you","text":"
|
|
4641
|
-
{"key":"L0064","section":"who can find you","text":"
|
|
4642
|
-
{"key":"L0065","section":"
|
|
4643
|
-
{"key":"L0066","section":"
|
|
4644
|
-
{"key":"L0067","section":"
|
|
4645
|
-
{"key":"L0068","section":"
|
|
4646
|
-
{"key":"L0069","section":"
|
|
4647
|
-
{"key":"L0070","section":"
|
|
4648
|
-
{"key":"L0071","section":"
|
|
4649
|
-
{"key":"L0072","section":"
|
|
4650
|
-
{"key":"L0073","section":"your record","text":"
|
|
4651
|
-
{"key":"L0074","section":"your record","text":"
|
|
4652
|
-
{"key":"L0075","section":"your record","text":"
|
|
4653
|
-
{"key":"L0076","section":"your record","text":"·
|
|
4654
|
-
{"key":"L0077","section":"your record","text":"
|
|
4655
|
-
{"key":"L0078","section":"
|
|
4656
|
-
{"key":"L0079","section":"
|
|
4657
|
-
{"key":"L0080","section":"
|
|
4658
|
-
{"key":"L0081","section":"
|
|
4659
|
-
{"key":"L0082","section":"
|
|
4660
|
-
{"key":"L0083","section":"
|
|
4661
|
-
{"key":"L0084","section":"
|
|
4662
|
-
{"key":"L0085","section":"
|
|
4663
|
-
{"key":"L0086","section":"
|
|
4664
|
-
{"key":"L0087","section":"
|
|
4665
|
-
{"key":"L0088","section":"
|
|
4666
|
-
{"key":"L0089","section":"
|
|
4667
|
-
{"key":"L0090","section":"
|
|
4668
|
-
{"key":"L0091","section":"
|
|
4669
|
-
{"key":"L0092","section":"
|
|
4621
|
+
{"key":"L0044","section":"ways to sign in","text":"ways to sign in","placement":"placed","file":"modules/public-landing/public/account.html","line":223,"string_id":"667fd4617840","state":"in"},
|
|
4622
|
+
{"key":"L0045","section":"ways to sign in","text":"GitHub, Google or both. Each one opens this same account, with the same credits, ranks and history.","placement":"placed","file":"modules/public-landing/public/account.html","line":224,"string_id":"8c815a28df30","state":"in"},
|
|
4623
|
+
{"key":"L0046","section":"ways to sign in","text":"GitHub","placement":"unplaced","state":"in"},
|
|
4624
|
+
{"key":"L0047","section":"ways to sign in","text":"linked","placement":"unplaced","state":"in"},
|
|
4625
|
+
{"key":"L0048","section":"ways to sign in","text":"Your account was made with GitHub, so GitHub stays linked.","placement":"unplaced","state":"in"},
|
|
4626
|
+
{"key":"L0049","section":"ways to sign in","text":"Google","placement":"unplaced","state":"in"},
|
|
4627
|
+
{"key":"L0050","section":"ways to sign in","text":"Remove","placement":"unplaced","state":"in"},
|
|
4628
|
+
{"key":"L0051","section":"ways to sign in","text":"Linking never joins two accounts. If the other one already has its own account here, we say so and change nothing.","placement":"placed","file":"modules/public-landing/public/account.html","line":228,"string_id":"62bf4cd7f215","state":"in"},
|
|
4629
|
+
{"key":"L0052","section":"who can find you","text":"who can find you","placement":"placed","file":"modules/public-landing/public/account.html","line":240,"string_id":"7322eee6b8b4","state":"in"},
|
|
4630
|
+
{"key":"L0053","section":"who can find you","text":"Your profile page can be public or private, and recruiters are let in separately — so a private profile can still be reachable for work, and a public one can stay out of recruiting entirely.","placement":"placed","file":"modules/public-landing/public/account.html","line":241,"string_id":"38f40fb26117","state":"in"},
|
|
4631
|
+
{"key":"L0054","section":"who can find you","text":"Your profile page","placement":"placed","file":"modules/public-landing/public/account.html","line":244,"string_id":"2df3e63e4761","state":"in"},
|
|
4632
|
+
{"key":"L0055","section":"who can find you","text":"Public","placement":"unplaced","state":"in"},
|
|
4633
|
+
{"key":"L0056","section":"who can find you","text":"Anyone find you.","placement":"placed","file":"modules/public-landing/public/account.html","line":246,"string_id":"525a9aa1dd71","state":"in"},
|
|
4634
|
+
{"key":"L0057","section":"who can find you","text":"Private","placement":"unplaced","state":"in"},
|
|
4635
|
+
{"key":"L0058","section":"who can find you","text":"you are a ninja. Private hides the page from everyone — including you. You see your record here.","placement":"placed","file":"modules/public-landing/public/account.html","line":247,"string_id":"8caf52240833","state":"in"},
|
|
4636
|
+
{"key":"L0059","section":"who can find you","text":"Every guild you belong to counts your numbers in its rounded totals unless you leave them out — even while your profile page is {…}. They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. Each guild’s page has the switch:","placement":"unplaced","state":"in-privacy"},
|
|
4637
|
+
{"key":"L0060","section":"who can find you","text":"· counted","placement":"unplaced","state":"in-privacy"},
|
|
4638
|
+
{"key":"L0061","section":"who can find you","text":"· left out","placement":"unplaced","state":"in-privacy"},
|
|
4639
|
+
{"key":"L0062","section":"who can find you","text":"Recruiters","placement":"unplaced","state":"in"},
|
|
4640
|
+
{"key":"L0063","section":"who can find you","text":"Can find me","placement":"placed","file":"modules/public-landing/public/account.html","line":254,"string_id":"50d493f08964","state":"in"},
|
|
4641
|
+
{"key":"L0064","section":"who can find you","text":"Recruiters looking for builders can find you.","placement":"placed","file":"modules/public-landing/public/account.html","line":254,"string_id":"604f5274ae98","state":"in"},
|
|
4642
|
+
{"key":"L0065","section":"who can find you","text":"Can’t find me","placement":"placed","file":"modules/public-landing/public/account.html","line":255,"string_id":"48d721eb2ed7","state":"in"},
|
|
4643
|
+
{"key":"L0066","section":"who can find you","text":"You stay out of the recruiting directory and a recruiter’s look-up.","placement":"placed","file":"modules/public-landing/public/account.html","line":255,"string_id":"af2177106d7c","state":"in"},
|
|
4644
|
+
{"key":"L0067","section":"who can find you","text":"Because your cross-project record is hidden, they will see your name and your picture, and nothing else. This is the one thing a recruiter can see while your profile page is {…}. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one {…} — straight away.","placement":"unplaced","state":"in-private"},
|
|
4645
|
+
{"key":"L0068","section":"who can find you","text":"They will see your name, your picture, your roles, and whether you have been {…} in the last 30 days — never your {…}, works shipped, karma, or the projects you build on. This is the one thing a recruiter can see while your profile page is {…}. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one {…} — straight away.","placement":"unplaced","state":"in-privacy"},
|
|
4646
|
+
{"key":"L0069","section":"who can find you","text":"They will see your name, your picture, your roles, and whether you have been {…} in the last 30 days — never your {…}, works shipped, karma, or the projects you build on. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one {…} — straight away.","placement":"unplaced","state":"in"},
|
|
4647
|
+
{"key":"L0070","section":"who can find you","text":"· For now this follows your profile page setting.","placement":"unplaced","state":"in"},
|
|
4648
|
+
{"key":"L0071","section":"who can find you","text":"Your profile page setting applies now — private already answers “not found” everywhere your profile is read. Recruiter discoverability applies now too — switching it off removes you from both recruiting surfaces immediately, and back on puts you there again.","placement":"placed","file":"modules/public-landing/public/account.html","line":259,"string_id":"e0d6e4196161","state":"in"},
|
|
4649
|
+
{"key":"L0072","section":"who can find you","text":"save","placement":"unplaced","state":"in"},
|
|
4650
|
+
{"key":"L0073","section":"your record","text":"your record","placement":"placed","file":"modules/public-landing/public/account.html","line":271,"string_id":"b494287ce724","state":"in"},
|
|
4651
|
+
{"key":"L0074","section":"your record","text":"Your track record across every project you build on. Hiding it takes effect immediately everywhere — your profile, the recruiting directory, and a recruiter’s look-up of one builder.","placement":"placed","file":"modules/public-landing/public/account.html","line":272,"string_id":"f18600e9cff1","state":"in"},
|
|
4652
|
+
{"key":"L0075","section":"your record","text":"No cross-project activity reported yet. Your profile is not published yet; it publishes after your next sign-in.","placement":"unplaced","state":"in-private"},
|
|
4653
|
+
{"key":"L0076","section":"your record","text":"Your record: 3 projects · 1000 {…} · 19 works shipped · 10 karma.","placement":"unplaced","state":"in"},
|
|
4654
|
+
{"key":"L0077","section":"your record","text":"Cross-project Record","placement":"placed","file":"modules/public-landing/public/account.html","line":276,"string_id":"d5c4a0e0f864","state":"in"},
|
|
4655
|
+
{"key":"L0078","section":"your record","text":"Show it","placement":"placed","file":"modules/public-landing/public/account.html","line":278,"string_id":"960b894c0ea1","state":"in"},
|
|
4656
|
+
{"key":"L0079","section":"your record","text":"Your stats appear on your profile and in the directories.","placement":"placed","file":"modules/public-landing/public/account.html","line":278,"string_id":"edf1c4f5e0c5","state":"in"},
|
|
4657
|
+
{"key":"L0080","section":"your record","text":"Hide it","placement":"placed","file":"modules/public-landing/public/account.html","line":279,"string_id":"ccd4861996eb","state":"in"},
|
|
4658
|
+
{"key":"L0081","section":"your record","text":"Your stats are hidden.","placement":"placed","file":"modules/public-landing/public/account.html","line":279,"string_id":"44c8b76631bf","state":"in"},
|
|
4659
|
+
{"key":"L0082","section":"your record","text":"Every guild you belong to counts your numbers in its rounded totals unless you leave them out — even while your record is hidden. They are never shown on their own: only inside totals rounded down to a band (10+, 20+, 50+ …) and counted once a week. Each guild’s page has the switch:","placement":"unplaced","state":"in-private"},
|
|
4660
|
+
{"key":"L0083","section":"your record","text":"· counted","placement":"unplaced","state":"in-private"},
|
|
4661
|
+
{"key":"L0084","section":"your record","text":"· left out","placement":"unplaced","state":"in-private"},
|
|
4662
|
+
{"key":"L0085","section":"your record","text":"save","placement":"unplaced","state":"in"},
|
|
4663
|
+
{"key":"L0086","section":"projects you build on","text":"projects you build on","placement":"placed","file":"modules/public-landing/public/account.html","line":296,"string_id":"f8aa3ff34a10","state":"in"},
|
|
4664
|
+
{"key":"L0087","section":"projects you build on","text":"The projects you have joined as a builder. Only you see this list.","placement":"placed","file":"modules/public-landing/public/account.html","line":297,"string_id":"d466ee6b6e30","state":"in"},
|
|
4665
|
+
{"key":"L0088","section":"projects you build on","text":"example.com","placement":"unplaced","state":"in"},
|
|
4666
|
+
{"key":"L0089","section":"projects you build on","text":"Leave","placement":"unplaced","state":"in","occurrences":2},
|
|
4667
|
+
{"key":"L0090","section":"projects you build on","text":"{…}-circles.cloudbongos.com","placement":"unplaced","state":"in"},
|
|
4668
|
+
{"key":"L0091","section":"footer","text":"A non-profit, capture-resistant commons.","placement":"shared","file":"modules/public-landing/public/account.html","line":308,"string_id":"afe83bbbd0d4","state":"in","occurrences":2},
|
|
4669
|
+
{"key":"L0092","section":"footer","text":"Built by agents, on Cloud Bongos.","placement":"shared","file":"modules/public-landing/public/account.html","line":308,"string_id":"bd315dd71ea7","state":"in","occurrences":2},
|
|
4670
|
+
{"key":"L0093","section":"footer","text":"Open source","placement":"shared","file":"modules/public-landing/public/account.html","line":308,"string_id":"d6314ddcc1d0","state":"in","occurrences":2},
|
|
4671
|
+
{"key":"L0094","section":"footer","text":"AGPL-3.0","placement":"shared","state":"in","occurrences":2},
|
|
4672
|
+
{"key":"L0095","section":"navigation","text":"Privacy","placement":"shared","state":"in","occurrences":2},
|
|
4673
|
+
{"key":"L0096","section":"navigation","text":"Terms","placement":"shared","state":"in","occurrences":2},
|
|
4674
|
+
{"key":"L0097","section":"navigation","text":"Contact","placement":"shared","state":"in","occurrences":2},
|
|
4675
|
+
{"key":"L0098","section":"navigation","text":"Builders' hall","placement":"shared","file":"modules/public-landing/public/account.html","line":308,"string_id":"90a128c97eae","state":"in","occurrences":2},
|
|
4676
|
+
{"key":"L0099","section":"your account","text":"Sign in to change your handle, your bio and links, and who can find you.","placement":"placed","file":"modules/public-landing/public/account.html","line":159,"string_id":"d9eefaa86565"},
|
|
4677
|
+
{"key":"L0100","section":"your account","text":"Sign in","placement":"placed","file":"modules/public-landing/public/account.html","line":113,"string_id":"ad80586f802a"}
|
|
4670
4678
|
]
|
|
4671
4679
|
},
|
|
4672
4680
|
{
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
-- core_286_credit_log_no_spend.sql — task 1004988 (goal 1000092; ADR 0385
|
|
2
|
+
-- decision 2, ADR 0387).
|
|
3
|
+
--
|
|
4
|
+
-- Credits measure contribution. They are never spent, transferred or redeemed,
|
|
5
|
+
-- so the ledger has no row that takes credits away for something in return.
|
|
6
|
+
-- Until now that was a convention: credit_log's own column notes listed
|
|
7
|
+
-- 'redemption' as a reason and called a negative delta "spent (future)", and
|
|
8
|
+
-- nothing in Postgres stopped a negative spend row. This makes the DATABASE
|
|
9
|
+
-- refuse it, so no code path — present or future, ours or a module's — can
|
|
10
|
+
-- book one.
|
|
11
|
+
--
|
|
12
|
+
-- WHAT IS ADMITTED. A row with delta >= 0 is admitted as before (earning, and the
|
|
13
|
+
-- delta-0 audit rows rank changes and builder exits write). A NEGATIVE row is
|
|
14
|
+
-- admitted only as a REVERSAL of an existing positive row, in one of two shapes:
|
|
15
|
+
--
|
|
16
|
+
-- 1. reason = 'reversal:<credit_log id>' — the general shape. The named row
|
|
17
|
+
-- must exist, belong to the same builder and be positive, and all the
|
|
18
|
+
-- reversals of it together may not exceed it.
|
|
19
|
+
-- 2. reason = '<original reason>_reversal' with task_id set — the shape
|
|
20
|
+
-- core_194 (ADR 0120's one-time clawback, reason 'task.confirmed_reversal')
|
|
21
|
+
-- already wrote. The reference is the (builder, task, original reason)
|
|
22
|
+
-- rows, which must exist and sum positive; reversals may not exceed them.
|
|
23
|
+
-- Kept so that migration, and the integration test that re-runs it, stay
|
|
24
|
+
-- valid on a database that carries this trigger.
|
|
25
|
+
--
|
|
26
|
+
-- Any other negative row is refused. A row whose reason names a spend,
|
|
27
|
+
-- redemption or transfer is refused at any sign: a positive "transfer in" is
|
|
28
|
+
-- the other half of a transfer, and decision 2 forbids the transfer itself.
|
|
29
|
+
--
|
|
30
|
+
-- WHAT IS NOT CHANGED (ADR 0387). No column is added and no existing row is
|
|
31
|
+
-- read, converted or rewritten: the trigger is BEFORE INSERT only, so history —
|
|
32
|
+
-- including core_194's reversal rows — stays exactly as booked. This does not
|
|
33
|
+
-- contradict ADR 0387's "the only schema change is the money record": that
|
|
34
|
+
-- sentence is about the SHAPE of credit_log (no money column, no backfill), and
|
|
35
|
+
-- this adds none. It enforces ADR 0387 decision 1 ("never redeemable") in the
|
|
36
|
+
-- database rather than in the code that writes the ledger.
|
|
37
|
+
--
|
|
38
|
+
-- Concurrency: the reversed row(s) are locked FOR UPDATE before the reversal
|
|
39
|
+
-- total is summed, so two reversals racing for the same row cannot both pass
|
|
40
|
+
-- the "may not exceed" check.
|
|
41
|
+
--
|
|
42
|
+
-- Cost: a positive row (every normal earning) pays one regex test and returns.
|
|
43
|
+
-- Only a negative row reads the ledger, and every read is indexed: the reversed
|
|
44
|
+
-- row by its primary key (shape 1), the original rows by idx_credit_log_task
|
|
45
|
+
-- (core_189, shape 2), and the reversals already booked by the PARTIAL index
|
|
46
|
+
-- below, which holds negative rows only — so it stays as small as the number of
|
|
47
|
+
-- reversals ever made, not the size of the ledger.
|
|
48
|
+
--
|
|
49
|
+
-- The refused names match as whole words: the name, then a separator (. _ : -)
|
|
50
|
+
-- or the end. So 'spend', 'spend.x' and 'transfer:3' are refused, and a reason
|
|
51
|
+
-- that merely starts with the letters ('spender-of-the-month') is not.
|
|
52
|
+
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_credit_log_reversals
|
|
54
|
+
ON credit_log (builder_id, reason) WHERE delta < 0;
|
|
55
|
+
|
|
56
|
+
CREATE OR REPLACE FUNCTION credit_log_refuse_spend() RETURNS trigger AS $$
|
|
57
|
+
DECLARE
|
|
58
|
+
ref_id bigint;
|
|
59
|
+
orig_rec record;
|
|
60
|
+
orig_reason text;
|
|
61
|
+
orig_total bigint;
|
|
62
|
+
reversed bigint;
|
|
63
|
+
BEGIN
|
|
64
|
+
IF NEW.reason ~* '^(redemption|redeem|spend|spent|transfer)([._:-]|$)' THEN
|
|
65
|
+
RAISE EXCEPTION 'credit_log refuses %: credits are never spent, transferred or redeemed (ADR 0385 decision 2)', NEW.reason
|
|
66
|
+
USING ERRCODE = 'check_violation';
|
|
67
|
+
END IF;
|
|
68
|
+
|
|
69
|
+
IF NEW.delta >= 0 THEN
|
|
70
|
+
RETURN NEW;
|
|
71
|
+
END IF;
|
|
72
|
+
|
|
73
|
+
-- Shape 1: 'reversal:<credit_log id>'.
|
|
74
|
+
IF NEW.reason ~ '^reversal:[0-9]{1,18}$' THEN
|
|
75
|
+
ref_id := substring(NEW.reason FROM 10)::bigint;
|
|
76
|
+
SELECT id, builder_id, delta INTO orig_rec FROM credit_log WHERE id = ref_id FOR UPDATE;
|
|
77
|
+
-- Two IFs, not one OR: plpgsql does not promise to short-circuit, and the
|
|
78
|
+
-- fields of a record no row was selected into must not be read.
|
|
79
|
+
IF NOT FOUND THEN
|
|
80
|
+
RAISE EXCEPTION 'credit_log refuses a reversal of row %: no such row', ref_id
|
|
81
|
+
USING ERRCODE = 'check_violation';
|
|
82
|
+
END IF;
|
|
83
|
+
IF orig_rec.builder_id <> NEW.builder_id OR orig_rec.delta <= 0 THEN
|
|
84
|
+
RAISE EXCEPTION 'credit_log refuses a reversal of row %: it must name an existing positive row of the same builder', ref_id
|
|
85
|
+
USING ERRCODE = 'check_violation';
|
|
86
|
+
END IF;
|
|
87
|
+
SELECT COALESCE(SUM(delta), 0) INTO reversed
|
|
88
|
+
FROM credit_log WHERE builder_id = NEW.builder_id AND reason = NEW.reason AND delta < 0;
|
|
89
|
+
IF orig_rec.delta + reversed + NEW.delta < 0 THEN
|
|
90
|
+
RAISE EXCEPTION 'credit_log refuses a reversal of row %: it would reverse more than the row booked', ref_id
|
|
91
|
+
USING ERRCODE = 'check_violation';
|
|
92
|
+
END IF;
|
|
93
|
+
RETURN NEW;
|
|
94
|
+
END IF;
|
|
95
|
+
|
|
96
|
+
-- Shape 2: '<original reason>_reversal' against the same builder and task.
|
|
97
|
+
IF NEW.reason ~ '^[a-z0-9_.]+_reversal$' AND NEW.task_id IS NOT NULL THEN
|
|
98
|
+
orig_reason := left(NEW.reason, length(NEW.reason) - length('_reversal'));
|
|
99
|
+
PERFORM 1 FROM credit_log
|
|
100
|
+
WHERE builder_id = NEW.builder_id AND task_id = NEW.task_id AND reason = orig_reason
|
|
101
|
+
FOR UPDATE;
|
|
102
|
+
SELECT COALESCE(SUM(delta), 0) INTO orig_total
|
|
103
|
+
FROM credit_log WHERE builder_id = NEW.builder_id AND task_id = NEW.task_id AND reason = orig_reason;
|
|
104
|
+
SELECT COALESCE(SUM(delta), 0) INTO reversed
|
|
105
|
+
FROM credit_log WHERE builder_id = NEW.builder_id AND task_id = NEW.task_id AND reason = NEW.reason AND delta < 0;
|
|
106
|
+
IF orig_total <= 0 OR orig_total + reversed + NEW.delta < 0 THEN
|
|
107
|
+
RAISE EXCEPTION 'credit_log refuses %: it must reverse existing % credit on the same task, and no more than was booked', NEW.reason, orig_reason
|
|
108
|
+
USING ERRCODE = 'check_violation';
|
|
109
|
+
END IF;
|
|
110
|
+
RETURN NEW;
|
|
111
|
+
END IF;
|
|
112
|
+
|
|
113
|
+
RAISE EXCEPTION 'credit_log refuses a negative row (reason %): credits are never spent or redeemed; only a reversal of an existing row may be negative (ADR 0385 decision 2)', NEW.reason
|
|
114
|
+
USING ERRCODE = 'check_violation';
|
|
115
|
+
END;
|
|
116
|
+
$$ LANGUAGE plpgsql;
|
|
117
|
+
|
|
118
|
+
DROP TRIGGER IF EXISTS trg_credit_log_refuse_spend ON credit_log;
|
|
119
|
+
CREATE TRIGGER trg_credit_log_refuse_spend
|
|
120
|
+
BEFORE INSERT ON credit_log
|
|
121
|
+
FOR EACH ROW EXECUTE FUNCTION credit_log_refuse_spend();
|
|
122
|
+
|
|
123
|
+
COMMENT ON TABLE credit_log IS
|
|
124
|
+
'Append-only contribution ledger (ADR 0385, ADR 0387). Credits measure work: they are never money, never spent, transferred or redeemed. builders.total_credits is a cache kept by trg_credit_log_apply.';
|
|
125
|
+
COMMENT ON COLUMN credit_log.delta IS
|
|
126
|
+
'Credits earned (positive), an audit row (0), or a reversal of an earlier row (negative). The trigger trg_credit_log_refuse_spend refuses every other negative row.';
|
|
127
|
+
COMMENT ON COLUMN credit_log.reason IS
|
|
128
|
+
'What earned the credits, e.g. task.shipped, session.token_reward, idea.credit.*. A negative row must read reversal:<credit_log id> or <original reason>_reversal with its task_id set.';
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
// (unplaced, shared or removed lines), so /tweak has no line
|
|
19
19
|
// to apply and the round can never move on its own.
|
|
20
20
|
// draft_expired `ready` with a draft and no batch, untouched for
|
|
21
|
-
// page-status's DRAFT_EXPIRES_AFTER (
|
|
22
|
-
//
|
|
21
|
+
// page-status's DRAFT_EXPIRES_AFTER (task 1004901; one day
|
|
22
|
+
// since task 1004976, the owner's answer on task 1004971,
|
|
23
|
+
// with no reminder step first). The draft text stays on
|
|
24
|
+
// the task: no compose, so the description is not touched.
|
|
23
25
|
//
|
|
24
26
|
// All three are abandoned through the lifecycle port's releaseStuckTweak (no compose,
|
|
25
27
|
// so the note is the reason and nothing is written to the description). The
|
|
@@ -38,7 +40,7 @@ const GRACE_MS = Number(process.env.COPY_AUTO_CLOSE_GRACE_MS) || 60 * 60_000;
|
|
|
38
40
|
const NOTES = Object.freeze({
|
|
39
41
|
blank: 'Closed: nothing was written. The round was opened and left blank for a day; claim the page again to start one.',
|
|
40
42
|
nothing_to_apply: 'Closed by itself: every rewritten line went to an engineer task, so nothing was left to apply on the page.',
|
|
41
|
-
draft_expired: 'Closed by itself: the draft sat untouched for
|
|
43
|
+
draft_expired: 'Closed by itself: the draft sat untouched for a day. Its words stay on this task; claim the page again to start over.',
|
|
42
44
|
orphaned_apply: 'Released: the apply stopped without finishing. Nobody holds this round any more, so it is back in the queue.',
|
|
43
45
|
});
|
|
44
46
|
|
|
@@ -311,8 +311,8 @@ function orderedSurfaces(inventory) {
|
|
|
311
311
|
// This is every page with an open round, where it is, since when (the round's
|
|
312
312
|
// last update), and whether it has sat there too long. Waiting for review is
|
|
313
313
|
// the artist's own move and a draft is theirs to finish: a draft or a page
|
|
314
|
-
// being written is stuck after 7 days and a draft expires after
|
|
315
|
-
// 1004901); a review is stuck after 3 days (task 1004777, so its artist is
|
|
314
|
+
// being written is stuck after 7 days, and a draft left untouched expires after
|
|
315
|
+
// one day (task 1004901, shortened by task 1004976); a review is stuck after 3 days (task 1004777, so its artist is
|
|
316
316
|
// told); a state the reads cannot name always needs to be reviewed.
|
|
317
317
|
// ---------------------------------------------------------------------------
|
|
318
318
|
|
|
@@ -322,12 +322,15 @@ const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
322
322
|
// work) and WHEN it is overdue (stuck_after: then it is pushed to that person on
|
|
323
323
|
// Home and in the daily digest). A draft nobody touches also EXPIRES
|
|
324
324
|
// (expires_after): the auto-close sweep abandons it, its words kept on the task.
|
|
325
|
-
|
|
325
|
+
// task 1004976 (owner, task 1004971): a draft expires after ONE day, and there
|
|
326
|
+
// is no separate reminder step at that length, so the draft is due when it
|
|
327
|
+
// expires (stuck_after = expires_after), not reminded a week before.
|
|
328
|
+
const DRAFT_EXPIRES_AFTER = DAY_MS;
|
|
326
329
|
// task 1004972 (owner, task 1004971): a round opened and left blank closes after a day.
|
|
327
330
|
const BLANK_CLOSES_AFTER = DAY_MS;
|
|
328
331
|
const IN_FLIGHT = Object.freeze({
|
|
329
332
|
blank: { word: 'Opened, nothing written yet', stuck_after: BLANK_CLOSES_AFTER, waits_on: 'artist', expires_after: BLANK_CLOSES_AFTER },
|
|
330
|
-
draft: { word: 'Being written', stuck_after:
|
|
333
|
+
draft: { word: 'Being written', stuck_after: DRAFT_EXPIRES_AFTER, waits_on: 'artist', expires_after: DRAFT_EXPIRES_AFTER },
|
|
331
334
|
writing: { word: 'Being written', stuck_after: 7 * DAY_MS, waits_on: 'artist' },
|
|
332
335
|
words_review: { word: 'Words waiting for review', stuck_after: 3 * DAY_MS, waits_on: 'artist' },
|
|
333
336
|
submitted: { word: 'Approved, waiting to be applied', stuck_after: 3 * DAY_MS, waits_on: 'appliers' },
|
|
@@ -33,7 +33,7 @@ const ROUNDS = {
|
|
|
33
33
|
blank: round({ id: '11' }),
|
|
34
34
|
empty_batch: round({ id: '12', description: `${draft}\n\n${batch([])}` }),
|
|
35
35
|
real_batch: round({ id: '13', description: `${draft}\n\n${batch([line])}` }),
|
|
36
|
-
draft_only: round({ id: '14', description: draft }),
|
|
36
|
+
draft_only: round({ id: '14', description: draft, updated_at: new Date(NOW - 12 * 3600_000).toISOString() }),
|
|
37
37
|
held: round({ id: '15', status: 'active', holder_id: '7' }),
|
|
38
38
|
waiting: round({ id: '16', status: 'completed', description: `${draft}\n\n${batch([line])}` }),
|
|
39
39
|
fresh_blank: round({ id: '17', updated_at: new Date(NOW - 5 * 60_000).toISOString() }),
|
|
@@ -56,11 +56,11 @@ await test('a blank round and an empty batch are the only two that close', () =>
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
await test('a draft left untouched for
|
|
59
|
+
await test('a draft left untouched for a day expires; a younger one stays (task 1004901, one day since task 1004976)', () => {
|
|
60
60
|
const D = 86400000;
|
|
61
61
|
const at = (days) => round({ description: draft, updated_at: new Date(NOW - days * D).toISOString() });
|
|
62
|
-
assert.equal(autoClose.closeReason(at(
|
|
63
|
-
assert.equal(autoClose.closeReason(at(
|
|
62
|
+
assert.equal(autoClose.closeReason(at(0.9), { now: NOW }), null);
|
|
63
|
+
assert.equal(autoClose.closeReason(at(1.1), { now: NOW }), 'draft_expired');
|
|
64
64
|
assert.equal(autoClose.closeReason({ ...at(30), holder_id: '7' }, { now: NOW }), null, 'a held draft is being written, never expired');
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Project settings — {{worldName}}</title>
|
|
7
|
-
<meta name="description" content="The fundamentals of this project in one place — how tasks ship, the stale timer, the category set, how credits are calculated, why credits are not equity, and whether a credit-weighted pool can pay out.">
|
|
7
|
+
<meta name="description" content="The fundamentals of this project in one place — how tasks ship, how many tweak rounds a month, the stale timer, the category set, how credits are calculated, why credits are not equity, and whether a credit-weighted pool can pay out.">
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
<section class="scroll" id="ps-body" aria-labelledby="ps-body-h" hidden>
|
|
39
39
|
<h2 class="scroll__h" id="ps-body-h">The fundamentals</h2>
|
|
40
40
|
<p class="scroll__lede">
|
|
41
|
-
|
|
41
|
+
Seven things every project settles. Four of them are knobs you can turn here;
|
|
42
42
|
the other three are decided in committed configuration, and each says which
|
|
43
43
|
file decides it.
|
|
44
44
|
</p>
|
|
45
|
-
<!-- One mount,
|
|
45
|
+
<!-- One mount, seven rows, written by project-settings.js. The rows are NOT
|
|
46
46
|
authored in this file: each carries a value plus the provenance the
|
|
47
47
|
server resolved it through, and markup that could drift from that is the
|
|
48
48
|
thing this page exists to end. -->
|
|
@@ -76,6 +76,6 @@
|
|
|
76
76
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
77
77
|
<script src="/builders/palette.js" defer></script>
|
|
78
78
|
<script src="/builders/hall-kit.js"></script>
|
|
79
|
-
<script src="/builders/project-settings.js?v=2026-10-
|
|
79
|
+
<script src="/builders/project-settings.js?v=2026-10-29-tweak-cap"></script>
|
|
80
80
|
</body>
|
|
81
81
|
</html>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// project-settings.js — the
|
|
1
|
+
// project-settings.js — the seven fundamentals of this project, each with the place
|
|
2
2
|
// its value came from (task 1003280, goal 1000072, criterion legibility-project-settings).
|
|
3
3
|
//
|
|
4
4
|
// WHAT THIS PAGE IS FOR. Goal 1000072's thesis: the project is legible to Claude and
|
|
@@ -148,6 +148,35 @@
|
|
|
148
148
|
</div>`;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// The monthly tweak-round cap (task 1005003): the project's number, then one box
|
|
152
|
+
// per builder rank. The ranks and the bound come from the server; a blank rank box
|
|
153
|
+
// means that rank follows the project's number.
|
|
154
|
+
function tweakCapBodyHtml(row) {
|
|
155
|
+
const v = row.value || {};
|
|
156
|
+
const ranks = Array.isArray(v.ranks) ? v.ranks : [];
|
|
157
|
+
const max = escapeHtml(String(v.max));
|
|
158
|
+
const invalid = row.invalid
|
|
159
|
+
? `<p class="ps-invalid">A configured value could not be used: <code>${escapeHtml(String(row.invalid.raw))}</code>
|
|
160
|
+
— ${escapeHtml(String(row.invalid.reason))}. The limit above is the fallback that is actually in force.</p>`
|
|
161
|
+
: '';
|
|
162
|
+
return `
|
|
163
|
+
${invalid}
|
|
164
|
+
<div class="ps-controls">
|
|
165
|
+
<label for="ps-tweak-cap">Project limit</label>
|
|
166
|
+
<input type="number" id="ps-tweak-cap" min="1" max="${max}" step="1" value="${escapeHtml(String(v.cap))}">
|
|
167
|
+
<span>rounds a month (1–${max})</span>
|
|
168
|
+
</div>
|
|
169
|
+
${ranks.map((r) => `
|
|
170
|
+
<div class="ps-controls">
|
|
171
|
+
<label for="ps-tweak-rank-${escapeHtml(String(r.rank))}">${escapeHtml(String(r.rank))}</label>
|
|
172
|
+
<input type="number" class="ps-tweak-rank" id="ps-tweak-rank-${escapeHtml(String(r.rank))}" data-rank="${escapeHtml(String(r.rank))}"
|
|
173
|
+
min="1" max="${max}" step="1" value="${r.cap == null ? '' : escapeHtml(String(r.cap))}" placeholder="project limit">
|
|
174
|
+
</div>`).join('')}
|
|
175
|
+
<div class="ps-controls">
|
|
176
|
+
<button type="button" class="btn" id="ps-tweak-save">Save</button>
|
|
177
|
+
</div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
151
180
|
function catsBodyHtml(row) {
|
|
152
181
|
const v = row.value || {};
|
|
153
182
|
const cats = Array.isArray(v.categories) ? v.categories : [];
|
|
@@ -177,6 +206,7 @@
|
|
|
177
206
|
const mount = $('ps-rows');
|
|
178
207
|
mount.innerHTML = `<ul class="ov-rows">${rows.map((row) => {
|
|
179
208
|
if (row.key === 'ship_mode' && row.editable) return rowHtml(row, shipBodyHtml(row));
|
|
209
|
+
if (row.key === 'tweak_round_cap' && row.editable) return rowHtml(row, tweakCapBodyHtml(row));
|
|
180
210
|
if (row.key === 'rot_timer' && row.editable) return rowHtml(row, rotBodyHtml(row));
|
|
181
211
|
if (row.key === 'work_categories' && row.editable) return rowHtml(row, catsBodyHtml(row));
|
|
182
212
|
return rowHtml(row, '');
|
|
@@ -210,6 +240,26 @@
|
|
|
210
240
|
});
|
|
211
241
|
}
|
|
212
242
|
|
|
243
|
+
const tweakSave = $('ps-tweak-save');
|
|
244
|
+
if (tweakSave) {
|
|
245
|
+
tweakSave.addEventListener('click', async () => {
|
|
246
|
+
const rankCaps = {};
|
|
247
|
+
document.querySelectorAll('.ps-tweak-rank').forEach((el) => {
|
|
248
|
+
const raw = String(el.value || '').trim();
|
|
249
|
+
rankCaps[el.getAttribute('data-rank')] = raw === '' ? null : Number(raw);
|
|
250
|
+
});
|
|
251
|
+
setBusy(true);
|
|
252
|
+
try {
|
|
253
|
+
const out = await sendJson('PATCH', '/project-settings/tweak-autopickup', { cap: Number($('ps-tweak-cap').value), rank_caps: rankCaps });
|
|
254
|
+
toast(`Monthly tweak rounds: ${out.cap} for the project.`);
|
|
255
|
+
await load();
|
|
256
|
+
} catch (err) {
|
|
257
|
+
toast(err.message || 'Could not save the limit.');
|
|
258
|
+
setBusy(false);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
213
263
|
const rotSave = $('ps-rot-save');
|
|
214
264
|
if (rotSave) {
|
|
215
265
|
rotSave.addEventListener('click', async () => {
|