watchfor 0.13.0 → 0.15.0

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: 980b1fdcb72ae50da30b5abf19101018add41e1d1b840ef9c6378211abb07f58
4
- data.tar.gz: '099d81bbfe131ceac6031c37078304a11673cd6c40219ac3b101bdf88a71ecf5'
3
+ metadata.gz: f5ad40afc1187be8bcdf41c4c418b3c18b304b097a901219edfe777ae41087d1
4
+ data.tar.gz: b4da046bc763240825953b51a69e13e50838272d7e642bf78f450b4f7914810d
5
5
  SHA512:
6
- metadata.gz: 040a75ce5d87d1d7ef6855edae019724e4247a3b823fdad13873611888ce98e464c2e494d4bc83682a148390c429fa01350d910cc04182ca590d2dff8da51653
7
- data.tar.gz: 6b5b2506e0da8ebec2aedcf1340520287ac7456ed7ff67b800154a2591872b70c683ee2e833e49a7b48753a0c1953d593edef8803d0b46399acfe77f5a32add2
6
+ metadata.gz: 73e41e8dc6673385ab4bfca74bb41a6e2d83d08429b88ff1bf826c835201295eda5c5b1edd76cf32518cb86ba9829b4b8d9b4641e9f1ee0f5a50743e2fe848e6
7
+ data.tar.gz: 1a4951d407a131eec5206d4b909de80a26f4833aaa916a08aa914846c6f71b8b6161cb5ceb90b982c3a0a2ce91fb465d5f7c297ce7bac2db92215e3b3481160d
data/README.md CHANGED
@@ -40,8 +40,8 @@ wf.incidents.exclude(19351, reason: "Probe-side DNS hiccup")
40
40
  # Irreversible: wipe a monitor's checks and incidents; uptime restarts "since reset"
41
41
  wf.monitors.reset(mon["id"])
42
42
 
43
- # Page integrity: after a deploy, accept what the newest check saw as the new baseline
44
- wf.monitors.accept_baseline(integrity_monitor_id)
43
+ # Page integrity: after a deploy, check the pages and accept what that check saw
44
+ wf.monitors.accept_baseline(integrity_monitor_id, fresh: true)
45
45
 
46
46
  # Browser checks: try a Playwright script once (nothing saved), then read the result
47
47
  run = wf.playwright.test_run(script: script, target: "https://shop.example.com")
