particle-calendar 0.1.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: b9ce49a6213f463b1903aada3e6bdf5bcc28a0563617934738603e14e8d83e2c
4
- data.tar.gz: f27791d9393c905ca3e29d46709866100de32a3033fc9aef326f2e48f6c0dda9
3
+ metadata.gz: e9dccada98e81f51f44f33a7b403e3a1c1822784af127acd3ebb882942cbb3ee
4
+ data.tar.gz: cc94bd0ffe53aec20a7569524c4741d4a2b6a2a9a9d8a2dbdc294b622757c16b
5
5
  SHA512:
6
- metadata.gz: 4b1abcf266c1caf0d698a9817506d81d10eb44f23bd7c640e2cf763e9e8e597b8ebfbd3398b70db5411174d1dc5a647ae8c4022189e213ee96bb83f228155c98
7
- data.tar.gz: 006ab70996c606074c88668426be2ad0d185f2dc4c63d1cf389cfd758d4831a76cdfd922b11a0071b8c5a5a7e7e6a2081409e5a26cb148e0e186f6f0c7177e3e
6
+ metadata.gz: 63208f59ada5e3f59be029d064588dfea533061a18d34139a0063fb26cc5725baf5af301b49da4a14e7b1b9e68206234352814f68f47750ab3ccce9519f12e96
7
+ data.tar.gz: bb7143dcf7636bfe0eaa802b61c43b4787ab9ada41c2db7d5ea8c2dcb6fc010b52fe28479b6f7df07fa1be1e4275c0cf9f94c65b884aa57ff581857446e3a483
data/README.md CHANGED
@@ -53,7 +53,7 @@ Create a starter configuration, an Nginx server-block sample, and the static out
53
53
  particle setup
54
54
  ```
55
55
 
56
- By default this writes `particle.yml` with mode `0600`, `particle.nginx.conf`, and `public/` in the current directory. It generates a long random URL path for the Nginx sample and refuses to replace either setup file if it already exists.
56
+ By default this writes `particle.yml` with mode `0600`, `particle.nginx.conf`, `public/`, and `log/` in the current directory. It generates a long random URL path for the Nginx sample and refuses to replace either setup file if it already exists.
57
57
 
58
58
  Customize every destination when needed:
59
59
 
@@ -78,6 +78,7 @@ calendar_urls:
78
78
 
79
79
  days_to_show: 28
80
80
  minimum_slot_minutes: 60
81
+ first_day_of_week: monday
81
82
 
82
83
  event_buffer:
83
84
  before_minutes: 30
@@ -151,6 +152,14 @@ availability:
151
152
  unavailable: true
152
153
  ```
153
154
 
155
+ ### Calendar week start
156
+
157
+ `first_day_of_week` controls the first column of the rendered calendar and its week labels. It defaults to `monday`; use any lowercase weekday name, for example:
158
+
159
+ ```yaml
160
+ first_day_of_week: sunday
161
+ ```
162
+
154
163
  ### Minimum slots and event buffers
155
164
 
156
165
  `minimum_slot_minutes` removes shorter free fragments after subtraction. The default is `0`.
