slk 0.11.1 → 0.11.2

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: 6377478c56d25efc2b234bcd4ab2118bbe394520ff4f48aed0da0db5bb6c2cea
4
- data.tar.gz: 869096e0ca163dc27042e199e5b2f4f44c81cbdc176ad00ed8546b59cdac7553
3
+ metadata.gz: 663f35d4a747ac42d0fb9596caa7dd7b4809d7f3451b811b7fb7e8389c7a991c
4
+ data.tar.gz: 42e1ba0fda08b83c644b77212155ab2c68ac318d95f4966060624f5f0eb94827
5
5
  SHA512:
6
- metadata.gz: c5ce6c57a9978ad2152a3267e9892274883a0980ee33f2648fe7ca14718f870ea3a0a7ecfa2420abffdea18b565c500bfda1524f2efc29bb73a35eaa9de56318
7
- data.tar.gz: ffb2bd3791481c90223a9050760769868194e8044ffc0750704965180b5dfb750cdad1dd4a297f17e1688fc7e9e41c07a231f0b87d85e0af485a2c221e518f2b
6
+ metadata.gz: bd98ff12ddd5d0dfcf0ebc33e72852bfffed272c4136b74ed4b63b41ef38c0aab01b28826a02069738556781bec325a63b73eab66407f72c099de9186a6b56c2
7
+ data.tar.gz: 92a34fac64238598233edf4b8273d4597bb80deb07b316504649e21da575b4f56e75969a5178f2035c5fbaf5f0fa2ac70fe1eeb2e89d5baa3d6054285991a59a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.2] - 2026-09-24
11
+
12
+ ### Changed
13
+
14
+ - `slk deactivations` now breaks the active-account total into full members, multi-channel guests, and single-channel guests, excluding bots and deactivated accounts. The summary labels the total as Slack accounts rather than company members, and `--json` adds the same breakdown while retaining `active_members`. An older roster cache is re-fetched so the new counts cannot appear as zeros.
15
+
10
16
  ## [0.11.1] - 2026-09-23
11
17
 
12
18
  ### Fixed
data/README.md CHANGED
@@ -225,7 +225,8 @@ slk deactivations --json # Machine-readable output
225
225
  ```
226
226
 
227
227
  ```
228
- acme: 16 deactivated since 30d (664 active members)
228
+ acme: 16 deactivated since 30d (664 active accounts)
229
+ 360 full members · 120 multi-channel guests · 184 single-channel guests
229
230
 
230
231
  2026-09-14 Dana Whitfield Platform Support
231
232
  2026-09-11 Priya Raghunathan UX Researcher
@@ -239,6 +240,12 @@ for that user. For a deactivated account that change is almost always the
239
240
  deactivation, but an admin editing a departed profile afterwards moves the date
240
241
  forward, so treat it as "last touched" rather than a payroll record.
241
242
 
243
+ The active-account count excludes bots and deactivated accounts; full members and
244
+ both types of Slack guests are shown separately. It is not an employee headcount.
245
+ The same breakdown is available in `--json` as `active_full_members`,
246
+ `active_multi_channel_guests`, and `active_single_channel_guests` (alongside the
247
+ existing `active_members` total).
248
+
242
249
  The roster costs one API call per 1000 members, so the result is cached for six
243
250
  hours; `--refresh` re-fetches it.
244
251
 
@@ -248,7 +255,8 @@ hours; `--refresh` re-fetches it.
248
255
  "Start Date" profile field:
249
256
 
