gitoriaLog in with ident

calendar

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Main branchmain2881119fcalendar mission 001 (4/4): code order — README layout + same-output test, STATUS, LOG, report; tests/letcount.py (sub-folders), tests/realdata-baseline.mjs, tests/realdata-compare.pymremain/lib/events-helpers.hl

5.0 KB

  1. // lib/events-helpers.hl — what lib/events.hl looks up (calendar mission 001, code order): the size limits, what the
  2. // page sends → a WorldAPI Event record (build), a stored event → what the page shows (viewOf), the temporary ids of
  3. // events made offline, the window check of the faces and the order by start.
  4. import { isText, isDate, isTime, isZone, nextDay, prevDay, before } from '../components/month-view/dates.hl'
  5. import { toReminders, toRows } from '../components/month-view/reminders.hl'
  6. static maxTitle = 200
  7. static maxPlace = 200
  8. static maxNotes = 5000
  9. // what the page sends: { id?, custom, reminders (rows), title, allDay, startDate, startTime, endDate, endTime, place, notes, zone }
  10. // → { error } or { record } (a WorldAPI Event without owner/id). endDate/endTime may be empty (no end); all-day: the
  11. // last day, inclusive ('' = one day).
  12. static build = (b) => {
  13. if (b == null || hlTypeName(b) != 'Hybrid') { return { error = 'the event must be an object' } }
  14. if (!isText(b.title) || b.title.trim() == '') { return { error = 'the event needs a title' } }
  15. if (b.title.length > maxTitle) { return { error = 'the title is too long (at most ' + maxTitle + ' characters)' } }
  16. if (!isText(b.place) || b.place.length > maxPlace) { return { error = 'the place is too long (at most ' + maxPlace + ' characters)' } }
  17. if (!isText(b.notes) || b.notes.length > maxNotes) { return { error = 'the notes are too long (at most ' + maxNotes + ' characters)' } }
  18. if (b.allDay != true && b.allDay != false) { return { error = 'allDay must be true or false' } }
  19. if (!isDate(b.startDate)) { return { error = 'the start date is not a date' } }
  20. if (!isZone(b.zone)) { return { error = 'the time zone is missing' } }
  21. r = {}
  22. r['$type'] = 'event'
  23. r['$version'] = 1
  24. r['$title'] = b.title.trim()
  25. if (b.notes.trim() != '') { r['$description'] = b.notes }
  26. if (b.place.trim() != '') { r['$location'] = b.place.trim() }
  27. r['$status'] = 'confirmed'
  28. r['$visibility'] = 'private'
  29. // no own reminders: `$reminders` stays out and the calendar's apply; own ones (also none at all) are written
  30. if (b.custom == true) {
  31. rem = toReminders(b.reminders)
  32. if (rem.error != null) { return { error = rem.error } }
  33. r['$reminders'] = rem.reminders
  34. }
  35. if (b.allDay) {
  36. r['$kind'] = 'allday'
  37. r['$start'] = b.startDate
  38. if (isText(b.endDate) && b.endDate != '') {
  39. if (!isDate(b.endDate)) { return { error = 'the end date is not a date' } }
  40. if (before(b.endDate, b.startDate)) { return { error = 'the end is before the start' } }
  41. if (b.endDate != b.startDate) { r['$end'] = nextDay(b.endDate) }
  42. }
  43. return { record = r }
  44. }
  45. if (!isTime(b.startTime)) { return { error = 'the start time is not a time' } }
  46. r['$kind'] = 'appointment'
  47. r['$timezone'] = b.zone
  48. r['$start'] = b.startDate + 'T' + b.startTime + ':00'
  49. hasEnd = (isText(b.endDate) && b.endDate != '') || (isText(b.endTime) && b.endTime != '')
  50. if (hasEnd) {
  51. ed = isText(b.endDate) && b.endDate != '' ? b.endDate : b.startDate
  52. if (!isDate(ed)) { return { error = 'the end date is not a date' } }
  53. if (!isTime(b.endTime)) { return { error = 'the end time is not a time' } }
  54. end = ed + 'T' + b.endTime + ':00'
  55. if (before(end, r['$start'])) { return { error = 'the end is before the start' } }
  56. r['$end'] = end
  57. }
  58. return { record = r }
  59. }
  60. // what the page shows of a stored event: first / last day (inclusive), times, texts
  61. static viewOf = (e) => {
  62. let allDay = e['$kind'] == 'allday'
  63. let start = e['$start']
  64. end = e['$end']
  65. let first = start.slice(0, 10)
  66. let last = first
  67. if (allDay) {
  68. if (end != null) { last = prevDay(end) }
  69. } else if (end != null) {
  70. last = end.slice(0, 10)
  71. }
  72. v = { id = e.id title = e['$title'] allDay = allDay first = first last = last
  73. startDate = first startTime = allDay ? '' : start.slice(11, 16)
  74. endDate = end == null ? '' : last endTime = allDay || end == null ? '' : end.slice(11, 16)
  75. place = e['$location'] == null ? '' : e['$location'] notes = e['$description'] == null ? '' : e['$description']
  76. start = start zone = e['$timezone'] == null ? '' : e['$timezone']
  77. custom = e['$reminders'] != null rows = e['$reminders'] == null ? [] : toRows(e['$reminders']) }
  78. return v
  79. }
  80. // an event made offline carries a temporary id `local-…` until the page reloads; the server keeps it as `ref`, so an edit
  81. // (or delete) sent with that id reaches the same event instead of making a second one
  82. static isLocalId = (id) => { return isText(id) && id.length > 6 && id.length <= 60 && id.slice(0, 6) == 'local-' }
  83. // the window is two dates the page shows; anything else is refused
  84. static validWindow = (first, last) => { return isDate(first) && isDate(last) }
  85. // the views ordered by start (a hand-written selection sort)
  86. static byStart = (views) => {
  87. m = views.length
  88. out = []
  89. used = []
  90. let i = 0
  91. while (i < m) { used.push(false) i = i + 1 }
  92. let round = 0
  93. while (round < m) {
  94. let best = -1
  95. let j = 0
  96. while (j < m) {
  97. if (!used[j] && (best < 0 || before(views[j].start, views[best].start))) { best = j }
  98. j = j + 1
  99. }
  100. used[best] = true
  101. out.push(views[best])
  102. round = round + 1
  103. }
  104. return out
  105. }

