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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4107ac9d894f9af9647f19152855f13854d4cfebd8013d2d22057ca98de87274
4
- data.tar.gz: 3c86e0b865cccc27f8c785f39c49384744ad4e6cce6cdf257ca0485f1d62c260
3
+ metadata.gz: 9c90dd169535f465bc4f41ff976ed6c77a66e1bfce5a552a286a6e2d77ef89c4
4
+ data.tar.gz: f9b44af17510f9e51844e7be9ccf32b106a385af727d6711f89bf14364a6f9a7
5
5
  SHA512:
6
- metadata.gz: e6d66fd62f6434c05420f5f639a412fb953e49634c3f0740f155a2d20b70d91acb12a9844a61330f0761d88ff2c284a1d071f0590b3e53553bd11d57a035669f
7
- data.tar.gz: c64f138d54a5397ba0a49b6878b79576f12b6fc2dec05f7e4b24b5eaf3490060743a0e3a20da17b6cf47476a85eaa093742b616b5be9ab65859db6e8db035404
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.1.4"
4
+ VERSION = "1.1.5"
5
5
  end
@@ -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
- // Show a top banner instead of a blocking full-screen panel.
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
- // Clicking the banner creates a dedicated session and invokes the
79
- // Clicking the banner opens Settings and focuses the license key input directly.
80
- function _showActivationBanner(brandName, freeCount, paidCount) {
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 vars = { name, free, paid, freePlural: free === 1 ? "" : "s", paidPlural: paid === 1 ? "" : "s" };
100
- span.textContent = I18n.t(i18nKey, vars);
101
- span.setAttribute("data-i18n", i18nKey);
102
- span.setAttribute("data-i18n-vars", `name=${name};free=${free};paid=${paid};freePlural=${vars.freePlural};paidPlural=${vars.paidPlural}`);
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.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy