Skip to content

Latest commit

 

History

History
679 lines (574 loc) · 28.1 KB

File metadata and controls

679 lines (574 loc) · 28.1 KB

Calendar API

General

Base URL:

  • production: https://www.shift2bikes.org/api/
  • local development: https://localhost:4443/api/

Most responses are in JSON format, except for:

  • event export returns vCalendar format
  • event crawl returns HTML

Viewing events

Retrieving public event data

Endpoint:

  • GET events

Example requests:

  • /events.php?id=1234
  • /events.php?startdate=2019-06-01&enddate=2019-06-15

URL parameters:

  • id:
    • caldaily event ID
    • if id is provided it takes precedence; all other params will be ignored
  • startdate:
    • first day of range, inclusive
    • YYYY-MM-DD format
    • if not provided, current date is used
  • enddate:
    • last day of range, inclusive
    • YYYY-MM-DD format
    • if not provided, current date is used
  • all:
    • for range requests only (no effect when id is provided)
    • if true, delisted events are included, with minimal information provided in their event object; useful for clients that cache results and need to reconcile events that have been removed

Unknown parameters are ignored.

It is recommended that you always provide either an event id or both startdate and enddate. Relying on default or inferred values may return unexpected results.

Success:

  • status code: 200
  • events: array of event objects; array may be empty
  • each event object: key-value pairs of all available public fields; does not contain any private fields (use manage_event endpoint for those)
  • when using id parameter, array is expected to return 1 object; if the ID does not match a known event, you will receive a 200 response with an empty events array

Example response for a single event:

{
  "events": [
    {
      "id": "6245",
      "title": "Shift to Pedalpalooza Ride",
      "venue": "director park",
      "address": "877 SW park",
      "organizer": "fool",
      "details": "Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website.  There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond.  Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future.  We will end at a family friendly watering hole.  First round of drinks is on shift.  We will be done by 8 so you can check out other rides.",
      "time": "18:00:00",
      "hideemail": "1",
      "length": null,
      "timedetails": null,
      "locdetails": null,
      "eventduration": "120",
      "weburl": null,
      "webname": "shift",
      "image": "/eventimages/6245-3.jpg",
      "audience": "G",
      "tinytitle": "shift2pedalpalooza",
      "printdescr": "learn how to get involved with shift and pedalpalooza",
      "datestype": "O",
      "area": "P",
      "featured": false,
      "printemail": false,
      "printphone": false,
      "printweburl": false,
      "printcontact": false,
      "email": null,
      "phone": null,
      "contact": null,
      "date": "2017-06-05",
      "caldaily_id": "9300",
      "shareable": "https://shift2bikes.org/calendar/event-9300",
      "exportable": "https://shift2bikes.org/api/ics.php?event_id=9300",
      "cancelled": false,
      "newsflash": null,
      "status": "A",
      "endtime": "20:00:00"
    }
  ]
}

Example response for a range of events:

{
  "events": [
    {
      "id": "1234",
      ...
    },
    {
      "id": "1236",
      ...
    },
    {
      "id": "2200",
      ...
    }
  ], 
  "pagination": {
    "start": "2024-07-01",
    "end": "2024-07-10",
    "range": 10,
    "events": 3,
    "prev": "https://www.shift2bikes.org/api/events.php?startdate=2024-06-21&enddate=2024-06-30",
    "next": "https://www.shift2bikes.org/api/events.php?startdate=2024-07-11&enddate=2024-07-20"
  }
}

Example response for a range of events, including delisted events:

