fewald-worklog 0 → 0.1.1

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: dc96d7e3c9fa2cb64c09f04b13683f64dbce8b993d16e0529df92fddff947901
4
- data.tar.gz: 0d4de596547f66de795f7de9ec979924b18d19bac9e5b4722f755306db85f8fd
3
+ metadata.gz: 95211b380e35341b87a230655872177aa6bcb4263fcdbc0b0a590bdc357346d3
4
+ data.tar.gz: d64857404d738154ee5b5b7bd717eea2d862bad294cede2946b1debf08f6940d
5
5
  SHA512:
6
- metadata.gz: a48499cd05d20d472b4126e18b588114740c26a18cb7b9fe6eb10aaabff231654406a70cf467c3a5d175165b8b6a7c81749bed376a8f0ffebbf97d43e50a1e94
7
- data.tar.gz: ef081d883a7f2ed48e7513690a2844235851cf90bd03792d49e74f0b574b42161e1f674a37fadded9a37c66b6f938719dfdda4666a94a7066b11de17da5b18ce
6
+ metadata.gz: 27f40f229dc637b3985793a8b2bcb5858ac3883cc739a5bc56c77919e65d3a6580b7deac2e1f5825e9165da0035754f3b15d68a22e4971bf460b0c51be4048bc
7
+ data.tar.gz: 4292212bb151fe886c8996b8538144b7a76427e5518783d62bc40616a3c969fe398edbfbaa2fcfbc042c76785f11fba1ad186670a378bcccbe2f45264e3e7e23
data/bin/wl CHANGED
@@ -6,11 +6,9 @@
6
6
  if ENV['WL_PATH']
7
7
  # Import the worklog CLI from the path specified in the WL_PATH environment variable
8
8
  # This is used during development to avoid having to rely on the order of the $PATH.
9
- puts "Loading worklog from #{ENV['WL_PATH']}. This should only be used during development."
10
- puts 'To use the installed worklog, unset the WL_PATH environment variable.'
11
- require_relative File.join(ENV['WL_PATH'], 'lib', 'cli')
9
+ require_relative File.join(ENV['WL_PATH'], 'worklog', 'cli')
12
10
  else
13
- require_relative '../lib/cli'
11
+ require_relative '../worklog/cli'
14
12
  end
15
13
 
16
14
  WorklogCLI.start
