openclacky 1.1.4 → 1.1.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/lib/clacky/server/http_server.rb +1 -19
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/brand.js +36 -29
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9c90dd169535f465bc4f41ff976ed6c77a66e1bfce5a552a286a6e2d77ef89c4
|
|
4
|
+
data.tar.gz: f9b44af17510f9e51844e7be9ccf32b106a385af727d6711f89bf14364a6f9a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52e1c244505b21f4b7e8d6b8dd6adf2a87fe87094df5742c2d24772cff0fdb0f3924ec8f23c006f428cd86b8075f5f35f4b49b708b9959a351bfaa1f924130e1
|
|
7
|
+
data.tar.gz: 72bd1fb0568f833c8102fbc4d0c2ac6dd10ce6cd0de06ede155fabececd395c51cb13e22b52a114b121a62f32cc5e0243b8144785d4d8e96b886e3bea5e48448
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.5] - 2026-05-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Async free skills handling
|
|
12
|
+
|
|
8
13
|
## [1.1.4] - 2026-05-22
|
|
9
14
|
|
|
10
15
|
### Added
|
|
@@ -803,30 +803,12 @@ module Clacky
|
|
|
803
803
|
refresh_pending = true
|
|
804
804
|
end
|
|
805
805
|
|
|
806
|
-
# Free-mode counts: synchronous fetch is acceptable here because
|
|
807
|
-
# this endpoint is polled lazily and the platform call is cached
|
|
808
|
-
# via http keep-alive. On error we just return zero counts and the
|
|
809
|
-
# banner falls back to the legacy "not activated" message.
|
|
810
|
-
free_count = 0
|
|
811
|
-
paid_count = 0
|
|
812
|
-
begin
|
|
813
|
-
result = brand.fetch_free_skills!
|
|
814
|
-
if result[:success]
|
|
815
|
-
free_count = result[:skills].size
|
|
816
|
-
paid_count = result[:paid_skills_count].to_i
|
|
817
|
-
end
|
|
818
|
-
rescue StandardError
|
|
819
|
-
# Network errors are non-fatal here.
|
|
820
|
-
end
|
|
821
|
-
|
|
822
806
|
json_response(res, 200, {
|
|
823
807
|
branded: true,
|
|
824
808
|
needs_activation: true,
|
|
825
809
|
product_name: brand.product_name,
|
|
826
810
|
test_mode: @brand_test,
|
|
827
|
-
distribution_refresh_pending: refresh_pending
|
|
828
|
-
free_skills_count: free_count,
|
|
829
|
-
paid_skills_count: paid_count
|
|
811
|
+
distribution_refresh_pending: refresh_pending
|
|
830
812
|
})
|
|
831
813
|
return
|
|
832
814
|
end
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/brand.js
CHANGED
|
@@ -37,14 +37,7 @@ const Brand = (() => {
|
|
|
37
37
|
// so no DOM update is needed here on boot.
|
|
38
38
|
|
|
39
39
|
if (data.needs_activation) {
|
|
40
|
-
|
|
41
|
-
// Boot continues normally; user can activate at any time via the banner.
|
|
42
|
-
_showActivationBanner(data.product_name, data.free_skills_count, data.paid_skills_count);
|
|
43
|
-
|
|
44
|
-
// Apply logo/theme from whatever is already cached in brand.yml —
|
|
45
|
-
// install.sh only writes product_name + package_name, but if a
|
|
46
|
-
// previous session already pulled the public distribution info,
|
|
47
|
-
// we can light up the full brand visuals right now.
|
|
40
|
+
_showActivationBanner(data.product_name);
|
|
48
41
|
_applyHeaderLogo();
|
|
49
42
|
|
|
50
43
|
// Backend just kicked off an async refresh of the public distribution
|
|
@@ -75,9 +68,9 @@ const Brand = (() => {
|
|
|
75
68
|
// ── Internal ───────────────────────────────────────────────────────────────
|
|
76
69
|
|
|
77
70
|
// Show a dismissible activation banner at the top of the page.
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
function _showActivationBanner(brandName
|
|
71
|
+
// Renders immediately with a generic prompt, then asynchronously fetches
|
|
72
|
+
// the free/paid skill counts and refines the copy.
|
|
73
|
+
function _showActivationBanner(brandName) {
|
|
81
74
|
const existing = document.getElementById("brand-activation-banner");
|
|
82
75
|
if (existing) return;
|
|
83
76
|
|
|
@@ -85,21 +78,12 @@ const Brand = (() => {
|
|
|
85
78
|
bar.id = "brand-activation-banner";
|
|
86
79
|
bar.className = "brand-activation-banner";
|
|
87
80
|
|
|
88
|
-
const span = document.createElement("span");
|
|
89
81
|
const name = brandName || I18n.t("brand.banner.defaultName");
|
|
90
|
-
const free = Number(freeCount) || 0;
|
|
91
|
-
const paid = Number(paidCount) || 0;
|
|
92
|
-
|
|
93
|
-
let i18nKey;
|
|
94
|
-
if (free > 0 && paid > 0) i18nKey = "brand.banner.freePromptBoth";
|
|
95
|
-
else if (free > 0 && paid === 0) i18nKey = "brand.banner.freePromptOnlyFree";
|
|
96
|
-
else if (free === 0 && paid > 0) i18nKey = "brand.banner.freePromptOnlyPaid";
|
|
97
|
-
else i18nKey = "brand.banner.prompt";
|
|
98
82
|
|
|
99
|
-
const
|
|
100
|
-
span.textContent = I18n.t(
|
|
101
|
-
span.setAttribute("data-i18n",
|
|
102
|
-
span.setAttribute("data-i18n-vars", `name=${name}
|
|
83
|
+
const span = document.createElement("span");
|
|
84
|
+
span.textContent = I18n.t("brand.banner.prompt", { name });
|
|
85
|
+
span.setAttribute("data-i18n", "brand.banner.prompt");
|
|
86
|
+
span.setAttribute("data-i18n-vars", `name=${name}`);
|
|
103
87
|
|
|
104
88
|
const link = document.createElement("button");
|
|
105
89
|
link.className = "brand-activation-banner-link";
|
|
@@ -107,11 +91,6 @@ const Brand = (() => {
|
|
|
107
91
|
link.setAttribute("data-i18n", "brand.banner.action");
|
|
108
92
|
link.addEventListener("click", () => _goToLicenseInput());
|
|
109
93
|
|
|
110
|
-
// Hide the "Activate Now" button when there is nothing premium to unlock.
|
|
111
|
-
if (paid === 0 && free > 0) {
|
|
112
|
-
link.style.display = "none";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
94
|
const closeBtn = document.createElement("button");
|
|
116
95
|
closeBtn.className = "brand-activation-banner-close";
|
|
117
96
|
closeBtn.innerHTML = "✕";
|
|
@@ -121,6 +100,34 @@ const Brand = (() => {
|
|
|
121
100
|
bar.appendChild(link);
|
|
122
101
|
bar.appendChild(closeBtn);
|
|
123
102
|
document.getElementById("main").prepend(bar);
|
|
103
|
+
|
|
104
|
+
_refineBannerWithCounts(name, span, link);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function _refineBannerWithCounts(name, span, link) {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetch("/api/brand/skills");
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
if (!data.ok || !data.free_mode) return;
|
|
112
|
+
|
|
113
|
+
const free = (data.skills || []).length;
|
|
114
|
+
const paid = Number(data.paid_skills_count) || 0;
|
|
115
|
+
|
|
116
|
+
let i18nKey;
|
|
117
|
+
if (free > 0 && paid > 0) i18nKey = "brand.banner.freePromptBoth";
|
|
118
|
+
else if (free > 0 && paid === 0) i18nKey = "brand.banner.freePromptOnlyFree";
|
|
119
|
+
else if (free === 0 && paid > 0) i18nKey = "brand.banner.freePromptOnlyPaid";
|
|
120
|
+
else return;
|
|
121
|
+
|
|
122
|
+
const vars = { name, free, paid, freePlural: free === 1 ? "" : "s", paidPlural: paid === 1 ? "" : "s" };
|
|
123
|
+
span.textContent = I18n.t(i18nKey, vars);
|
|
124
|
+
span.setAttribute("data-i18n", i18nKey);
|
|
125
|
+
span.setAttribute("data-i18n-vars", `name=${name};free=${free};paid=${paid};freePlural=${vars.freePlural};paidPlural=${vars.paidPlural}`);
|
|
126
|
+
|
|
127
|
+
if (paid === 0 && free > 0) link.style.display = "none";
|
|
128
|
+
} catch (_) {
|
|
129
|
+
// Network failure: leave the generic prompt in place.
|
|
130
|
+
}
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
// Navigate to Settings, scroll to Brand & License section, flash it, then focus the input.
|