@@ -59,11 +59,24 @@ wf.monitors.checks(mon["id"], hours: 24, limit: 100, cursor: page["next_cursor"]
59
59
 
60
60
  # Delete a contact group that alert rules still use (409 without force)
61
61
  wf.contact_groups.delete("<group-id>", force: true)
62
+
63
+ # What the alerting engine sees right now: alerted, recovering 1/3, confirming 2 of 3…
64
+ state = wf.monitors.get(mon["id"])["alerting_state"]
65
+
66
+ # Response times on a real time axis (5-min buckets for 24h, 15 min for 7d)
67
+ up = wf.monitors.uptime(mon["id"], "24h", series: true)
68
+ (up["response_time_series"] || []).each { |b| puts "#{b['at']} #{b['avg_ms']}" }
69
+
70
+ # Pause up to 100 monitors at once; post a status-page announcement
71
+ wf.monitors.bulk("pause", ["<id-1>", "<id-2>"])
72
+ notice = wf.status_pages.create_announcement("<page-id>",
73
+ { title: "Checkout errors", body: "We are looking into it." })
74
+ wf.status_pages.update_announcement("<page-id>", notice["id"], { body: "Fixed.", status: "resolved" })
62
75
  ```
63
76
 
64
77
  ### Live diagnostics
65
78
 
66
- Run any of 22 checks from WatchFor's probe fleet against **any public
79
+ Run any of 24 checks from WatchFor's probe fleet against **any public
67
80
  target**, monitored or not — this measures right now, rather than reading
68
81
  what WatchFor recorded:
69
82
 
@@ -78,6 +91,18 @@ dns = wf.diagnostics.run("dns-lookup", "example.com",
78
91
  puts "finding: #{dns['error']}" unless dns["success"]
79
92
  puts "runs left this hour: #{dns['remaining']}"
80
93
 
94
+ # Graded checks (tls-grade, http-headers, cwv-check, mcp-check, email-health,
95
+ # mail-server-check, cdn-check, smart-audit, dns-health) also return the
96
+ # dashboard's WatchFor Grade: letter, rubric id and what holds it down
97
+ headers = wf.diagnostics.run("http-headers", "https://example.com")
98
+ grade = headers["grade"] || {}
99
+ if grade["letter"]
100
+ puts "grade #{grade['letter']} (#{grade['rubric']})"
101
+ grade["capped_by"].each { |c| puts " max #{c['cap']}: #{c['title']} — fix: #{c['fix']}" }
102
+ else
103
+ puts "not graded: #{grade['not_graded_reason']}"
104
+ end
105
+
81
106
  # The whole picture in one call: DNS + propagation + TLS + HTTP + ping
82
107
  # from up to 3 regions, aggregated into one verdict
83
108
  report = wf.diagnostics.diagnose_target("example.com")
@@ -90,11 +115,38 @@ Details: <https://watchfor.io/docs/api/diagnostics>
90
115
 
91
116
  Namespaces: `wf.monitors`, `wf.alert_rules`, `wf.incidents`,
92
117
  `wf.hosts`, `wf.status_pages`, `wf.on_call`, `wf.notification_channels`, `wf.maintenance_windows`, `wf.contacts`, `wf.contact_groups`,
93
- `wf.diagnostics`. Top-level:
118
+ `wf.diagnostics`, `wf.playwright`, `wf.variables`. Top-level:
94
119
  `wf.summary`, `wf.plan`, `wf.me`, `wf.locations`, `wf.monitor_types`,
95
- `wf.incident_stats(period)`, `wf.notifications`, `wf.activity`. Errors raise
120
+ `wf.incident_stats(period)`, `wf.report`, `wf.notifications`, `wf.activity`,
121
+ `wf.batch([...])`, `wf.metrics`. Errors raise
96
122
  `Watchfor::Error` with `#status`, `#code`, `#message`.
97
123
 
124
+ ## Organizations
125
+
126
+ An API key belongs to one organization. An **OAuth access token** acts as
127
+ the user who approved it, so it reaches every organization that user
128
+ belongs to; choose one with `organization:` — the SDK sends it as the
129
+ `X-WatchFor-Organization` header on every request:
130
+
131
+ ```ruby
132
+ acme = Watchfor::Client.new(api_key: oauth_access_token, organization: "<organization-id>")
133
+ acme.me # {"organization" => {"id", "name", "slug"}, "scope" => ...}
134
+ other = acme.with_organization("<another-organization-id>") # new client, same token
135
+ other.monitors.list
136
+ ```
137
+
138
+ - Without `organization:`, an OAuth token acts on the user's oldest owned
139
+ organization (else their oldest membership).
140
+ - The user must be a member of that organization (otherwise `403
141
+ forbidden`), and their role there applies: members read, acknowledge
142
+ incidents and run diagnostics; changes need owner or admin.
143
+ - With an API key leave it unset; it may only repeat the key's own
144
+ organization id, and any other id is `403 forbidden` (a key never
145
+ switches organization).
146
+ - `with_organization(nil)` drops the header again.
147
+
148
+ Details: <https://watchfor.io/docs/api/authentication#how-oauth-maps-to-your-data>
149
+
98
150
  ## Prometheus metrics (Pro plan and above)
99
151
 
100
152
  ```ruby
@@ -112,6 +164,12 @@ watchfor monitors
112
164
  watchfor incidents
113
165
  watchfor checks <monitor_id> [--cursor <next_cursor>]
114
166
  watchfor report --period 30d
167
+ watchfor me # which organization + scope
168
+
169
+ # OAuth token: pick the organization per call or via the environment
170
+ export WATCHFOR_API_KEY=<oauth-access-token>
171
+ watchfor --organization <organization-id> monitors
172
+ WATCHFOR_ORGANIZATION=<organization-id> watchfor summary
115
173
  ```
116
174
 
117
175
  Reference: <https://watchfor.io/openapi.json> · <https://watchfor.io/docs/api>
data/exe/watchfor CHANGED
@@ -8,6 +8,22 @@ api_key = ENV["WATCHFOR_API_KEY"]
8
8
  base_url = ENV.fetch("WATCHFOR_BASE_URL", Watchfor::DEFAULT_BASE_URL)
9
9
 
10
10
  args = ARGV.dup
11
+
12
+ # Global --organization <id> / --organization=<id>, anywhere on the line;
13
+ # WATCHFOR_ORGANIZATION is the default. OAuth tokens reach every
14
+ # organization their user belongs to; an API key only accepts its own id.
15
+ organization = ENV.fetch("WATCHFOR_ORGANIZATION", nil)
16
+ if (i = args.index("--organization"))
17
+ organization = args[i + 1].to_s
18
+ args.slice!(i, 2)
19
+ if organization.strip.empty?
20
+ warn "--organization needs an organization id"
21
+ exit 2
22
+ end
23
+ elsif (i = args.index { |a| a.start_with?("--organization=") })
24
+ organization = args.delete_at(i).split("=", 2)[1].to_s
25
+ end
26
+
11
27
  command = args.shift
12
28
 
13
29
  if command == "--version"
@@ -16,15 +32,16 @@ if command == "--version"
16
32
  end
17
33
 
18
34
  if api_key.nil? || api_key.empty?
19
- warn "No API key. Set WATCHFOR_API_KEY."
35
+ warn "No API key. Set WATCHFOR_API_KEY (an API key or an OAuth access token)."
20
36
  exit 2
21
37
  end
22
38
 
23
- wf = Watchfor::Client.new(api_key: api_key, base_url: base_url)
39
+ wf = Watchfor::Client.new(api_key: api_key, base_url: base_url, organization: organization)
24
40
 
25
41
  begin
26
42
  result =
27
43
  case command
44
+ when "me" then wf.me
28
45
  when "summary" then wf.summary
29
46
  when "plan" then wf.plan
30
47
  when "monitors" then wf.monitors.list
@@ -42,7 +59,10 @@ begin
42
59
  print wf.metrics(tag: tag, type: type)
43
60
  exit 0
44
61
  else
45
- warn "Usage: watchfor [summary|plan|monitors|locations|monitor-types|incidents|checks <monitor_id> [--cursor c]|report [--period 7d|30d]|metrics [--tag t] [--type t]]"
62
+ warn "Usage: watchfor [--organization <id>] " \
63
+ "[me|summary|plan|monitors|locations|monitor-types|incidents|checks <monitor_id> [--cursor c]|" \
64
+ "report [--period 7d|30d]|metrics [--tag t] [--type t]]\n" \
65
+ "Env: WATCHFOR_API_KEY (API key or OAuth access token), WATCHFOR_ORGANIZATION, WATCHFOR_BASE_URL"
46
66
  exit 2
47
67
  end
48
68
  puts JSON.pretty_generate(result)
@@ -6,6 +6,8 @@ require "uri"
6
6
 
7
7
  module Watchfor
8
8
  DEFAULT_BASE_URL = "https://watchfor.io/api/v1"
9
+ # The request header that selects the organization an OAuth token acts on.
10
+ ORGANIZATION_HEADER = "X-WatchFor-Organization"
9
11
 
10
12
  # Raised when the API returns a non-2xx response. Carries the HTTP
11
13
  # +status+ and the API's structured +code+ and +message+.
@@ -24,17 +26,28 @@ module Watchfor
24
26
  # wf = Watchfor::Client.new(api_key: "wf_live_...")
25
27
  # wf.summary
26
28
  # wf.monitors.list["data"].each { |m| puts "#{m['name']} #{m['status']}" }
29
+ #
30
+ # An OAuth access token reaches every organization its user belongs to;
31
+ # pick one with +organization:+ (sent as X-WatchFor-Organization):
32
+ #
33
+ # acme = Watchfor::Client.new(api_key: oauth_token, organization: "<org id>")
34
+ # other = acme.with_organization("<another org id>")
35
+ #
36
+ # An API key belongs to one organization: leave +organization:+ unset, or
37
+ # pass that same id (any other id is 403 forbidden).
27
38
  class Client
28
39
  attr_reader :monitors, :alert_rules, :incidents, :hosts, :status_pages, :on_call, :notification_channels,
29
40
  :maintenance_windows, :contacts, :contact_groups,
30
- :diagnostics, :playwright, :variables
41
+ :diagnostics, :playwright, :variables, :organization
31
42
 
32
- def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: 30)
43
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: 30, organization: nil)
33
44
  raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