data/worklog/cli.rb ADDED
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Add the current directory to the load path
5
+ curr_dir = File.expand_path(__dir__)
6
+ $LOAD_PATH << curr_dir unless $LOAD_PATH.include?(curr_dir)
7
+
8
+ require 'thor'
9
+ require 'date'
10
+ require 'logger'
11
+
12
+ require 'date_parser'
13
+ require 'printer'
14
+ require 'statistics'
15
+ require 'storage'
16
+ require 'webserver'
17
+ require 'worklog'
18
+ require_relative 'summary'
19
+ require_relative 'editor'
20
+ require_relative 'string_helper'
21
+
22
+ # CLI for the work log application
23
+ class WorklogCLI < Thor
24
+ include StringHelper
25
+ class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
26
+
27
+ package_name 'Worklog'
28
+
29
+ def self.exit_on_failure?
30
+ true
31
+ end
32
+
33
+ desc 'add MESSAGE', 'Add a new entry to the work log, defaults to the current date.'
34
+ long_desc <<~LONGDESC
35
+ Add a new entry with the current date and time to the work log.
36
+ The message is required and must be enclosed in quotes if it contains more than one word.
37
+
38
+ People can be referenced either by using the tilde "~" or the at symbol "@", followed by
39
+ an alphanumeric string.
40
+ LONGDESC
41
+ option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d'), desc: 'Set the date of the entry'
42
+ option :time, type: :string, default: DateTime.now.strftime('%H:%M:%S'), desc: 'Set the time of the entry'
43
+ option :tags, type: :array, default: [], desc: 'Add tags to the entry'
44
+ option :ticket, type: :string, desc: 'Ticket number associated with the entry. Can be any alphanumeric string.'
45
+ option :url, type: :string, desc: 'URL to associate with the entry'
46
+ option :epic, type: :boolean, default: false, desc: 'Mark the entry as an epic'
47
+ def add(message)
48
+ set_log_level
49
+
50
+ # Remove leading and trailing whitespaces
51
+ # Raise an error if the message is empty
52
+ message = message.strip
53
+ raise ArgumentError, 'Message cannot be empty' if message.empty?
54
+
55
+ date = Date.strptime(options[:date], '%Y-%m-%d')
56
+ time = Time.strptime(options[:time], '%H:%M:%S')
57
+ Storage.create_file_skeleton(date)
58
+
59
+ daily_log = Storage.load_log(Storage.filepath(date))
60
+ daily_log.entries << LogEntry.new(time:, tags: options[:tags], ticket: options[:ticket], url: options[:url],
61
+ epic: options[:epic], message:)
62
+
63
+ # Sort by time in case an entry was added later out of order.
64
+ daily_log.entries.sort_by!(&:time)
65
+
66
+ Storage.write_log(Storage.filepath(options[:date]), daily_log)
67
+ WorkLogger.info Rainbow("Added to the work log for #{options[:date]}").green
68
+ end
69
+
70
+ desc 'edit', 'Edit a specified day in the work log'
71
+ option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
72
+ def edit
73
+ set_log_level
74
+
75
+ date = Date.strptime(options[:date], '%Y-%m-%d')
76
+
77
+ # Load existing log
78
+ log = Storage.load_log(Storage.filepath(date))
79
+ unless log
80
+ WorkLogger.error "No work log found for #{options[:date]}. Aborting."
81
+ exit 1
82
+ end
83
+
84
+ txt = Editor::EDITOR_PREAMBLE.result_with_hash(content: YAML.dump(log))
85
+ return_val = Editor.open_editor(txt)
86
+
87
+ Storage.write_log(Storage.filepath(date),
88
+ YAML.load(return_val, permitted_classes: [Date, Time, DailyLog, LogEntry]))
89
+ WorkLogger.info Rainbow("Updated work log for #{options[:date]}").green
90
+ end
91
+
92
+ desc 'remove', 'Remove last entry from the log'
93
+ option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
94
+ def remove
95
+ set_log_level
96
+
97
+ date = Date.strptime(options[:date], '%Y-%m-%d')
98
+ unless File.exist?(Storage.filepath(date))
99
+ WorkLogger.error Rainbow("No work log found for #{options[:date]}. Aborting.").red
100
+ exit 1
101
+ end
102
+
103
+ daily_log = Storage.load_log(Storage.filepath(options[:date]))
104
+ if daily_log.entries.empty?
105
+ WorkLogger.error Rainbow("No entries found for #{options[:date]}. Aborting.").red
106
+ exit 1
107
+ end
108
+
109
+ removed_entry = daily_log.entries.pop
110
+ Storage.write_log(Storage.filepath(date), daily_log)
111
+ WorkLogger.info Rainbow("Removed entry: #{removed_entry.message}").green
112
+ end
113
+
114
+ desc 'show', 'Show the work log for a specific date or a range of dates. Defaults to todays date.'
115
+ long_desc <<~LONGDESC
116
+ Show the work log for a specific date or a range of dates. As a default, all items from the current day will be shown.
117
+ LONGDESC
118
+ option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d'),
119
+ desc: <<~DESC
120
+ Show the work log for a specific date. If this option is provided, --from and --to and --days should not be used.
121
+ DESC
122
+ option :from, type: :string, desc: <<~EOF
123
+ Inclusive start date of the range. Takes precedence over --date, if defined.
124
+ EOF
125
+ option :to, type: :string, desc: <<~EOF
126
+ Inclusive end date of the range. Takes precedence over --date, if defined.
127
+ EOF
128
+ option :days, type: :numeric, desc: <<~EOF
129
+ Number of days to show starting from --date. Takes precedence over --from and --to if defined.
130
+ EOF
131
+ option :epics_only, type: :boolean, default: false, desc: 'Show only entries that are marked as epic'
132
+ option :tags, type: :array, default: [], desc: 'Filter entries by tags. Tags are treated as an OR condition.'
133
+ def show
134
+ set_log_level
135
+
136
+ start_date, end_date = start_end_date(options)
137
+
138
+ entries = Storage.days_between(start_date, end_date)
139
+ if entries.empty?
140
+ Printer.no_entries(start_date, end_date)
141
+ else
142
+ entries.each do |entry|
143
+ Printer.print_day(entry, entries.size > 1, options[:epics_only])
144
+ end
145
+ end
146
+ end
147
+
148
+ desc 'people', 'Show all people mentioned in the work log'
149
+ def people
150
+ set_log_level
151
+
152
+ puts 'People mentioned in the work log:'
153
+
154
+ mentions = {}
155
+ all_logs = Storage.all_days
156
+ all_logs.map(&:people).each do |people|
157
+ mentions.merge!(people) { |_key, oldval, newval| oldval + newval }
158
+ end
159
+ mentions.each { |k, v| puts "#{Rainbow(k).gold}: #{v} #{pluralize(v, 'occurrence')}" }
160
+ end
161
+
162
+ desc 'tags', 'Show all tags used in the work log'
163
+ def tags
164
+ set_log_level
165
+
166
+ all_logs = Storage.all_days
167
+
168
+ puts Rainbow('Tags used in the work log:').gold
169
+
170
+ # Count all tags used in the work log
171
+ tags = all_logs.map(&:entries).flatten.map(&:tags).flatten.compact.tally
172
+
173
+ # Determine length of longest tag for formatting
174
+ max_len = tags.keys.map(&:length).max
175
+
176
+ tags.each { |k, v| puts "#{Rainbow(k.ljust(max_len)).gold}: #{v} #{pluralize(v, 'occurrence')}" }
177
+ end
178
+
179
+ desc 'server', 'Start the work log server'
180
+ def server
181
+ set_log_level
182
+
183
+ WorkLogServer.new.start
184
+ end
185
+
186
+ desc 'stats', 'Show statistics for the work log'
187
+ def stats
188
+ stats = Statistics.calculate
189
+ puts "#{format_left('Total days')}: #{stats.total_days}"
190
+ puts "#{format_left('Total entries')}: #{stats.total_entries}"
191
+ puts "#{format_left('Total epics')}: #{stats.total_epics}"
192
+ puts "#{format_left('Entries per day')}: #{'%.2f' % stats.avg_entries}"
193
+ puts "#{format_left('First entry')}: #{stats.first_entry}"
194
+ puts "#{format_left('Last entry')}: #{stats.last_entry}"
195
+ end
196
+
197
+ desc 'summary', 'Generate a summary of the work log entries'
198
+ option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
199
+ option :from, type: :string, desc: <<-EOF
200
+ 'Inclusive start date of the range. Takes precedence over --date if defined.'
201
+ EOF
202
+ option :to, type: :string, desc: <<-EOF
203
+ 'Inclusive end date of the range. Takes precedence over --date if defined.'
204
+ EOF
205
+ option :days, type: :numeric, desc: <<-EOF
206
+ 'Number of days to show starting from --date. Takes precedence over --from and --to if defined.'
207
+ EOF
208
+ def summary
209
+ set_log_level
210
+
211
+ start_date, end_date = start_end_date(options)
212
+ entries = Storage.days_between(start_date, end_date).map(&:entries).flatten
213
+
214
+ # Do nothing if no entries are found.
215
+ if entries.empty?
216
+ Printer.no_entries(start_date, end_date)
217
+ return
218
+ end
219
+ puts Summary.generate_summary(entries)
220
+ end
221
+
222
+ # Define shortcuts and aliases
223
+ map 'a' => :add
224
+ map 'statistics' => :stats
225
+ map 'serve' => :server
226
+
227
+ no_commands do
228
+ def set_log_level
229
+ # Set the log level based on the verbose option
230
+ WorkLogger.level = options[:verbose] ? Logger::Severity::DEBUG : Logger::Severity::INFO
231
+ end
232
+
233
+ def format_left(string)
234
+ # Format a string to be left-aligned in a fixed-width field
235
+ #
236
+ # @param string [String] the string to format
237
+ # @return [String] the formatted string
238
+ format('%18s', string)
239
+ end
240
+
241
+ # Parse the start and end date based on the options provided
242
+ #
243
+ # @param options [Hash] the options hash
244
+ # @return [Array] the start and end date as an array
245
+ def start_end_date(options)
246
+ if options[:days]
247
+ # Safeguard against negative days
248
+ raise ArgumentError, 'Number of days cannot be negative' if options[:days].negative?
249
+
250
+ start_date = Date.today - options[:days]
251
+ end_date = Date.today
252
+ elsif options[:from]
253
+ start_date = DateParser.parse_date_string!(options[:from], true)
254
+ end_date = DateParser.parse_date_string!(options[:to], false) if options[:to]
255
+ else
256
+ start_date = Date.strptime(options[:date], '%Y-%m-%d')
257
+ end_date = start_date
258
+ end
259
+ [start_date, end_date]
260
+ end
261
+ end
262
+ end
263
+
264
+ # Start the CLI if the file is executed
265
+ # This prevents the CLI from starting when the file is required in another file,
266
+ # which is useful for testing.
267
+ WorklogCLI.start if __FILE__ == $PROGRAM_NAME
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'hash'
3
+ require_relative 'hash'
4
4
 