Branches

Latest commits

  • 2881119fcalendar mission 001 (4/4): code order — README layout + same-output test, STATUS, LOG, report; tests/letcount.py (sub-folders), tests/realdata-baseline.mjs, tests/realdata-compare.pymre
  • 3b2a4cd0calendar mission 001 (3/4): code order — let only where reassigned (147 dropped incl. components/month-view; 110 left: 67 reassigned, 43 loop-bound); gates 78/0 (3 of 4 runs; 1 known 'next month' flake) + 18/0, live-data run = step 2mre
  • f5ce6b1dcalendar mission 001 (2/4): code order — topics, map, thin faces: lib/util.hl, lib/events(-helpers).hl (+calendarView/settingsView/soonOf), lib/settings.hl, lib/users.hl (+userOfLoginCode, tagOf), lib/api(-helpers).hl; project.hl = map; login route takes &req/&sessions (failed-login reason now kept); gates 78/0 + 18/0mre
  • c6fbd011calendar mission 001 (1/4): code order — files moved: lib/events.hl, lib/users.hl, components/styles.hl (imports only); gates 78/0 + 18/0, live-data run identicalmre
  • d6c59290calendar: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 78/0 + 18/0mre
  • 7bd0337ccalendar: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gates 78/0 + 18/0mre
  • ff41310ccalendar: Hybriel master 8efba065 (#126 memory, #48 lambda copy; audit: no & needed)mre
  • 14ba08c7antcolony#40: mission references point to the moved missionsmre
  • 99c73346antcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 76edaa62calendar: Hybriel master ff51cf46 (re-vendor round, static workaround removed)mre
  • 90a3fc2cdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 6722b72ddeploy.sh: never send .git or .gitignore to Byrodinmre
  • be099807State of 2026-09-27, before the move to gitoriamre