34
45
 
35
46
  @api_key = api_key
36
47
  @base_url = base_url.chomp("/")
37
48
  @timeout = timeout
49
+ org = organization.to_s.strip
50
+ @organization = org.empty? ? nil : org
38
51
  @monitors = Monitors.new(self)
39
52
  @alert_rules = AlertRules.new(self)
40
53
  @incidents = Incidents.new(self)
@@ -50,6 +63,14 @@ module Watchfor
50
63
  @variables = Variables.new(self)
51
64
  end
52
65
 
66
+ # A new client acting on another organization (same credential and
67
+ # settings); nil drops the header so the credential's default
68
+ # organization applies. This client is left unchanged.
69
+ def with_organization(organization)
70
+ self.class.new(api_key: @api_key, base_url: @base_url, timeout: @timeout,
71
+ organization: organization)
72
+ end
73
+
53
74
  # Core request. +query+ and +body+ are optional hashes.
54
75
  def request(method, path, query: nil, body: nil, idempotency_key: nil, text: false)
55
76
  uri = URI("#{@base_url}#{path}")
@@ -68,6 +89,7 @@ module Watchfor
68
89
  req["Accept"] = text ? "text/plain, application/json" : "application/json"
69
90
  req["User-Agent"] = "watchfor-ruby/#{Watchfor::VERSION}"
