nothumanallowed 13.2.64 → 13.2.65

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nothumanallowed",
3
- "version": "13.2.64",
3
+ "version": "13.2.65",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1009,17 +1009,47 @@ export async function cmdUI(args) {
1009
1009
  return;
1010
1010
  }
1011
1011
 
1012
- // GET /api/calendar?date=YYYY-MM-DD
1012
+ // GET /api/calendar?date=YYYY-MM-DD OR ?month=YYYY-MM (loads entire month)
1013
1013
  if (method === 'GET' && pathname === '/api/calendar') {
1014
1014
  try {
1015
1015
  const dateParam = url.searchParams.get('date');
1016
- let events;
1017
- if (dateParam && dateParam !== new Date().toISOString().split('T')[0]) {
1018
- events = await getEventsForDate(config, new Date(dateParam));
1016
+ const monthParam = url.searchParams.get('month'); // e.g. "2026-05"
1017
+ if (monthParam) {
1018
+ // Load entire month across all calendars in one shot
1019
+ const [y, m] = monthParam.split('-').map(Number);
1020
+ const startOfMonth = new Date(y, m - 1, 1);
1021
+ const endOfMonth = new Date(y, m, 1);
1022
+ const { listCalendars: lc, listEvents: le } = await import('../services/google-calendar.mjs');
1023
+ const calendars = await lc(config);
1024
+ const byDate = {};
1025
+ for (const cal of calendars) {
1026
+ if (cal.accessRole === 'freeBusyReader') continue;
1027
+ const isHolidayFeed = cal.id.includes('#holiday@group');
1028
+ try {
1029
+ const evts = await le(config, cal.id, startOfMonth, endOfMonth);
1030
+ for (const e of evts) {
1031
+ e.calendarId = cal.id;
1032
+ e.calendarName = cal.summary;
1033
+ e.readOnly = cal.accessRole === 'reader' || cal.accessRole === 'freeBusyReader';
1034
+ e._isHoliday = isHolidayFeed;
1035
+ const dk = (e.start || '').slice(0, 10);
1036
+ if (!byDate[dk]) byDate[dk] = [];
1037
+ // Dedup holidays per date
1038
+ if (isHolidayFeed && byDate[dk].some(x => x._isHoliday)) continue;
1039
+ byDate[dk].push(e);
1040
+ }
1041
+ } catch { /* skip */ }
1042
+ }
1043
+ sendJSON(res, 200, { byDate, month: monthParam });
1019
1044
  } else {
1020
- events = await getTodayEvents(config);
1045
+ let events;
1046
+ if (dateParam) {
1047
+ events = await getEventsForDate(config, dateParam);
1048
+ } else {
1049
+ events = await getTodayEvents(config);
1050
+ }
1051
+ sendJSON(res, 200, { events, date: dateParam || new Date().toISOString().split('T')[0] });
1021
1052
  }
1022
- sendJSON(res, 200, { events, date: dateParam || new Date().toISOString().split('T')[0] });
1023
1053
  } catch (e) {
1024
1054
  sendJSON(res, 200, { events: [], error: e.message });
1025
1055
  }
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '13.2.64';
8
+ export const VERSION = '13.2.65';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -98,25 +98,27 @@ export async function getTodayEvents(config) {
98
98
  const allEvents = [];
99
99
 
100
100
  for (const cal of calendars) {
101
- if (cal.accessRole === 'freeBusyReader') continue; // skip minimal access
101
+ if (cal.accessRole === 'freeBusyReader') continue;
102
+ const isHolidayFeed = cal.id.includes('#holiday@group');
102
103
  try {
103
104
  const events = await listEvents(config, cal.id, startOfDay, endOfDay);
104
105
  for (const e of events) {
105
106
  e.calendarName = cal.summary;
106
107
  e.calendarId = cal.id;
107
108
  e.readOnly = cal.accessRole === 'reader' || cal.accessRole === 'freeBusyReader';
109
+ e._isHoliday = isHolidayFeed;
108
110
  allEvents.push(e);
109
111
  }
110
112
  } catch { /* skip failed calendars */ }
111
113
  }
112
114
 
113
- // Sort by start time, then deduplicate same-day same-title events (holiday feeds)
114
115
  allEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
115
- const seen = new Set();
116
+ const holidayDates = new Set();
116
117
  return allEvents.filter(e => {
117
- const key = (e.start || '').slice(0, 10) + '|' + (e.summary || '').toLowerCase().trim();
118
- if (seen.has(key)) return false;
119
- seen.add(key);
118
+ if (!e._isHoliday) return true;
119
+ const dateKey = (e.start || '').slice(0, 10);
120
+ if (holidayDates.has(dateKey)) return false;
121
+ holidayDates.add(dateKey);
120
122
  return true;
121
123
  });
122
124
  }
@@ -125,8 +127,15 @@ export async function getTodayEvents(config) {
125
127
  * Get events for a specific date.
126
128
  */
127
129
  export async function getEventsForDate(config, date) {
128
- const d = new Date(date);
129
- const startOfDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
130
+ // Parse date string as LOCAL time to avoid UTC-midnight timezone shift
131
+ let startOfDay;
132
+ if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
133
+ const [y, m, d] = date.split('-').map(Number);
134
+ startOfDay = new Date(y, m - 1, d);
135
+ } else {
136
+ const d = new Date(date);
137
+ startOfDay = new Date(d.getFullYear(), d.getMonth(), d.getDate());
138
+ }
130
139
  const endOfDay = new Date(startOfDay.getTime() + 86400000);
131
140
 
132
141
  // Load from all calendars so calendarId is always accurate
@@ -134,26 +143,30 @@ export async function getEventsForDate(config, date) {
134
143
  const allEvents = [];
135
144
  for (const cal of calendars) {
136
145
  if (cal.accessRole === 'freeBusyReader') continue;
146
+ const isHolidayFeed = cal.id.includes('#holiday@group');
137
147
  try {
138
148
  const events = await listEvents(config, cal.id, startOfDay, endOfDay);
139
149
  for (const e of events) {
140
150
  e.calendarName = cal.summary;
141
151
  e.calendarId = cal.id;
142
152
  e.readOnly = cal.accessRole === 'reader' || cal.accessRole === 'freeBusyReader';
153
+ e._isHoliday = isHolidayFeed;
143
154
  allEvents.push(e);
144
155
  }
145
156
  } catch { /* skip */ }
146
157
  }
147
158
  allEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
148
159
 
149
- // Deduplicate: same start date + same normalized title = same event (e.g. IT + EN holiday feeds)
150
- const seen = new Set();
151
- const deduped = [];
152
- for (const e of allEvents) {
153
- const key = (e.start || '').slice(0, 10) + '|' + (e.summary || '').toLowerCase().trim();
154
- if (!seen.has(key)) { seen.add(key); deduped.push(e); }
155
- }
156
- return deduped;
160
+ // Deduplicate: for holiday feeds, keep only ONE holiday per date
161
+ // (IT + EN feeds have same day, different language titles — keep first)
162
+ const holidayDates = new Set();
163
+ return allEvents.filter(e => {
164
+ if (!e._isHoliday) return true;
165
+ const dateKey = (e.start || '').slice(0, 10);
166
+ if (holidayDates.has(dateKey)) return false;
167
+ holidayDates.add(dateKey);
168
+ return true;
169
+ });
157
170
  }
158
171
 
159
172
  /**
@@ -1115,29 +1115,38 @@ function renderCalendar(el){
1115
1115
 
1116
1116
  // Day headers
1117
1117
  h+='<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-bottom:4px">';
1118
- ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].forEach(function(d){
1119
- h+='<div style="text-align:center;font-size:10px;color:var(--dim);padding:4px">'+d+'</div>';
1118
+ ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'].forEach(function(d,i){
1119
+ var isWe=i>=5;
1120
+ h+='<div style="text-align:center;font-size:10px;color:'+(isWe?'var(--red)':'var(--dim)')+';padding:4px;font-weight:'+(isWe?'600':'400')+'">'+d+'</div>';
1120
1121
  });
1121
1122
  h+='</div>';
1122
1123
 
1123
1124
  // Calendar grid - square cells
1124
1125
  h+='<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:3px">';
1125
1126
  // Empty cells before first day
1126
- for(var i=0;i<startDay;i++){h+='<div style="aspect-ratio:1;background:var(--bg);border-radius:6px"></div>'}
1127
+ for(var i=0;i<startDay;i++){
1128
+ var isWeCol=i>=5;
1129
+ h+='<div style="aspect-ratio:1;background:'+(isWeCol?'rgba(255,80,80,0.04)':'var(--bg)')+';border-radius:6px"></div>';
1130
+ }
1127
1131
  // Day cells
1128
1132
  for(var d=1;d<=daysInMonth;d++){
1129
1133
  var key=calKey(calYear,calMonth,d);
1130
1134
  var today=isToday(calYear,calMonth,d);
1131
1135
  var evts=calEventsCache[key]||[];
1132
1136
  var count=evts.length;
1133
- var bg=today?'var(--greendim)':'var(--bg2)';
1134
- var bdr=today?'var(--green3)':count>0?'var(--amber)':'var(--border)';
1137
+ var dayOfWeek=(startDay+d-1)%7; // 0=Mon … 5=Sat, 6=Sun
1138
+ var isWeekend=dayOfWeek>=5;
1139
+ var hasHoliday=evts.some(function(e){return e._isHoliday||e.readOnly});
1140
+ var bg=today?'var(--greendim)':isWeekend?'rgba(255,80,80,0.06)':'var(--bg2)';
1141
+ var bdr=today?'var(--green3)':hasHoliday?'var(--red)':count>0?'var(--amber)':'var(--border)';
1142
+ var numColor=today?'var(--green)':isWeekend?'var(--red)':'var(--text)';
1135
1143
  h+='<div onclick="openDayDetail(\\x27'+key+'\\x27)" style="aspect-ratio:1;background:'+bg+';border:1px solid '+bdr+';border-radius:6px;padding:6px;cursor:pointer;display:flex;flex-direction:column;overflow:hidden">';
1136
- h+='<div style="font-size:14px;font-weight:'+(today?'800':'500')+';color:'+(today?'var(--green)':'var(--text)')+'">'+d+'</div>';
1144
+ h+='<div style="font-size:14px;font-weight:'+(today?'800':'500')+';color:'+numColor+'">'+d+'</div>';
1137
1145
  if(count>0){
1138
1146
  h+='<div style="flex:1;display:flex;flex-direction:column;justify-content:flex-end;gap:1px;min-height:0">';
1139
1147
  evts.slice(0,2).forEach(function(x){
1140
- h+='<div style="font-size:8px;color:var(--amber);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;background:var(--bg3);border-radius:2px;padding:1px 3px">'+esc(x.summary)+'</div>';
1148
+ var evtColor=x._isHoliday||x.readOnly?'var(--red)':'var(--amber)';
1149
+ h+='<div style="font-size:8px;color:'+evtColor+';overflow:hidden;white-space:nowrap;text-overflow:ellipsis;background:var(--bg3);border-radius:2px;padding:1px 3px">'+esc(x.summary)+'</div>';
1141
1150
  });
1142
1151
  if(count>2)h+='<div style="font-size:8px;color:var(--dim);text-align:center">+'+String(count-2)+'</div>';
1143
1152
  h+='</div>';
@@ -1154,26 +1163,25 @@ function renderCalendar(el){
1154
1163
  }
1155
1164
 
1156
1165
  function loadMonthEvents(){
1166
+ var monthKey=calYear+'-'+String(calMonth+1).padStart(2,'0');
1167
+ // Check if already loaded
1157
1168
  var daysInMonth=new Date(calYear,calMonth+1,0).getDate();
1158
- var promises=[];
1159
- for(var d=1;d<=daysInMonth;d++){
1160
- var key=calKey(calYear,calMonth,d);
1161
- if(!calEventsCache[key]){
1162
- (function(k,day){
1163
- promises.push(apiGet('/api/calendar?date='+k).then(function(r){
1164
- calEventsCache[k]=(r&&r.events)||[];
1165
- }));
1166
- })(key,d);
1169
+ var allLoaded=true;
1170
+ for(var d=1;d<=daysInMonth;d++){if(!calEventsCache[calKey(calYear,calMonth,d)]){allLoaded=false;break}}
1171
+ if(allLoaded){var li=document.getElementById('calLoading');if(li)li.style.display='none';return;}
1172
+
1173
+ apiGet('/api/calendar?month='+monthKey).then(function(r){
1174
+ if(r&&r.byDate){
1175
+ // Fill all days — ensure days with no events get empty array so we don't re-fetch
1176
+ for(var d=1;d<=daysInMonth;d++){
1177
+ var k=calKey(calYear,calMonth,d);
1178
+ calEventsCache[k]=r.byDate[k]||[];
1179
+ }
1167
1180
  }
1168
- }
1169
- if(promises.length===0){
1170
1181
  var li=document.getElementById('calLoading');if(li)li.style.display='none';
1171
- return;
1172
- }
1173
- Promise.all(promises).then(function(){
1174
- var li=document.getElementById('calLoading');if(li)li.style.display='none';
1175
- // Re-render just the grid cells with events
1176
1182
  renderCalendar(document.getElementById('content'));
1183
+ }).catch(function(){
1184
+ var li=document.getElementById('calLoading');if(li)li.style.display='none';
1177
1185
  });
1178
1186
  }
1179
1187