5
- # DailyLog is a container for a day's work log.
6
5
  class DailyLog
7
6
  # Container for a day's work log.
8
7
  include Hashify
@@ -2,13 +2,11 @@
2
2
 
3
3
  require 'yaml'
4
4
  require 'rainbow'
5
- require 'daily_log'
6
- require 'hash'
5
+ require_relative 'daily_log'
6
+ require_relative 'hash'
7
7
 
8
8
  # A single log entry.
9
9
  class LogEntry
10
- PERSON_REGEX = /\s[~@](\w+)/
11
-
12
10
  include Hashify
13
11
 
14
12
  # Represents a single entry in the work log.
@@ -31,27 +29,13 @@ class LogEntry
31
29
  end
32
30
 
33
31
  # Returns the message string with formatting without the time.
34
- # @param people Hash[String, Person] A hash of people with their handles as keys.
35
- def message_string(known_people = nil)
36
- # replace all mentions of people with their names.
37
- msg = @message.dup
38
- people.each do |person|
39
- next unless known_people && known_people[person]
40
-
41
- msg.gsub!(/[~@]#{person}/) do |match|
42
- s = ''
43
- s += ' ' if match[0] == ' '
44
- s += "#{Rainbow(known_people[person].name).underline} (~#{person})" if known_people && known_people[person]
45
- s
46
- end
47
- end
48
-
32
+ def message_string
49
33
  s = ''
50
34
 
51
35
  s += if epic
52
- Rainbow("[EPIC] #{msg}").bg(:white).fg(:black)
36
+ Rainbow("[EPIC] #{@message}").bg(:white).fg(:black)
53
37
  else
54
- msg
38
+ message
55
39
  end
56
40
 
57
41
  s += " [#{Rainbow(@ticket).fg(:blue)}]" if @ticket
@@ -73,7 +57,7 @@ class LogEntry
73
57
  # Empty array if no people are mentioned.
74
58
  #
75
59
  # @return [Array<String>]
76
- @message.scan(PERSON_REGEX).flatten.uniq.sort
60
+ @message.scan(/\s[~@](\w+)/).flatten.uniq.sort
77
61
  end
78
62
 
79
63
  def people?
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  # Represents a person at work.
4
5
  class Person
@@ -13,14 +14,6 @@ class Person
13
14
  end
14
15
 
15
16
  def to_s
16
- return "#{name} (~#{handle})" if @email.nil?
17
-
18
17
  "#{name} (~#{handle}) <#{email}>"
19
18
  end
20
-
21
- def ==(other)
22
- return false unless other.is_a?(Person)
23
-
24
- handle == other.handle && name == other.name && email == other.email && team == other.team && notes == other.notes
25
- end
26
19
  end
@@ -3,19 +3,11 @@
3
3
  require 'rainbow'
4
4
 
5
5
  # Printer for work log entries
6
- class Printer
7
- attr_reader :people
8
-
9
- # Initializes the printer with a list of people.
10
- # @param people [Array<Person>] An array of Person objects.
11
- def initialize(people = nil)
12
- @people = (people || []).to_h { |person| [person.handle, person] }
13
- end
14
-
6
+ module Printer
15
7
  # Prints a whole day of work log entries.
16
8
  # If date_inline is true, the date is printed inline with the time.
17
9
  # If epics_only is true, only epic entries are printed.
18
- def print_day(daily_log, date_inline = false, epics_only = false)
10
+ def self.print_day(daily_log, date_inline = false, epics_only = false)
19
11
  daily_log.date = Date.strptime(daily_log.date, '%Y-%m-%d') unless daily_log.date.respond_to?(:strftime)
20
12
 
21
13
  date_string = daily_log.date.strftime('%a, %B %-d, %Y')
@@ -32,7 +24,7 @@ class Printer
32
24
  # @param start_date [Date]
33
25
  # @param end_date [Date]
34
26
  # @return [void]
35
- def no_entries(start_date, end_date)
27
+ def self.no_entries(start_date, end_date)
36
28
  if start_date == end_date
37
29
  date_string = start_date.strftime('%a, %B %-d, %Y')
38
30
  puts "No entries found for #{Rainbow(date_string).gold}."
@@ -43,10 +35,9 @@ class Printer
43
35
  end
44
36
  end
45
37
 
38
+ private
39
+
46
40
  # Prints a single entry, formats the date and time.
47
- # @param daily_log [DailyLog]
48
- # @param entry [LogEntry]
49
- # @param date_inline [Boolean] If true, the date is printed inline with the time.
50
41
  def print_entry(daily_log, entry, date_inline = false)
51
42
  entry.time = DateTime.strptime(entry.time, '%H:%M:%S') unless entry.time.respond_to?(:strftime)
52
43
 
@@ -56,6 +47,8 @@ class Printer
56
47
  entry.time.strftime('%H:%M')
57
48
  end
58
49
 
59
- puts "#{Rainbow(time_string).gold} #{entry.message_string(@people)}"
50
+ puts "#{Rainbow(time_string).gold} #{entry.message_string}"
60
51
  end
52
+
53
+ module_function :print_entry
61
54
  end
@@ -6,17 +6,11 @@ require 'storage'
6
6
  STATS = Data.define(:total_days, :total_entries, :total_epics, :avg_entries, :first_entry, :last_entry)
7
7
 
8
8
  # Module for calculating statistics for the work log.
9
- class Statistics
10
- # Initialize the Statistics class.
11
- def initialize(config)
12
- @config = config
13
- @storage = Storage.new(config)
14
- end
15
-
9
+ module Statistics
16
10
  # Calculate statistics for the work log for all days.
17
- # @return [STATS] The statistics for the work log
18
- def calculate
19
- all_entries = @storage.all_days
11
+ # @return [STATS]
12
+ def self.calculate
13
+ all_entries = Storage.all_days
20
14
  return STATS.new(0, 0, 0, 0, Date.today, Date.today) if all_entries.empty?
21
15
 
22
16
  total_days = all_entries.length
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rainbow'
4
+ require_relative 'daily_log'
5
+ require_relative 'logger'
6
+
7
+ module Storage
8
+ # LogNotFoundError is raised when a log file is not found
9
+ class LogNotFoundError < StandardError; end
10
+
11
+ FILE_SUFFIX = '.yaml'
12
+ DATA_DIR = File.join(Dir.home, '.worklog')
13
+
14
+ def self.folder_exists?
15
+ Dir.exist?(DATA_DIR)
16
+ end
17
+
18
+ # Return all days with logs
19
+ def self.all_days
20
+ return [] unless folder_exists?
21
+
22
+ logs = []
23
+ Dir.glob(File.join(DATA_DIR, "*#{FILE_SUFFIX}")).map do |file|
24
+ logs << load_log(file)
25
+ end
26
+
27
+ logs
28
+ end
29
+
30
+ # Return days between start_date and end_date
31
+ # If end_date is nil, return logs from start_date to today
32
+ #
33
+ # @param [Date] start_date The start date, inclusive
34
+ # @param [Date] end_date The end date, inclusive
35
+ # @param [Boolean] epics_only If true, only return logs with epic entries
36
+ # @param [Array] tags_filter If provided, only return logs with entries that have at least one of the tags
37
+ def self.days_between(start_date, end_date = nil, epics_only = nil, tags_filter = nil)
38
+ return [] unless folder_exists?
39
+
40
+ logs = []
41
+ end_date = Date.today if end_date.nil?
42
+
43
+ return [] if start_date > end_date
44
+
45
+ while start_date <= end_date
46
+ if File.exist?(filepath(start_date))
47
+ tmp_logs = load_log(filepath(start_date))
48
+ tmp_logs.entries.keep_if { |entry| entry.epic? } if epics_only
49
+
50
+ if tags_filter
51
+ # Safeguard against entries without any tags, not just empty array
52
+ tmp_logs.entries.keep_if { |entry| entry.tags && (entry.tags & tags_filter).size > 0 }
53
+ end
54
+
55
+ logs << tmp_logs if tmp_logs.entries.length > 0
56
+ end
57
+
58
+ start_date += 1
59
+ end
60
+ logs
61
+ end
62
+
63
+ # Create file for a new day if it does not exist
64
+ def self.create_file_skeleton(date)
65
+ create_folder
66
+
67
+ File.write(filepath(date), YAML.dump(DailyLog.new(date:, entries: []))) unless File.exist?(filepath(date))
68
+ end
69
+
70
+ def self.load_log(file)
71
+ load_log!(file)
72
+ rescue LogNotFoundError
73
+ WorkLogger.error "No work log found for #{file}. Aborting."
74
+ nil
75
+ end
76
+
77
+ def self.load_log!(file)
78
+ WorkLogger.debug "Loading file #{file}"
79
+ begin
80
+ log = YAML.load_file(file, permitted_classes: [Date, Time, DailyLog, LogEntry])
81
+ log.entries.each do |entry|
82
+ entry.time = Time.parse(entry.time) unless entry.time.respond_to?(:strftime)
83
+ end
84
+ log
85
+ rescue Errno::ENOENT
86
+ raise LogNotFoundError
87
+ end
88
+ end
89
+
90
+ def self.write_log(file, daily_log)
91
+ create_folder
92
+
93
+ WorkLogger.debug "Writing to file #{file}"
94
+
95
+ File.open(file, 'w') do |f|
96
+ f.puts daily_log.to_yaml
97
+ end
98
+ end
99
+
100
+ def self.load_single_log_file(file, headline = true)
101
+ daily_log = load_log(file)
102
+ puts "Work log for #{Rainbow(daily_log.date).gold}:" if headline
103
+ daily_log.entries
104
+ end
105
+
106
+ private
107
+
108
+ # Create folder if not exists already.
109
+ def create_folder
110
+ Dir.mkdir(DATA_DIR) unless Dir.exist?(DATA_DIR)
111
+ end
112
+
113
+ def filepath(date)
114
+ # Construct filepath for a given date.
115
+ File.join(DATA_DIR, "#{date}#{FILE_SUFFIX}")
116
+ end
117
+
118
+ module_function :create_folder, :filepath
119
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  # Helpers for String manipulation
4
5
  module StringHelper
@@ -24,12 +25,4 @@ module StringHelper
24
25
  "#{singular}s"
25
26
  end
26
27
  end
27
-
28
- # Format a string to be left-aligned in a fixed-width field
29
- #
30
- # @param string [String] the string to format
31
- # @return [String] the formatted string
32
- def format_left(string)
33
- format('%18s', string)
34
- end
35
28
  end
@@ -4,10 +4,10 @@ require 'erb'
4
4
  require 'httparty'
5
5
  require 'json'
6
6
 
7
- require 'worklogger'
7
+ require_relative 'logger'
8
8
 
9
9
  # AI Summary generation.
10
- class Summary
10
+ module Summary
11
11
  MODEL = 'llama3.2'
12
12
  SUMMARY_INSTRUCTION = <<~INSTRUCTION
13
13
  <% entries.each do |entry| -%>
@@ -41,19 +41,13 @@ class Summary
41
41
  Example Output: "[Name] demonstrated outstanding performance during the review period. Key accomplishments include exceeding sales targets by 15% in Q3, implementing a new CRM system that improved customer response times by 30%, and mentoring two junior team members who achieved career advancements. These achievements highlight [Name]'s exceptional contributions to team success and organizational growth."
42
42
  INSTRUCTION
43
43
 
44
- def initialize(config)
45
- @config = config
46
- end
47
-
48
44
  # Build the prompt from provided log entries.
49
- def build_prompt(log_entries)
45
+ def self.build_prompt(log_entries)
50
46
  ERB.new(SUMMARY_INSTRUCTION, trim_mode: '-').result_with_hash(entries: log_entries)
51
47
  end
52
48
 
53
49
  # Generate a summary from provided log entries.
54
- # @param log_entries [Array<LogEntry>] The log entries to summarize.
55
- # @return [String] The generated summary.
56
- def generate_summary(log_entries)
50
+ def self.generate_summary(log_entries)
57
51
  prompt = build_prompt(log_entries)
58
52
 
59
53
  WorkLogger.debug("Using prompt: #{prompt}")
@@ -64,6 +64,7 @@
64
64
 
65
65
 
66
66
  </nav>
67
+
67
68
  <hr class="border border-primary border-2 opacity-75">
68
69
  <div class="pb-4">
69
70
  Show
@@ -100,21 +101,22 @@
100
101
  <div class="dropdown d-inline">
101
102
  <a class="btn border-secondary dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
102
103
  <% if tags %>
103
- <%= tags.size > 1 ? 'multiple tags' : "#{tags.first} tags" %>
104
+ <%= tags.first %> tag
104
105
  <% else %>
105
106
  all tags
106
107
  <% end %>
107
108
  </a>
108
109
  <ul class="dropdown-menu">
109
110
  <li><a class="dropdown-item" href="<%= update_query({'tags' => nil}) %>">all tags</a></li>
110
- <% @tags.to_a.each do |tag| %>
111
- <li><a class="dropdown-item" href="<%= update_query({'tags' => [tag]}) %>"><%= tag %> tags</a></li>
112
- <% end %>
111
+ <li><a class="dropdown-item" href="<%= update_query({'tags' => ['oncall']}) %>">oncall tags</a></li>
112
+ <li><a class="dropdown-item" href="<%= update_query({'tags' => ['1:1']}) %>">1:1 tags</a></li>
113
113
  </ul>
114
114
  </div>
115
115
  .
116
116
  </div>
117
117
 
118
+
119
+
118
120
  <%- logs.each do |log| -%>
119
121
  <section class="day">
120
122
  <strong><%= log.date.strftime('%a, %B %-d, %Y') %></strong>
@@ -138,10 +140,10 @@
138
140
  <p class="entries"><%= log.entries.size %> entries</p>
139
141
  </section>
140
142
  <%- end %>
141
- <p><%= total_entries %> entries total</p>
143
+ <p><%= total_entries%> entries total</p>
142
144
  </div>
143
145
  <hr/>
144
- <footer class="container pb-4 text-muted">
146
+ <footer class="container pb-4">
145
147
  Generated at <%= Time.now.strftime('%Y-%m-%d %H:%M %Z') %>
146
148
  </footer>
147
149
  </body>