250
257
  ```
251
- acme: 3 deactivated since 30d (664 active members)
258
+ acme: 3 deactivated since 30d (664 active accounts)
259
+ 360 full members · 120 multi-channel guests · 184 single-channel guests
252
260
 
253
261
  2026-09-14 Dana Whitfield 1mo Platform Support
254
262
  2026-09-11 Priya Raghunathan 2y 7mo UX Researcher
@@ -309,6 +309,7 @@ module Slk
309
309
  def render(workspace, report, records)
310
310
  formatter = Formatters::DeactivationFormatter.new(output: output, width: @options[:width])
311
311
  formatter.summary(summary_line(workspace, report, records))
312
+ render_account_breakdown(formatter, report)
312
313
  render_body(formatter, workspace, records)
313
314
  footer = footer(report, records)
314
315
  return if footer.empty?
@@ -340,7 +341,36 @@ module Slk
340
341
 
341
342
  def summary_line(workspace, report, records)
342
343
  "#{workspace.name}: #{records.size} #{scope_phrase} " \
343
- "(#{report.active_count} active members)"
344
+ "(#{count_label(report.active_count, 'active account')})"
345
+ end
346
+
347
+ # Keep each guest type visible without overflowing a narrow terminal.
348
+ # rubocop:disable Metrics/MethodLength
349
+ def render_account_breakdown(formatter, report)
350
+ line = ' '
351
+ account_parts(report).each do |part|
352
+ combined = line == ' ' ? "#{line}#{part}" : "#{line} · #{part}"
353
+ if @options[:width] && combined.length > @options[:width] && line != ' '
354
+ formatter.note(line)
355
+ line = " #{part}"
356
+ else
357
+ line = combined
358
+ end
359
+ end
360
+ formatter.note(line)
361
+ end
362
+ # rubocop:enable Metrics/MethodLength
363
+
364
+ def account_parts(report)
365
+ [
366
+ count_label(report.full_member_count, 'full member'),
367
+ count_label(report.multi_channel_guest_count, 'multi-channel guest'),
368
+ count_label(report.single_channel_guest_count, 'single-channel guest')
369
+ ]
370
+ end
371
+
372
+ def count_label(count, singular)
373
+ "#{count} #{singular}#{'s' unless count == 1}"
344
374
  end
345
375
 
346
376
  def scope_phrase
@@ -390,7 +420,7 @@ module Slk
390
420
  {
391
421
  workspace: workspace.name,
392
422
  fetched_at: report.fetched_at,
393
- active_members: report.active_count,
423
+ **active_counts_json(report),
394
424
  accounts_ever: report.human_count,
395
425
  total_deactivated: total_deactivated(report),
396
426
  includes_bots: @options[:bots] ? true : false,
@@ -399,6 +429,15 @@ module Slk
399
429
  }
400
430
  end
401
431
 
432
+ def active_counts_json(report)
433
+ {
434
+ active_members: report.active_count,
435
+ active_full_members: report.full_member_count,
436
+ active_multi_channel_guests: report.multi_channel_guest_count,
437
+ active_single_channel_guests: report.single_channel_guest_count
438
+ }
439
+ end
440
+
402
441
  # started_on and tenure_months appear only when they were asked for:
403
442
  # a null that means "not looked up" is indistinguishable from one that
404
443
  # means "nobody filled it in".
@@ -11,11 +11,17 @@ module Slk
11
11
  # (deactivated members plus headcounts, not the whole roster) is cached in
12
12
  # the workspace meta cache with a TTL.
13
13
  class DeactivationScanner
14
- CACHE_KEY = 'deactivations_v1'
14
+ CACHE_KEY = 'deactivations_v2'
15
15
  DEFAULT_TTL = 21_600 # 6 hours
16
- REQUIRED_KEYS = %w[fetched_at member_count human_count active_count records].freeze
17
-
18
- Report = Data.define(:records, :member_count, :human_count, :active_count, :fetched_at) do
16
+ REQUIRED_KEYS = %w[
17
+ fetched_at member_count human_count active_count full_member_count
18
+ multi_channel_guest_count single_channel_guest_count records
19
+ ].freeze
20
+
21
+ Report = Data.define(
22
+ :records, :member_count, :human_count, :active_count, :full_member_count,
23
+ :multi_channel_guest_count, :single_channel_guest_count, :fetched_at
24
+ ) do
19
25
  def deactivated_count = records.size
20
26
 
21
27
  def bots = records.count(&:bot)
@@ -70,30 +76,55 @@ module Slk
70
76
 
71
77
  def counts(members)
72
78
  humans = members.reject { |m| Models::Deactivation.bot?(m) }
79
+ active = humans.reject { |m| m['deleted'] }
73
80
 
74
81
  {
75
82
  'member_count' => members.size,
76
83
  'human_count' => humans.size,
77
- 'active_count' => humans.count { |m| !m['deleted'] }
84
+ 'active_count' => active.size
85
+ }.merge(guest_counts(active))
86
+ end
87
+
88
+ def guest_counts(active)
89
+ groups = active.group_by { |member| guest_type(member) }
90
+ {
91
+ 'full_member_count' => groups.fetch(:full, []).size,
92
+ 'multi_channel_guest_count' => groups.fetch(:multi, []).size,
93
+ 'single_channel_guest_count' => groups.fetch(:single, []).size
78
94
  }
79
95
  end
80
96
 
97
+ def guest_type(member)
98
+ return :single if member['is_ultra_restricted']
99
+ return :multi if member['is_restricted']
100
+
101
+ :full
102
+ end
103
+
81
104
  def fetch_members
82
105
  @users_api.list_all do |total|
83
106
  @on_debug&.call("users.list: #{total} members fetched")
84
107
  end
85
108
  end
86
109
 
110
+ # Keep the cache fields explicit: a missing headcount must not become a
111
+ # plausible-looking zero without usable? first checking the cache shape.
112
+ # rubocop:disable Metrics/AbcSize
87
113
  def build_report(data)
88
114
  Report.new(
89
115
  records: sorted_records(data['records'] || []),
90
116
  member_count: data['member_count'].to_i,
91
117
  human_count: data['human_count'].to_i,
92
118
  active_count: data['active_count'].to_i,
119
+ full_member_count: data['full_member_count'].to_i,
120
+ multi_channel_guest_count: data['multi_channel_guest_count'].to_i,
121
+ single_channel_guest_count: data['single_channel_guest_count'].to_i,
93
122
  fetched_at: data['fetched_at']&.to_i
94
123
  )
95
124
  end
96
125
 
126
+ # rubocop:enable Metrics/AbcSize
127
+
97
128
  # Newest first; accounts with no usable timestamp sort to the bottom
98
129
  # rather than pretending to be from 1970.
99
130
  def sorted_records(raw)
data/lib/slk/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slk
4
- VERSION = '0.11.1'
4
+ VERSION = '0.11.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Boehs