{
  "events": [
    {
      // for scheduled and canceled events,
      // all event fields are provided
      "id": "9000",
      "title": "Early Bird New Year's Day Ride",
      ... // all standard fields
      "date": "2025-01-01",
      "caldaily_id": "10101",
      "shareable": "https://shift2bikes.org/calendar/event-10101",
      "cancelled": false,
      "newsflash": null,
      "status": "A",
      "endtime": "10:00:00"
    },
    {
      // for delisted occurrences when there are still valid occurences in the series,
      // all event fields are provided
      "id": "9001",
      "title": "Late-Riser New Year's Day Ride",
      ... // all standard fields
      "date": "2025-01-01",
      "caldaily_id": "10102",
      "shareable": "https://shift2bikes.org/calendar/event-10102",
      "cancelled": true,
      "newsflash": "New start time",
      "status": "D",     // D=delisted
      "endtime": null
    },
    {
      // for delisted occurences when the entire event series has been deleted,
      // only these limited fields are provided
      "id": "9002",
      "deleted": true,
      "date": "2025-01-01",
      "caldaily_id": "10103",
      "shareable": "https://shift2bikes.org/calendar/event-10103",
      "cancelled": true,
      "newsflash": null, // always null when delisted
      "status": "D",     // D=delisted
      "endtime": null
    }
      // if an event is deleted when it is unpublished (aka hidden),
      // the event is entirely gone and is not included here
  ],
  "pagination": {
    "start": "2025-01-01",
    "end": "2025-01-01",
    "range": 1,
    "events": 3,
    "prev": "https://www.shift2bikes.org/api/events.php?startdate=2024-12-31&enddate=2023-12-31",
    "next": "https://www.shift2bikes.org/api/events.php?startdate=2025-01-02&enddate=2025-01-02"
  }
}

Errors:

  • status code: 400
  • error: object containing message key
  • message: text string explaining the error
  • possible errors
    • enddate before startdate
    • date range too large (100 days maximum)

Example error:

{
  "error": {
    "message": "enddate: 2019-06-01 is before startdate: 2019-06-15"
  }
}

Fetching event images

Images must be fetched separately from the initial event request; each event object provides a URL to its image, if it has one.

Some best practices for handling images:

  • Only fetch images if you plan to use them. Especially for range requests, you may not need images for every event in the range.
  • Clients should be able to handle image request failures (e.g. 404, 429, or 5xx errors).
  • Caching is encouraged. The image file has change number after the event ID, e.g. /eventimages/6245-3.jpg. This number is incremented only when the image has changed. If the change number is the same, you can safely use a cached image.

Exporting an event

Endpoint:

  • GET ics

Example request:

  • /ics.php?event_id=9300
  • /ics.php?series_id=6245
  • /ics.php?startdate=2019-06-01&enddate=2019-06-15

URL parameters:

  • event_id:
    • caldaily ID (single occurrence)
    • if event_id is provided it takes precedence; all other params will be ignored
  • series_id:
    • calevent ID (all events in a series)
  • id:
    • alias for series_id
    • deprecated; clients should use event_id or series_id for clarity
  • startdate:
    • first day of range, inclusive
    • YYYY-MM-DD format
    • if not provided, current date is used
  • enddate:
    • last day of range, inclusive
    • YYYY-MM-DD format
    • if not provided, current date is used
  • all:
    • for range requests only (no effect when id is provided)
    • if true, delisted events are included; useful for clients that cache results and need to reconcile events that have been removed
  • filename:
    • if none, the output is plain text instead of an ICS file; useful for local debugging

If multiple ID/range parameters are provided, they take precedence with decreasing specificity: event_id, series_id, startdate/enddate.

If no parameters are provided, it responds with a range from 1 month prior to 6 months forward from the current date.

Unknown parameters are ignored.

Example response for a single event:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//shift2bikes.org//NONSGML shiftcal v2.1//EN
METHOD:PUBLISH
X-WR-CALNAME:Shift Community Calendar
X-WR-CALDESC:Find fun bike events all year round.
X-WR-RELCALID:community@shift2bikes.org
BEGIN:VEVENT
UID:event-9300@shift2bikes.org
SUMMARY:Shift to Pedalpalooza Ride
CONTACT:fool
DESCRIPTION:Have you ever wondered how Pedalpalooza happens every 
 year...and did you know we have a team of programmers who work on the 
 shift calendar and website.  There is a lot of rewarding volunteer work 
 that goes on behind the scenes and we are recruiting for new folks who are
  interested in helping out next year and beyond.  Come on this ride and we
  will talk a little bit about the history of shift and try to find you a 
 place to help out in the future.  We will end at a family friendly 
 watering hole.  First round of drinks is on shift.  We will be done by 8 
 so you can check out other rides.\nhttps://shift2bikes.org/calendar/event-
 9300