70
91
  req["Idempotency-Key"] = idempotency_key if idempotency_key
92
+ req[ORGANIZATION_HEADER] = @organization if @organization
71
93
  unless body.nil?
72
94
  req["Content-Type"] = "application/json"
73
95
  req.body = JSON.generate(body)
@@ -92,7 +114,17 @@ module Watchfor
92
114
  end
93
115
 
94
116
  # Top-level reads
117
+ # The credential's scope and the organization it acts on (with an OAuth
118
+ # token, the one +organization:+ selected).
95
119
  def me = request("GET", "/me")
120
+
121
+ # Run up to 20 operations ({ method:, path:, body: } with a relative
122
+ # /api/v1 path) in one request, with the same credential and
123
+ # organization; "results" come back in request order as {status, body}.
124
+ def batch(operations) = request("POST", "/batch", body: { operations: operations })
125
+
126
+ # The OpenAPI 3.1 document this client wraps (no key needed).
127
+ def openapi_spec = request("GET", "/openapi.json")
96
128
  def summary = request("GET", "/summary")
97
129
 
98
130
  # The organization in Prometheus text format (Pro plan and above):
@@ -108,11 +140,24 @@ module Watchfor
108
140
  # Org report vs the previous period (period: "7d"/"30d", or from/to ISO 8601
109
141
  # on advanced-reports plans): uptime, incidents, MTTR/MTBF, response-time
110
142
  # trend, per-monitor rows, type-aware sections and plain-language insights.
111
- def report(period: nil, from: nil, to: nil)
143
+ #
144
+ # Scoping (advanced-reports plans): monitor_ids:/tags: (union), sections:
145
+ # (locations, ssl, domains, cwv, heartbeat, blacklist, mcp, hosts),
146
+ # incident_status: (acknowledged, resolved, open) — arrays or
147
+ # comma-separated strings. false for response_time:/monitors_table:/
148
+ # insights: drops that block.
149
+ def report(period: nil, from: nil, to: nil, monitor_ids: nil, tags: nil, sections: nil,
150
+ response_time: nil, monitors_table: nil, insights: nil, incident_status: nil)
151
+ csv = ->(v) { v.is_a?(Array) ? v.join(",") : v }
152
+ query = { monitor_ids: csv.call(monitor_ids), tags: csv.call(tags), sections: csv.call(sections),
153
+ incident_status: csv.call(incident_status),
154
+ response_time: response_time.nil? ? nil : response_time.to_s,
155
+ monitors_table: monitors_table.nil? ? nil : monitors_table.to_s,
156
+ insights: insights.nil? ? nil : insights.to_s }
112
157
  if from || to
113
- request("GET", "/reports", query: { from: from, to: to })
158
+ request("GET", "/reports", query: query.merge(from: from, to: to))
114
159
  else
115
- request("GET", "/reports", query: { period: period })
160
+ request("GET", "/reports", query: query.merge(period: period))
116
161
  end
117
162
  end
118
163
  # The org's two report-email cadences (weekly + monthly); set toggles
@@ -131,8 +176,17 @@ module Watchfor
131
176
 
132
177
  class Monitors < Namespace
133
178
  def list(**query) = @c.request("GET", "/monitors", query: query)
134
- # The monitor plus available_metrics (alert metrics valid for its type).
179
+ # The monitor plus available_metrics (alert metrics valid for its type)
180
+ # and alerting_state — what the alerting engine sees right now (alerted,
181
+ # recovering n of m, failing/confirming n of m; nil when every rule is
182
+ # healthy).
135
183
  def get(id) = @c.request("GET", "/monitors/#{id}")