@@ -235,7 +244,6 @@ Create a dedicated account or deploy as an unprivileged service user, install Ru
235
244
  ```bash
236
245
  gem install particle-calendar
237
246
  cd /opt/particle
238
- mkdir -p log
239
247
  particle setup --server-name calendar.example.com
240
248
  # Edit particle.yml and replace the example calendar placeholders.
241
249
  particle generate --config /opt/particle/particle.yml --output /opt/particle/public
@@ -297,6 +305,7 @@ The repository includes a [`deploy/availability.logrotate`](deploy/availability.
297
305
  - **Calendar exceeded a safe limit:** reduce an unusually large displayed range or inspect the feed privately for excessive event counts or dense recurrence rules. The generator intentionally fails before publishing rather than risking resource exhaustion.
298
306
  - **Old update timestamp:** inspect cron logs. A stale page usually means a later run failed safely.
299
307
  - **Permission denied while publishing:** the generator needs write permission on `public/`; Nginx needs read permission only.
308
+ - **Nginx reports that `index.htmlindex.html` is not a directory:** replace the installed page locations with the current `particle.nginx.conf` example, then validate and reload Nginx. Older examples pointed a trailing-slash location directly at the file, causing Nginx's index module to append `index.html` twice.
300
309
 
301
310
  ## Known limits
302
311
 
@@ -10,6 +10,9 @@ calendar_urls:
10
10
  days_to_show: 28
11
11
  minimum_slot_minutes: 60
12
12
 
13
+ # The calendar grid starts on Monday by default. Any lowercase weekday is accepted.
14
+ first_day_of_week: monday
15
+
13
16
  event_buffer:
14
17
  before_minutes: 0
15
18
  after_minutes: 0
@@ -68,7 +68,8 @@ module Availability
68
68
  timezone: config.timezone,
69
69
  enabled: config.enabled,
70
70
  today: today,
71
- days_to_show: config.days_to_show
71
+ days_to_show: config.days_to_show,
72
+ first_day_of_week: config.first_day_of_week
72
73
  )
73
74
  end
74
75
 
@@ -113,6 +113,7 @@ module Availability
113
113
  config_path: File.join(@working_directory, 'particle.yml'),
114
114
  nginx_path: File.join(@working_directory, 'particle.nginx.conf'),
115
115
  output_dir: File.join(@working_directory, 'public'),
116
+ log_dir: File.join(@working_directory, 'log'),
116
117
  server_name: 'example.com',
117
118
  url_path: "/#{@random_path.call}/"
118
119
  }
@@ -150,6 +151,7 @@ module Availability
150
151
  @output.puts("Created #{setup.config_path} (mode 0600)")
151
152
  @output.puts("Created #{setup.nginx_path}")
152
153
  @output.puts("Created output directory #{setup.output_dir}")
154
+ @output.puts("Created log directory #{setup.log_dir}")
153
155
  @output.puts('Next: edit the config, run particle generate, then review and install the Nginx file.')
154
156
  end
155
157
 
@@ -14,7 +14,7 @@ module Availability
14
14
  include ConfigValueParser
15
15
 
16
16
  TOP_LEVEL_KEYS = %w[
17
- enabled timezone calendar_urls days_to_show minimum_slot_minutes event_buffer availability
17
+ enabled timezone calendar_urls days_to_show minimum_slot_minutes event_buffer availability first_day_of_week
18
18
  ].freeze
19
19
  EVENT_BUFFER_KEYS = %w[before_minutes after_minutes].freeze
20
20
  WEEKDAYS = %w[sunday monday tuesday wednesday thursday friday saturday].freeze
@@ -24,7 +24,7 @@ module Availability
24
24
  MAX_CALENDAR_URLS = 20
25
25
 
26
26
  attr_reader :enabled, :timezone, :calendar_urls, :days_to_show,
27
- :minimum_slot_minutes, :buffer_before_minutes, :buffer_after_minutes
27
+ :minimum_slot_minutes, :buffer_before_minutes, :buffer_after_minutes, :first_day_of_week
28
28
 
29
29
  def self.load(path, env: ENV)
30
30
  raise ConfigError, "configuration file not found: #{path}" unless File.file?(path)
@@ -41,10 +41,7 @@ module Availability
41
41
  @raw = stringify_keys(raw)
42
42
  @env = env
43
43
  validate_known_keys(@raw, TOP_LEVEL_KEYS, 'configuration')
44
- @enabled = boolean('enabled', default: true)
45
- @timezone = parse_timezone
46
- @days_to_show = bounded_positive_integer('days_to_show', default: 28, maximum: MAX_DAYS_TO_SHOW)
47
- @minimum_slot_minutes = nonnegative_integer('minimum_slot_minutes', default: 0)
44
+ parse_general_settings
48
45
  parse_buffers
49
46
  @availability = parse_availability
50
47
  @calendar_urls = parse_calendar_urls
@@ -56,6 +53,14 @@ module Availability
56
53
 
57
54
  private
58
55
 
56
+ def parse_general_settings
57
+ @enabled = boolean('enabled', default: true)
58
+ @timezone = parse_timezone
59
+ @days_to_show = bounded_positive_integer('days_to_show', default: 28, maximum: MAX_DAYS_TO_SHOW)
60
+ @minimum_slot_minutes = nonnegative_integer('minimum_slot_minutes', default: 0)
61
+ @first_day_of_week = parse_first_day_of_week
62
+ end
63
+
59
64
  def stringify_keys(value)
60
65
  if value.is_a?(Hash)
61
66
  return value.each_with_object({}) do |(key, item), hash|
@@ -76,6 +81,15 @@ module Availability
76
81
  raise ConfigError, "timezone is unknown: #{name}"
77
82
  end
78
83
 
84
+ def parse_first_day_of_week
85
+ day = @raw.fetch('first_day_of_week', 'monday')
86
+ unless day.is_a?(String) && WEEKDAYS.include?(day)
87
+ raise ConfigError, "first_day_of_week must be one of: #{WEEKDAYS.join(', ')}"
88
+ end
89
+
90
+ day
91
+ end
92
+
79
93
  def parse_buffers
80
94
  buffer = @raw.fetch('event_buffer', {})
81
95
  raise ConfigError, 'event_buffer must be a mapping' unless buffer.is_a?(Hash)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'cgi'
4
+ require 'date'
4
5
  require 'erb'
5
6
 
6
7
  module Availability
@@ -12,14 +13,15 @@ module Availability
12
13
  @template = ERB.new(File.read(template_path), trim_mode: '-')
13
14
  end
14
15
 
15
- def render(days:, generated_at:, timezone:, enabled:, today:, days_to_show:)
16
+ def render(days:, generated_at:, timezone:, enabled:, today:, days_to_show:, first_day_of_week:)
16
17
  view = View.new(
17
18
  days: days,
18
19
  generated_at: generated_at,
19
20
  timezone: timezone,
20
21
  enabled: enabled,
21
22
  today: today,
22
- days_to_show: days_to_show
23
+ days_to_show: days_to_show,
24
+ first_day_of_week: first_day_of_week
23
25
  )
24
26
  @template.result(view.template_binding)
25
27
  end
@@ -30,13 +32,14 @@ module Availability
30
32
 
31
33
  attr_reader :enabled, :today
32
34
 
33
- def initialize(days:, generated_at:, timezone:, enabled:, today:, days_to_show:)
35
+ def initialize(days:, generated_at:, timezone:, enabled:, today:, days_to_show:, first_day_of_week:)
34
36
  @days = days
35
37
  @generated_at = generated_at
36
38
  @timezone = timezone
37
39
  @enabled = enabled
38
40
  @today = today
39
41
  @days_to_show = days_to_show
42
+ @first_day_of_week = first_day_of_week
40
43
  end
41
44
 
42
45
  def template_binding
@@ -57,8 +60,8 @@ module Availability
57
60
 
58
61
  def format_week_label(week)
59
62
  first_date = week.compact.first.date
60
- monday = first_date - (first_date.cwday - 1)
61
- "Week of #{monday.day} #{monday.strftime('%B %Y')}"
63
+ week_start = first_date - weekday_offset(first_date)
64
+ "Week of #{week_start.day} #{week_start.strftime('%B %Y')}"
62
65
  end
63
66
 
64
67
  def format_time(time)
@@ -68,13 +71,13 @@ module Availability
68
71
  def weeks
69
72
  return [] if @days.empty?
70
73
 
71
- padded_days = Array.new(@days.first.date.cwday - 1) + @days
74
+ padded_days = Array.new(weekday_offset(@days.first.date)) + @days
72
75
  padded_days.concat(Array.new((7 - padded_days.length) % 7))
73
76
  padded_days.each_slice(7).to_a
74
77
  end
75
78
 
76
79
  def weekdays
77
- WEEKDAYS
80
+ WEEKDAYS.rotate(WEEKDAYS.index(@first_day_of_week))
78
81
  end
79
82
 
80
83
  def updated_at
@@ -97,6 +100,12 @@ module Availability
97
100
 
98
101
  "#{@today.strftime('%-d %b')}–#{final_with_year}"
99
102
  end
103
+
104
+ private
105
+
106
+ def weekday_offset(date)
107
+ (date.wday - Date::DAYNAMES.index(@first_day_of_week.capitalize)) % 7
108
+ end
100
109
  end
101
110
  end
102
111
  end
@@ -9,21 +9,23 @@ module Availability
9
9
  SERVER_NAME_PATTERN = /\A[a-zA-Z0-9.-]+\z/
10
10
  URL_PATH_PATTERN = %r{\A/[a-zA-Z0-9_-]+/\z}
11
11
 
12
- def initialize(config_path:, nginx_path:, output_dir:, server_name:, url_path:)
12
+ def initialize(config_path:, nginx_path:, output_dir:, log_dir:, server_name:, url_path:)
13
13
  @config_path = File.expand_path(config_path)
14
14
  @nginx_path = File.expand_path(nginx_path)
15
15
  @output_dir = File.expand_path(output_dir)
16
+ @log_dir = File.expand_path(log_dir)
16
17
  @server_name = server_name
17
18
  @url_path = url_path
18
19
  end
19
20
 
20
- attr_reader :config_path, :nginx_path, :output_dir
21
+ attr_reader :config_path, :nginx_path, :output_dir, :log_dir
21
22
 
22
23
  def run
23
24
  validate!
24
25
  ensure_targets_are_available!
25
26
  create_files
26
27
  FileUtils.mkdir_p(output_dir)
28
+ FileUtils.mkdir_p(log_dir)
27
29
  true
28
30
  rescue SystemCallError => e
29
31
  cleanup_created_files
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Availability
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
@@ -19,7 +19,8 @@ server {
19
19
  }
20
20
 
21
21
  location = <%= @url_path %> {
22
- alias "<%= escaped_output_dir %>/index.html";
22
+ alias "<%= escaped_output_dir %>/";
23
+ index index.html;
23
24
  default_type text/html;
24
25
 
25
26
  # For enforced privacy, create this file with `htpasswd` and uncomment:
@@ -27,6 +28,13 @@ server {
27
28
  # auth_basic_user_file /etc/nginx/particle.htpasswd;
28
29
  }
29
30
 
31
+ # The index module redirects internally here after verifying that the file exists.
32
+ location = <%= @url_path %>index.html {
33
+ internal;
34
+ alias "<%= escaped_output_dir %>/index.html";
35
+ default_type text/html;
36
+ }
37
+
30
38
  location = <%= @url_path %>favicon.svg {
31
39
  alias "<%= escaped_output_dir %>/favicon.svg";
32
40
  default_type image/svg+xml;
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: particle-calendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timur