LOCATION:director park\n877 SW park
STATUS:CONFIRMED
DTSTART:20170606T010000Z
DTEND:20170606T030000Z
CREATED:20230512T085747Z
DTSTAMP:20170512T050134Z
SEQUENCE:1
URL:https://shift2bikes.org/calendar/event-9300
END:VEVENT
END:VCALENDAR

Errors:

  • status code: 400, 404
  • possible errors
    • event_id or series_id not found
    • enddate before startdate
    • date range too large (100 days maximum)

Crawling an event

Endpoint:

  • GET crawl

Example request:

  • /crawl.php?id=1234

URL parameters:

  • id: caldaily event ID

Unknown parameters are ignored.

This endpoint is used by web crawlers such as search engines.

Success:

  • status code: 200
  • returns a simple HTML rendering of ride data
  • if id parameter is not present, a short, general message about Shift

Example response:

<html>
    <head>
        <title>Shift to Pedalpalooza Ride</title>
        <meta property="og:title" content="Shift to Pedalpalooza Ride">
        <meta property="og:url" content="https://www.shift2bikes.org/calendar/event-9300">
        <meta property="og:image" content="https://www.shift2bikes.org/eventimages/6245.jpg">
        <meta property="og:type" content="article">
        <meta property="og:description" content="Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website.  There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond.  Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future.  We will end at a family friendly watering hole.  First round of drinks is on shift.  We will be done by 8 so you can check out other rides.">
        <meta property="og:site_name" content="SHIFT to Bikes">
        <meta name="description" content="Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website.  There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond.  Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future.  We will end at a family friendly watering hole.  First round of drinks is on shift.  We will be done by 8 so you can check out other rides.">
        <meta name="keywords" content="bikes,fun,friends,Portland,exercise,community,social,events,outdoors">
    </head>
    <body>
        <h2>Mon, Jun 5th, 6:00 PM - Shift to Pedalpalooza Ride</h2>
        <p>Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website.  There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond.  Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future.  We will end at a family friendly watering hole.  First round of drinks is on shift.  We will be done by 8 so you can check out other rides.</p>
        <p>877 SW park</p>
        <img src="https://www.shift2bikes.org/eventimages/6245.jpg">
    </body>
</html>

Errors:

  • status code: 404
  • body of response is empty
  • possible errors
    • id not found
    • id of a hidden (unpublished) event

Managing events

Retrieving all event data

Endpoint:

  • GET retrieve_event

URL parameters:

  • id: calevent event ID
  • secret: event password

The retrieve_event endpoint returns all private data for the event (if the secret is provided) so it can be edited. If you just want to retrieve public data to display the event, use the event endpoint.

Success:

  • status code: 200
  • key-value pairs of all available fields; the response is similar to the event endpoint's event object, but note that they are not identical (the datestatuses block, for example)
  • if a valid secret is provided, all stored values are returned; if not, you still get a 200 response but private fields (e.g. email) will be empty

Example response:

{
  "id": "6245",
  "title": "Shift to Pedalpalooza Ride",
  "venue": "director park",
  "address": "877 SW park",
  "organizer": "fool",
  "details": "Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website.  There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond.  Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future.  We will end at a family friendly watering hole.  First round of drinks is on shift.  We will be done by 8 so you can check out other rides.",
  "time": "18:00:00",
  "hideemail": "1",
  "length": null,
  "timedetails": null,
  "locdetails": null,
  "eventduration": "120",
  "weburl": null,
  "webname": "shift",
  "image": "/eventimages/6245-3.jpg",
  "audience": "G",
  "tinytitle": "shift2pedalpalooza",
  "printdescr": "learn how to get involved with shift and pedalpalooza",
  "datestype": "O",
  "area": "P",
  "featured": false,
  "printemail": false,
  "printphone": false,
  "printweburl": false,
  "printcontact": false,
  "email": "user@example.com",
  "phone": null,
  "contact": null,
  "datestatuses": [
    {
      "id": "9300",
      "date": "2017-06-05",
      "status": "A",
      "newsflash": null
    }
  ]
}