184
+ # Pause, resume or delete (+action+) up to 100 monitors in one call; ids
185
+ # that are not yours or need no change come back in "skipped".
186
+ def bulk(action, ids, idempotency_key: nil)
187
+ @c.request("POST", "/monitors/bulk", body: { action: action, ids: ids },
188
+ idempotency_key: idempotency_key)
189
+ end
136
190
  # Credentials (catalog fields marked secret: mailbox/SMTP/FTP passwords,
137
191
  # SSH keys, HTTP/API/MCP auth passwords, bearer tokens and credential
138
192
  # header values) are write-only: stored encrypted, returned as
@@ -142,7 +196,9 @@ module Watchfor
142
196
  # config is merged (omitted keys kept, nil removes). A "[redacted]"
143
197
  # credential is kept when omitted or sent back; changing the server,
144
198
  # port, TLS mode or login it is used with (HTTP: the URL's host, port
145
- # or scheme) needs it again (else 400).
199
+ # or scheme) needs it again (else 400). "check_queued" => true in the
200
+ # result means the change dispatched one immediate check, outside the
201
+ # Run-now budget.
146
202
  def update(id, body) = @c.request("PATCH", "/monitors/#{id}", body: body)
147
203
  def delete(id) = @c.request("DELETE", "/monitors/#{id}")
148
204
  def pause(id) = @c.request("POST", "/monitors/#{id}/pause")
@@ -153,7 +209,13 @@ module Watchfor
153
209
  # monitor and starts uptime over ("since reset"); the monitor and its
154
210
  # configuration, alert rules, tags and locations stay.
155
211
  def reset(id) = @c.request("POST", "/monitors/#{id}/reset")
156
- def uptime(id, period = nil) = @c.request("GET", "/monitors/#{id}/uptime", query: { period: period })
212
+ # Uptime (confirmed critical incidents only), check success and
213
+ # response-time stats. series: true (periods up to 7d) adds
214
+ # "response_time_series" — buckets of "series_bucket_seconds" (300 for
215
+ # 24h, 900 for 7d) on a real time axis, oldest first.
216
+ def uptime(id, period = nil, series: false)
217
+ @c.request("GET", "/monitors/#{id}/uptime", query: { period: period, series: series ? "true" : nil })
218
+ end
157
219
  # Recent probe results, newest first. Page with the response's next_cursor -> cursor:
158
220
  # (keep the same hours/success); "locations" is the per-location breakdown for the whole window.
159
221
  def checks(id, hours: nil, limit: nil, success: nil, cursor: nil, **query)
@@ -173,11 +235,18 @@ module Watchfor
173
235
  # Page integrity monitors: the accepted baseline of every watched page.
174
236
  def baseline(id) = @c.request("GET", "/monitors/#{id}/baseline")
175
237
  # Page integrity monitors: accept what the newest check saw as the new
176
- # baseline — all pages, or only `pages:`. Call it from a deploy pipeline
177
- # a minute after the release; unfetched pages come back in "skipped".
178
- def accept_baseline(id, pages: nil, idempotency_key: nil)
238
+ # baseline — all pages, or only `pages:`. Open integrity incidents
239
+ # resolve at once when nothing else differs ("incidents_resolved").
240
+ # From a deploy pipeline pass `fresh: true`: the pages are checked first
241
+ # and that check is accepted ("status" "pending" = still running, it
242
+ # finishes on its own — see baseline(id)["fresh_accept"]); unfetched
243
+ # pages come back in "skipped".
244
+ def accept_baseline(id, pages: nil, idempotency_key: nil, fresh: false)
245
+ body = {}
246
+ body[:pages] = pages if pages
247
+ body[:fresh] = true if fresh
179
248
  @c.request("POST", "/monitors/#{id}/baseline",
180
- body: pages ? { pages: pages } : {}, idempotency_key: idempotency_key)
249
+ body: body, idempotency_key: idempotency_key)
181
250
  end
182
251
  end
183
252
 
@@ -202,7 +271,9 @@ module Watchfor
202
271
  class AlertRules < Namespace
203
272
  def list(monitor_id) = @c.request("GET", "/monitors/#{monitor_id}/alert-rules")
204
273
  def get(monitor_id, rule_id) = @c.request("GET", "/monitors/#{monitor_id}/alert-rules/#{rule_id}")
