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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +10 -2
- data/lib/slk/commands/deactivations.rb +41 -2
- data/lib/slk/services/deactivation_scanner.rb +36 -5
- data/lib/slk/version.rb +1 -1
- 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: 663f35d4a747ac42d0fb9596caa7dd7b4809d7f3451b811b7fb7e8389c7a991c
|
|
4
|
+
data.tar.gz: 42e1ba0fda08b83c644b77212155ab2c68ac318d95f4966060624f5f0eb94827
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = '
|
|
14
|
+
CACHE_KEY = 'deactivations_v2'
|
|
15
15
|
DEFAULT_TTL = 21_600 # 6 hours
|
|
16
|
-
REQUIRED_KEYS = %w[
|
|
17
|
-
|
|
18
|
-
|
|
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' =>
|
|
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