Errors:

  • status code: 400
  • possible errors
    • no id specified
    • id not found

Adding or updating an event

Endpoint:

  • POST manage_event

Request can be sent as JSON, or as multipart/form-data containing binary image data plus JSON.

URL parameters:

  • none

Request body:

  • required fields
    • id: calevent event ID (blank when creating an event, required when updating); set by the server on create; ignored if provided by the user when creating a new event
    • secret: event password (required only when id is provided); set by the server, ignored if provided by the user when creating a new event
    • title: event name
    • details: event description
    • organizer: organizer name
    • email: organizer email
    • venue: location name
    • address: location address; should be mappable with online map services (e.g. Google Maps) or be a valid http/s URL
    • time: event start time
    • datestatuses: array of datestatus objects, one for each event occurrence
      • id: caldaily occurrence ID (blank when creating an occurrence, required when updating); set by the server, ignored if provided by the user when creating a new occurrence
      • date: event date, YYYY-MM-DD format
      • status: A (active, aka scheduled; default) or C (cancelled)
      • newsflash: brief message unique to the occurrence; optional
    • code_of_conduct: boolean; organizer must agree to Shift's Code of Conduct
    • read_comic: boolean; organizer must confirm they have read the Ride Leading Comic
  • optional fields
    • audience: G (General; default), F (Family-friendly), A (Adults-only)
    • safetyplan: boolean; if the organizer pledges to follow Shift's COVID Safety Plan
    • area: P (Portland; default), V (Vancouver WA), W (Westside), E (East Portland), C (Clackamas)
    • timedetails: any additional time details, e.g. if there is a separate meet time and ride time
    • eventduration: duration, in minutes
    • locdetails: any additional time details, e.g. meet by the tennis courts
    • locend: end location details; can be any description, does not have to be mappable (e.g. "Outer Southeast" or "near Beaverton Transit Center")
    • loopride: boolean; if ride end location is the same as the start
    • length: length of ride, in miles; -- for unspecified, or 0-3, 3-8, 8-15, or 15+
    • weburl: http/s URL, e.g. https://pdx.social/@shift2bikes
    • webname: friendly URL name, e.g. @shift2bikes@pdx.social
    • phone: organizer phone number
    • contact: any additional contact info for the organizer; can be a name, an email address, another web URL, social media link, PO Box, or anything else
    • image: URL to the event image, e.g. /eventimages/12345.jpg; set by the server, ignored if provided by the user (see note below for how to add an image)
    • tinytitle: short event name (max 24 characters); used for the Pedalpalooza print calendar, and in some places online where space is tight (e.g. month view); if this is not provided, the first 24 characters of the title field will be automatically copied into this field
    • printdescr: short event description (max 120 characters); used for the Pedalpalooza print calendar, and in some places online where space is tight
    • hideemail: boolean; don't list organizer's email online; default true
    • hidephone: boolean; don't list organizer's phone number online
    • hidecontact: boolean; don't list organizer's additional contact info online
    • printemail: boolean; include organizer's email in the print calendar
    • printphone: boolean; include organizer's email in the print calendar
    • printweburl: boolean; include organizer's email in the print calendar
    • printcontact: boolean; include organizer's email in the print calendar
    • featured: boolean; featured ride status, set by admins and ignored if provided by the user
    • published: boolean; set by the server, ignored if provided by the user

Unknown properties are ignored.

Example JSON request:

{
    "id": "99999",
    "secret": "1234567890abcdef1234567890abcdef",
    "title": "Fun Bike Ride",
    "details": "Funtime biketime get your bike fun on",
    "audience": "G",
    "time": "5:00 PM",
    "timedetails": "",
    "eventduration": "",
    "area": "P",
    "venue": "TBA",
    "address": "TBA",
    "locdetails": "",
    "locend": "",
    "length": "--",
    "organizer": "Josh",
    "email": "test@test.test",
    "hideemail": "1",
    "webname": "",
    "weburl": "",
    "phone": "",
    "contact": "",
    "tinytitle": "Fun Bike Ride",
    "printdescr": "So much fun!",
    "code_of_conduct": "1",
    "read_comic": "1",
    "datestatuses": [
      {
        "id": "",
        "date": "2024-07-07",
        "status": "A",
        "newsflash": ""
      }
    ]
}