205
- def create(monitor_id, body) = @c.request("POST", "/monitors/#{monitor_id}/alert-rules", body: body)
274
+ def create(monitor_id, body, idempotency_key: nil)
275
+ @c.request("POST", "/monitors/#{monitor_id}/alert-rules", body: body, idempotency_key: idempotency_key)
276
+ end
206
277
  def update(monitor_id, rule_id, body) = @c.request("PATCH", "/monitors/#{monitor_id}/alert-rules/#{rule_id}", body: body)
207
278
  def delete(monitor_id, rule_id) = @c.request("DELETE", "/monitors/#{monitor_id}/alert-rules/#{rule_id}")
208
279
  end
@@ -218,12 +289,40 @@ module Watchfor
218
289
  def exclude(id, reason: nil) = @c.request("POST", "/incidents/#{id}/exclude", body: reason.nil? ? {} : { reason: reason })
219
290
  # Undo #exclude - the incident counts as downtime again.
220
291
  def include(id) = @c.request("POST", "/incidents/#{id}/include")
292
+ # Retroactively count a RESOLVED incident as planned maintenance: it
293
+ # leaves uptime and SLA. Returns the "maintenance_window" recorded for it
294
+ # (marking again returns the same one); delete that window to undo.
295
+ def mark_maintenance(id, idempotency_key: nil)
296
+ @c.request("POST", "/incidents/#{id}/mark-maintenance", idempotency_key: idempotency_key)
297
+ end
221
298
  end
222
299
 
223
300
  class StatusPages < Namespace
224
301
  def list(**query) = @c.request("GET", "/status-pages", query: query)
225
302
  def get(id) = @c.request("GET", "/status-pages/#{id}")
303
+ # Create a page (slug, name, optional components...). Password protection,
304
+ # logo upload and custom subdomains are dashboard-only.
305
+ def create(body, idempotency_key: nil) = @c.request("POST", "/status-pages", body: body, idempotency_key: idempotency_key)
306
+ # Partial update; a slug change frees the old URL at once.
307
+ def update(id, body) = @c.request("PATCH", "/status-pages/#{id}", body: body)
308
+ def delete(id) = @c.request("DELETE", "/status-pages/#{id}")
309
+ # Announcements, newest first.
226
310
  def announcements(id, **query) = @c.request("GET", "/status-pages/#{id}/announcements", query: query)
311
+ def get_announcement(id, announcement_id) = @c.request("GET", "/status-pages/#{id}/announcements/#{announcement_id}")
312
+ # Publish an incident notice (title, body, optional status, severity,
313
+ # affectedComponentIds); body becomes the first timeline entry.
314
+ def create_announcement(id, body, idempotency_key: nil)
315
+ @c.request("POST", "/status-pages/#{id}/announcements", body: body, idempotency_key: idempotency_key)
316
+ end
317
+ # Post an update (body + status): appends to the public timeline;
318
+ # "resolved" closes it.
319
+ def update_announcement(id, announcement_id, body)
320
+ @c.request("PATCH", "/status-pages/#{id}/announcements/#{announcement_id}", body: body)
321
+ end
322
+ # Remove from the public page (soft delete).
323
+ def delete_announcement(id, announcement_id)
324
+ @c.request("DELETE", "/status-pages/#{id}/announcements/#{announcement_id}")
325
+ end
227
326
  end
228
327
 
229
328
  class OnCall < Namespace
@@ -242,6 +341,7 @@ module Watchfor
242
341
  end
243
342
 
244
343
  class MaintenanceWindows < Namespace
344
+ # state: "active" / "upcoming" keeps only live windows.
245
345
  def list(**query) = @c.request("GET", "/maintenance-windows", query: query)
246
346
  def get(id) = @c.request("GET", "/maintenance-windows/#{id}")
247
347
  def create(body, idempotency_key: nil) = @c.request("POST", "/maintenance-windows", body: body, idempotency_key: idempotency_key)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Watchfor
4
- VERSION = "0.13.0"
4
+ VERSION = "0.15.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: watchfor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - WatchFor
@@ -24,11 +24,11 @@ files:
24
24
  - lib/watchfor.rb
25
25
  - lib/watchfor/client.rb
26
26
  - lib/watchfor/version.rb
27
- homepage: https://watchfor.io/docs/api
27
+ homepage: https://watchfor.io
28
28
  licenses:
29
29
  - MIT
30
30
  metadata:
31
- homepage_uri: https://watchfor.io/docs/api
31
+ homepage_uri: https://watchfor.io
32
32
  documentation_uri: https://watchfor.io/docs/api
33
33
  changelog_uri: https://watchfor.io/changelog
34
34
  rubygems_mfa_required: 'true'