Success:

  • status code: 200
  • if a valid id is provided (to update an event), a valid secret must also be included
  • response body is the same as the retrieve_event endpoint

Errors:

  • status code: 400
  • possible errors
    • no request body or not parseable JSON
    • required field was not included, or has an invalid value
    • invalid secret (when updating)

Example error:

{
  "error": {
    "message": "Invalid secret, use link from email"
  }
}

To add an image to the event, use a multipart/form-data request and send the image as binary. Accepts gif, jpeg, pjpeg, and png images, up to 5 MB.

Example multipart/form-data request:

-----------------------------1234123412341234123412341234
Content-Disposition: form-data; name="file"; filename="image.jpg"
Content-Type: image/jpeg

<binary image data>
-----------------------------1234123412341234123412341234
Content-Disposition: form-data; name="json"

{ "id":"99999", ..., "datestatuses": [ { ... } ]}
-----------------------------1234123412341234123412341234--

Success:

  • status code: 200
  • response body is the same as when submitting a JSON-only request

Errors:

  • status code: 400 or 413
  • possible errors
    • unsupported file type (400)
    • file size too large (413)

Example error:

{
  "error": {
    "message": "There were errors in your fields",
    "fields": {
      "file": "The file uploaded is not an image"
    }
  }
}

Deleting an event

Endpoint:

  • POST delete_event

URL parameters:

  • none

Request body:

  • required fields
    • id: calevent event ID
    • secret: event password
  • optional fields
    • none

Unknown properties are ignored.

Example request:

{
    "id": "6245",
    "secret": "example"
}

Success:

  • status code: 200
  • success true message

Example response:

{
    "success": true
}

Errors:

  • status code: 400
  • possible errors
    • no request body or not parseable JSON
    • id not included
    • invalid id
    • invalid or missing secret

Example error:

{
  "error": {
    "message": "Invalid secret, use link from email"
  }
}

Changelog

v1

  • 1.0.0: From Shift formation (c. 2002) until v2 (mid-2017)

There were undoubtedly revisions during this time, but changelog documentation is not available.

v2

  • 2.0.0: (2017-06-09) Launch of the /fun mobile-friendly calendar view, added to the existing PHP-based site

As with v1, there were probably revisions to v2 during this time, but changelog documentation is not available.

v3

  • 3.0.0: (2019-03-14) Launch of Hugo-based site; the API is now fully separated from the front-end
  • 3.0.1: (2019-03-25) Fixed ICS export
  • 3.0.2: (2019-04-01) Events and ICS endpoint documentation
  • 3.1.0: (2019-04-11) Maximum day range (45 days) for Events endpoint
  • 3.2.0: (2019-04-18) Must agree to Code of Conduct to create an event
  • 3.3.0: (2020-02-02) Added more details to ICS export
  • 3.3.1: (2020-03-05) Bug fix for user edits overwriting the "featured" event status set by admins
  • 3.3.2: (2020-04-23) Added temporary blanket cancellation to Events endpoint
  • 3.3.3: (2020-05-21) Bug fixes and documentation for the Retrieve Event
  • 3.4.0: (2020-05-25) Hidden events are excluded from the Events and Crawl endpoints; added error checks on Crawl endpoint
  • 3.4.1: (2020-05-30) Error handling and documentation for Delete Event endpoint
  • 3.5.0: (2020-06-05) Publishing an event requires second post to Manage Event endpoint
  • 3.5.1: (2020-06-07) Updated blanket cancellation date on Events endpoint
  • 3.5.2: (2020-08-13) Removed blanket cancellation from Events endpoint
  • 3.6.0: (2021-03-28) Print details are no longer required to submit an event
  • 3.7.0: (2021-04-21) Increased max day range on Events endpoint to 100 days, to accommodate 3-month Pedalpalooza
  • 3.8.0: (2021-05-19) Added "loop ride" and "location end details" fields
  • 3.9.0: (2021-05-28) Added "COVID safety plan" field
  • 3.10.0: (2023-05-11) iCal feed improvements: better handling of cancelled or deleted events; adds more info to each event
  • 3.10.1: (2023-05-16) Bug fix for soft deleting event occurrences
  • 3.10.2: (2023-05-18) Better enforcement of max image size
  • 3.11.0: (2023-10-09) Cache busting for updated event images
  • 3.12.0: (2024-01-22) New values for Area field (Westside, East Portland, Clackamas); added pagination object to Events endpoint response when requesting a range of events
  • 3.12.1: (2024-02-12) Manage Event (create/update) endpoint documentation
  • 3.13.0: (2024-03-04) Event info now accepts UTF-8 characters (previously limited to latin1 ASCII)
  • 3.50.0: (2024-04-22) Backend now running on Node instead of PHP; no (planned) breaking changes
  • 3.50.1: (2024-04-29) Improved validation of data payloads for manage/delete event endpoint requests
  • 3.50.2: (2024-05-06) Fixed handling of some boolean fields which may unexpectedly be null (hidden, highlight, printemail, etc)
  • 3.51.0: (2024-05-20) Removed now-unused PHP
  • 3.52.0: (2024-06-04) Added all=true param to the events and ICS endpoints to include delisted events with a range request. For the events endpoint, minimal information will be provided in the event object if an event is delisted; useful for clients that cache results and need to reconcile events that have been removed. Also added a filename=none param to the ICS endpoint as a debugging tool, and improved line-wrapping in the iCal feed event descriptions.
  • 3.53.0: (2024-06-24) Increased event image size limit to 5 MB (previously 2 MB).
  • 3.54.0: (2024-07-02) Migrated to latest MySQL LTS version (v8.4).
  • 3.55.0: (2024-08-30) Added year-round calendar iCal feed (at /api/shift-calendar.php), in addition to Pedalpalooza-specific one
  • 3.55.1: (2024-12-09) Terms fields (code_of_conduct, ride_comic) are now only validated on initial submission
  • 3.56.0: (2024-12-13) Max day range is now set in config; prev URL added to pagination object; pagination range now reports an inclusive number of days (e.g. single day range now returns range: 1 instead of 0)
  • 3.56.1: (2025-03-03) Updated dependencies: nginx (patch)
  • 3.56.2: (2025-03-24) Updated dependencies: Node.js (patch) plus 1 of its dependencies
  • 3.56.3: (2025-04-07) Updated dependencies: MySQL (patch)
  • 3.57.0: (2025-06-23) Altered weburl field to allow 512 characters (up from 255)
  • 3.58.0: (2025-08-11) Added experimental ride_count endpoint: provides the number of events in a given time frame, excluding cancelled events. Syntax & usage may not be stable yet.
  • 3.58.1: (2025-09-15) Updated dependencies: nginx
  • 3.59.0: (2025-09-23) Fixed issue with ride length field; now saves, retrieves, and displays correctly
  • 3.59.1: (2025-09-29) Updated MySQL patch version
  • 3.59.2: (2025-10-20) Fixed some backend tests
  • 3.59.3: (2025-11-03) Adjusted search results order when searching past events
  • 3.59.4: (2025-11-17) When mapping an event location, "TBA"/"TBD" addresses are now handled more robustly. Search endpoint now looks at either A) today and future, sort order ascending (default), or B) past only, sort order descending.
  • 3.59.5: (2025-11-24) Updated dependencies: http-proxy-middleware
  • 3.59.6: (2025-12-01) Updated dependencies: Vite
  • 3.59.7: (2025-12-08) Updated dependencies: express, validator, multer
  • 3.59.8: (2025-12-12) Remove now-unneeded version from Docker compose file
  • 3.59.9: (2025-12-22) Updated dependencies: nodemailer
  • 3.59.10: (2026-01-15) Changed dependency management to only allow patch updates; updated dependencies: MySQL. Also removed unused example data.
  • 3.60.0: (2026-02-19) ICS export now supports either single occurrence (event_id; new default) or the series (series_id; previous default). The existing id parameter aliases to series_id for backwards compatibility, but clients are encouraged to specify the ID type explicitly. Also updated Node to v24.x (latest LTS).