@open-mercato/core 0.6.8-develop.7016.1.4ed7b1e49d → 0.6.8-develop.7019.1.f4c01c4b5c
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +1 -1
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js +3 -2
- package/dist/modules/customers/components/calendar/EventPeekPopover.js.map +2 -2
- package/dist/modules/data_sync/lib/abandoned-run.js +19 -0
- package/dist/modules/data_sync/lib/abandoned-run.js.map +7 -0
- package/dist/modules/data_sync/workers/sync-export.js +5 -1
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +5 -1
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +1 -1
- package/src/modules/customers/components/calendar/EventPeekPopover.tsx +4 -2
- package/src/modules/customers/i18n/de.json +1 -1
- package/src/modules/customers/i18n/en.json +1 -1
- package/src/modules/customers/i18n/es.json +1 -1
- package/src/modules/customers/i18n/ko.json +1 -1
- package/src/modules/customers/i18n/pl.json +1 -1
- package/src/modules/data_sync/lib/abandoned-run.ts +56 -0
- package/src/modules/data_sync/workers/sync-export.ts +4 -0
- package/src/modules/data_sync/workers/sync-import.ts +4 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -45,7 +45,7 @@ function CalendarSettingsModal({
|
|
|
45
45
|
const toggle = (key) => (checked) => setDraft((current) => ({ ...current, [key]: checked }));
|
|
46
46
|
const toggleRows = [
|
|
47
47
|
{ key: "showCrmActivities", label: t("customers.calendar.settings.showCrmActivities", "Show CRM activities on calendar") },
|
|
48
|
-
{ key: "aiSummaries", label: t("customers.calendar.settings.aiSummaries", "AI summaries
|
|
48
|
+
{ key: "aiSummaries", label: t("customers.calendar.settings.aiSummaries", "AI summaries") },
|
|
49
49
|
{ key: "conflictWarnings", label: t("customers.calendar.settings.conflictWarnings", "Conflict warnings") },
|
|
50
50
|
{ key: "showWeekends", label: t("customers.calendar.settings.showWeekends", "Show weekends") }
|
|
51
51
|
];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/calendar/CalendarSettingsModal.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Info, X } from 'lucide-react'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { TagInput } from '@open-mercato/ui/primitives/tag-input'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport {\n CalendarPreferences,\n ConflictScope,\n MAX_ACTIVITY_TYPES,\n MAX_EVENT_CATEGORIES,\n} from '../../lib/calendar/preferences'\nimport { SegmentGroup } from './editor/SegmentGroup'\n\nexport type CalendarSettingsModalProps = {\n open: boolean\n preferences: CalendarPreferences\n seedActivityTypes: string[]\n onOpenChange(open: boolean): void\n onSave(next: CalendarPreferences): void\n}\n\ntype ToggleKey = 'showCrmActivities' | 'aiSummaries' | 'conflictWarnings' | 'showWeekends'\n\n// An empty Activity Types list is an intentional floor meaning \"surface all\n// dictionary types\" rather than \"surface none\". When the stored list is empty\n// the modal seeds the dictionary types for display. NOTE: since the editor's\n// Category quick-pick was removed (owner feedback, #3552) these lists no longer\n// affect the event editor \u2014 the type switcher always shows the full dictionary.\nfunction buildDraft(preferences: CalendarPreferences, seedActivityTypes: string[]): CalendarPreferences {\n return {\n ...preferences,\n eventCategories: [...preferences.eventCategories],\n activityTypes:\n preferences.activityTypes.length > 0\n ? [...preferences.activityTypes]\n : seedActivityTypes.slice(0, MAX_ACTIVITY_TYPES),\n }\n}\n\nexport function CalendarSettingsModal({\n open,\n preferences,\n seedActivityTypes,\n onOpenChange,\n onSave,\n}: CalendarSettingsModalProps) {\n const t = useT()\n const [draft, setDraft] = React.useState<CalendarPreferences>(() => buildDraft(preferences, seedActivityTypes))\n const openRef = React.useRef(false)\n\n React.useEffect(() => {\n if (open && !openRef.current) setDraft(buildDraft(preferences, seedActivityTypes))\n openRef.current = open\n }, [open, preferences, seedActivityTypes])\n\n const handleSave = React.useCallback(() => {\n onSave(draft)\n onOpenChange(false)\n }, [draft, onOpenChange, onSave])\n\n const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave })\n\n const toggle = (key: ToggleKey) => (checked: boolean) => setDraft((current) => ({ ...current, [key]: checked }))\n\n const toggleRows: Array<{ key: ToggleKey; label: string }> = [\n { key: 'showCrmActivities', label: t('customers.calendar.settings.showCrmActivities', 'Show CRM activities on calendar') },\n { key: 'aiSummaries', label: t('customers.calendar.settings.aiSummaries', 'AI summaries
|
|
5
|
-
"mappings": ";AA8FU,cAGA,YAHA;AA5FV,YAAY,WAAW;AACvB,SAAS,MAAM,SAAS;AACxB,SAAS,sBAAsB;AAC/B,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,QAAQ,eAAe,mBAAmB;AACnD,SAAS,kBAAkB;AAC3B,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,2BAA2B;AACpC;AAAA,EAGE;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAiB7B,SAAS,WAAW,aAAkC,mBAAkD;AACtG,SAAO;AAAA,IACL,GAAG;AAAA,IACH,iBAAiB,CAAC,GAAG,YAAY,eAAe;AAAA,IAChD,eACE,YAAY,cAAc,SAAS,IAC/B,CAAC,GAAG,YAAY,aAAa,IAC7B,kBAAkB,MAAM,GAAG,kBAAkB;AAAA,EACrD;AACF;AAEO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA+B;AAC7B,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAA8B,MAAM,WAAW,aAAa,iBAAiB,CAAC;AAC9G,QAAM,UAAU,MAAM,OAAO,KAAK;AAElC,QAAM,UAAU,MAAM;AACpB,QAAI,QAAQ,CAAC,QAAQ,QAAS,UAAS,WAAW,aAAa,iBAAiB,CAAC;AACjF,YAAQ,UAAU;AAAA,EACpB,GAAG,CAAC,MAAM,aAAa,iBAAiB,CAAC;AAEzC,QAAM,aAAa,MAAM,YAAY,MAAM;AACzC,WAAO,KAAK;AACZ,iBAAa,KAAK;AAAA,EACpB,GAAG,CAAC,OAAO,cAAc,MAAM,CAAC;AAEhC,QAAM,gBAAgB,oBAAoB,EAAE,WAAW,WAAW,CAAC;AAEnE,QAAM,SAAS,CAAC,QAAmB,CAAC,YAAqB,SAAS,CAAC,aAAa,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,QAAQ,EAAE;AAE/G,QAAM,aAAuD;AAAA,IAC3D,EAAE,KAAK,qBAAqB,OAAO,EAAE,iDAAiD,iCAAiC,EAAE;AAAA,IACzH,EAAE,KAAK,eAAe,OAAO,EAAE,2CAA2C,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Info, X } from 'lucide-react'\nimport { VisuallyHidden } from '@radix-ui/react-visually-hidden'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Dialog, DialogContent, DialogTitle } from '@open-mercato/ui/primitives/dialog'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { TagInput } from '@open-mercato/ui/primitives/tag-input'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'\nimport {\n CalendarPreferences,\n ConflictScope,\n MAX_ACTIVITY_TYPES,\n MAX_EVENT_CATEGORIES,\n} from '../../lib/calendar/preferences'\nimport { SegmentGroup } from './editor/SegmentGroup'\n\nexport type CalendarSettingsModalProps = {\n open: boolean\n preferences: CalendarPreferences\n seedActivityTypes: string[]\n onOpenChange(open: boolean): void\n onSave(next: CalendarPreferences): void\n}\n\ntype ToggleKey = 'showCrmActivities' | 'aiSummaries' | 'conflictWarnings' | 'showWeekends'\n\n// An empty Activity Types list is an intentional floor meaning \"surface all\n// dictionary types\" rather than \"surface none\". When the stored list is empty\n// the modal seeds the dictionary types for display. NOTE: since the editor's\n// Category quick-pick was removed (owner feedback, #3552) these lists no longer\n// affect the event editor \u2014 the type switcher always shows the full dictionary.\nfunction buildDraft(preferences: CalendarPreferences, seedActivityTypes: string[]): CalendarPreferences {\n return {\n ...preferences,\n eventCategories: [...preferences.eventCategories],\n activityTypes:\n preferences.activityTypes.length > 0\n ? [...preferences.activityTypes]\n : seedActivityTypes.slice(0, MAX_ACTIVITY_TYPES),\n }\n}\n\nexport function CalendarSettingsModal({\n open,\n preferences,\n seedActivityTypes,\n onOpenChange,\n onSave,\n}: CalendarSettingsModalProps) {\n const t = useT()\n const [draft, setDraft] = React.useState<CalendarPreferences>(() => buildDraft(preferences, seedActivityTypes))\n const openRef = React.useRef(false)\n\n React.useEffect(() => {\n if (open && !openRef.current) setDraft(buildDraft(preferences, seedActivityTypes))\n openRef.current = open\n }, [open, preferences, seedActivityTypes])\n\n const handleSave = React.useCallback(() => {\n onSave(draft)\n onOpenChange(false)\n }, [draft, onOpenChange, onSave])\n\n const handleKeyDown = useDialogKeyHandler({ onConfirm: handleSave })\n\n const toggle = (key: ToggleKey) => (checked: boolean) => setDraft((current) => ({ ...current, [key]: checked }))\n\n const toggleRows: Array<{ key: ToggleKey; label: string }> = [\n { key: 'showCrmActivities', label: t('customers.calendar.settings.showCrmActivities', 'Show CRM activities on calendar') },\n { key: 'aiSummaries', label: t('customers.calendar.settings.aiSummaries', 'AI summaries') },\n { key: 'conflictWarnings', label: t('customers.calendar.settings.conflictWarnings', 'Conflict warnings') },\n { key: 'showWeekends', label: t('customers.calendar.settings.showWeekends', 'Show weekends') },\n ]\n\n const title = t('customers.calendar.settings.title', 'Customization')\n const removeTagLabel = React.useCallback(\n (tag: string) => t('customers.calendar.settings.removeTag', 'Remove {tag}', { tag }),\n [t],\n )\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent\n onKeyDown={handleKeyDown}\n aria-describedby={undefined}\n dismissible={false}\n className=\"flex w-full max-w-[400px] flex-col gap-0 overflow-hidden rounded-2xl border-0 bg-card p-0 shadow-xl\"\n >\n <VisuallyHidden>\n <DialogTitle>{title}</DialogTitle>\n </VisuallyHidden>\n <div className=\"flex shrink-0 items-start gap-3.5 border-b border-border py-4 pl-5 pr-4\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <p className=\"text-sm font-medium leading-5 text-foreground\">{title}</p>\n <p className=\"text-xs leading-4 text-muted-foreground\">\n {t('customers.calendar.settings.subtitle', 'Customise your calendar module.')}\n </p>\n </div>\n <IconButton\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => onOpenChange(false)}\n aria-label={t('customers.calendar.settings.close', 'Close')}\n className=\"shrink-0 text-muted-foreground\"\n >\n <X aria-hidden className=\"size-5\" />\n </IconButton>\n </div>\n\n <div className=\"flex flex-col gap-4 p-5\">\n <SettingsTagInput\n label={t('customers.calendar.settings.eventCategories', 'Event Categories')}\n maxLabel={t('customers.calendar.settings.max', '(max. {count})', { count: MAX_EVENT_CATEGORIES })}\n hint={t(\n 'customers.calendar.settings.eventCategoriesHint',\n 'Your own grouping labels (e.g. Team Meeting, Sales Call). Offered when creating an event.',\n )}\n placeholder={t('customers.calendar.settings.addCategory', 'Add a category\u2026')}\n value={draft.eventCategories}\n maxTags={MAX_EVENT_CATEGORIES}\n removeTagLabel={removeTagLabel}\n onChange={(eventCategories) => setDraft((current) => ({ ...current, eventCategories }))}\n />\n <SettingsTagInput\n label={t('customers.calendar.settings.activityTypes', 'Activity Types')}\n maxLabel={t('customers.calendar.settings.max', '(max. {count})', { count: MAX_ACTIVITY_TYPES })}\n hint={t(\n 'customers.calendar.settings.activityTypesHint',\n 'The activity types your calendar surfaces when creating an event. Seeded from your workspace dictionary.',\n )}\n placeholder={t('customers.calendar.settings.addType', 'Add a type\u2026')}\n value={draft.activityTypes}\n maxTags={MAX_ACTIVITY_TYPES}\n removeTagLabel={removeTagLabel}\n onChange={(activityTypes) => setDraft((current) => ({ ...current, activityTypes }))}\n />\n {toggleRows.map((row) => (\n <React.Fragment key={row.key}>\n <div className=\"flex items-center gap-2\">\n <Switch\n checked={draft[row.key]}\n onCheckedChange={toggle(row.key)}\n aria-label={row.label}\n />\n <span className=\"text-sm leading-5 text-foreground\">{row.label}</span>\n </div>\n {row.key === 'conflictWarnings' && draft.conflictWarnings ? (\n <div className=\"flex flex-col gap-1.5 pl-11\">\n <span className=\"text-xs leading-4 text-muted-foreground\">\n {t(\n 'customers.calendar.settings.conflictScopeHint',\n 'Choose whose overlaps the calendar flags as conflicts.',\n )}\n </span>\n <SegmentGroup<ConflictScope>\n ariaLabel={t('customers.calendar.settings.conflictScope', 'Conflict scope')}\n value={draft.conflictScope}\n onChange={(conflictScope) => setDraft((current) => ({ ...current, conflictScope }))}\n options={[\n { value: 'mine', label: t('customers.calendar.settings.conflictScopeMine', 'My meetings') },\n { value: 'all', label: t('customers.calendar.settings.conflictScopeAll', 'All meetings') },\n ]}\n />\n </div>\n ) : null}\n </React.Fragment>\n ))}\n </div>\n\n <div className=\"flex shrink-0 items-center gap-3 border-t border-border px-5 py-4\">\n <Button type=\"button\" variant=\"outline\" className=\"flex-1\" onClick={() => onOpenChange(false)}>\n {t('customers.calendar.settings.cancel', 'Cancel')}\n </Button>\n <Button type=\"button\" className=\"flex-1\" onClick={handleSave}>\n {t('customers.calendar.settings.save', 'Save Changes')}\n </Button>\n </div>\n </DialogContent>\n </Dialog>\n )\n}\n\ntype SettingsTagInputProps = {\n label: string\n maxLabel: string\n hint: string\n placeholder: string\n value: string[]\n maxTags: number\n removeTagLabel: (tag: string) => string\n onChange(value: string[]): void\n}\n\nfunction SettingsTagInput({ label, maxLabel, hint, placeholder, value, maxTags, removeTagLabel, onChange }: SettingsTagInputProps) {\n return (\n <div className=\"flex flex-col gap-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"text-sm font-medium leading-5 text-foreground\">{label}</span>\n <span className=\"text-sm leading-5 text-muted-foreground\">{maxLabel}</span>\n <SimpleTooltip content={hint}>\n <span className=\"inline-flex text-muted-foreground\" tabIndex={0} role=\"img\" aria-label={hint}>\n <Info aria-hidden className=\"size-4\" />\n </span>\n </SimpleTooltip>\n </div>\n <TagInput\n value={value}\n onChange={onChange}\n placeholder={placeholder}\n maxTags={maxTags}\n aria-label={label}\n removeTagLabel={removeTagLabel}\n />\n </div>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -19,6 +19,7 @@ function EventPeekPopover({
|
|
|
19
19
|
joinUrl,
|
|
20
20
|
aiSummaries,
|
|
21
21
|
canManage = true,
|
|
22
|
+
side = "right",
|
|
22
23
|
onOpenChange,
|
|
23
24
|
onJoin,
|
|
24
25
|
onEdit,
|
|
@@ -39,10 +40,10 @@ function EventPeekPopover({
|
|
|
39
40
|
(part) => Boolean(part)
|
|
40
41
|
);
|
|
41
42
|
const showSummary = aiSummaries && metaParts.length > 0;
|
|
42
|
-
const showJoin =
|
|
43
|
+
const showJoin = Boolean(joinUrl);
|
|
43
44
|
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange, children: [
|
|
44
45
|
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children }),
|
|
45
|
-
/* @__PURE__ */ jsx(PopoverContent, { align: "start", side
|
|
46
|
+
/* @__PURE__ */ jsx(PopoverContent, { align: "start", side, sideOffset: 8, className: "w-56 p-3", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
46
47
|
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold leading-5 text-foreground", children: title }),
|
|
47
48
|
/* @__PURE__ */ jsx("p", { className: "text-xs leading-4 text-muted-foreground", children: `${dateLabel} \xB7 ${timeRange}` }),
|
|
48
49
|
showSummary ? /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-1 text-xs leading-4 text-muted-foreground", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/calendar/EventPeekPopover.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { formatTimeRange } from './EventBlock'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst PLATFORM_FULL_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platformFull.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platformFull.meet', fallback: 'Google Meet' },\n slack: { key: 'customers.calendar.platformFull.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platformFull.teams', fallback: 'Microsoft Teams' },\n}\n\nexport type EventPeekPopoverProps = {\n item: CalendarItem\n open: boolean\n joinUrl: string | null\n aiSummaries: boolean\n canManage?: boolean\n onOpenChange(open: boolean): void\n onJoin(item: CalendarItem): void\n onEdit(item: CalendarItem): void\n children: React.ReactNode\n}\n\nexport function EventPeekPopover({\n item,\n open,\n joinUrl,\n aiSummaries,\n canManage = true,\n onOpenChange,\n onJoin,\n onEdit,\n children,\n}: EventPeekPopoverProps) {\n const t = useT()\n const locale = useLocale()\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n const dateLabel = React.useMemo(\n () => new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(item.start),\n [locale, item.start],\n )\n const timeRange = item.allDay\n ? t('customers.calendar.grid.allDay', 'All day')\n : formatTimeRange(locale, item.start, item.end)\n\n const attendeeCount = item.participants.length\n const attendeesLabel =\n attendeeCount === 1\n ? t('customers.calendar.peek.attendee', '1 attendee')\n : t('customers.calendar.peek.attendees', '{count} attendees', { count: attendeeCount })\n const platformLabel =\n item.locationKind === 'platform' && item.platform\n ? t(PLATFORM_FULL_LABELS[item.platform].key, PLATFORM_FULL_LABELS[item.platform].fallback)\n : null\n const metaParts = [platformLabel, attendeeCount > 0 ? attendeesLabel : null].filter(\n (part): part is string => Boolean(part),\n )\n const showSummary = aiSummaries && metaParts.length > 0\n const showJoin =
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'\nimport { formatTimeRange } from './EventBlock'\nimport type { CalendarItem, CalendarPlatform } from './types'\n\nconst PLATFORM_FULL_LABELS: Record<CalendarPlatform, { key: string; fallback: string }> = {\n zoom: { key: 'customers.calendar.platformFull.zoom', fallback: 'Zoom' },\n meet: { key: 'customers.calendar.platformFull.meet', fallback: 'Google Meet' },\n slack: { key: 'customers.calendar.platformFull.slack', fallback: 'Slack' },\n teams: { key: 'customers.calendar.platformFull.teams', fallback: 'Microsoft Teams' },\n}\n\nexport type EventPeekPopoverProps = {\n item: CalendarItem\n open: boolean\n joinUrl: string | null\n aiSummaries: boolean\n canManage?: boolean\n side?: React.ComponentProps<typeof PopoverContent>['side']\n onOpenChange(open: boolean): void\n onJoin(item: CalendarItem): void\n onEdit(item: CalendarItem): void\n children: React.ReactNode\n}\n\nexport function EventPeekPopover({\n item,\n open,\n joinUrl,\n aiSummaries,\n canManage = true,\n side = 'right',\n onOpenChange,\n onJoin,\n onEdit,\n children,\n}: EventPeekPopoverProps) {\n const t = useT()\n const locale = useLocale()\n const title = item.title || t('customers.calendar.grid.untitled', 'Untitled')\n const dateLabel = React.useMemo(\n () => new Intl.DateTimeFormat(locale, { weekday: 'short', month: 'short', day: 'numeric' }).format(item.start),\n [locale, item.start],\n )\n const timeRange = item.allDay\n ? t('customers.calendar.grid.allDay', 'All day')\n : formatTimeRange(locale, item.start, item.end)\n\n const attendeeCount = item.participants.length\n const attendeesLabel =\n attendeeCount === 1\n ? t('customers.calendar.peek.attendee', '1 attendee')\n : t('customers.calendar.peek.attendees', '{count} attendees', { count: attendeeCount })\n const platformLabel =\n item.locationKind === 'platform' && item.platform\n ? t(PLATFORM_FULL_LABELS[item.platform].key, PLATFORM_FULL_LABELS[item.platform].fallback)\n : null\n const metaParts = [platformLabel, attendeeCount > 0 ? attendeesLabel : null].filter(\n (part): part is string => Boolean(part),\n )\n const showSummary = aiSummaries && metaParts.length > 0\n const showJoin = Boolean(joinUrl)\n\n return (\n <Popover open={open} onOpenChange={onOpenChange}>\n <PopoverTrigger asChild>{children}</PopoverTrigger>\n <PopoverContent align=\"start\" side={side} sideOffset={8} className=\"w-56 p-3\">\n <div className=\"flex flex-col gap-2\">\n <p className=\"text-sm font-semibold leading-5 text-foreground\">{title}</p>\n <p className=\"text-xs leading-4 text-muted-foreground\">{`${dateLabel} \u00B7 ${timeRange}`}</p>\n {showSummary ? (\n <p className=\"flex items-center gap-1 text-xs leading-4 text-muted-foreground\">\n <Sparkles aria-hidden className=\"size-3.5 shrink-0\" />\n <span className=\"truncate\">{metaParts.join(' \u00B7 ')}</span>\n </p>\n ) : null}\n <div className=\"flex items-center gap-2 pt-1\">\n {showJoin ? (\n <Button type=\"button\" size=\"sm\" onClick={() => onJoin(item)}>\n {t('customers.calendar.peek.join', 'Join')}\n </Button>\n ) : null}\n {canManage ? (\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => {\n onOpenChange(false)\n onEdit(item)\n }}\n >\n {t('customers.calendar.peek.edit', 'Edit')}\n </Button>\n ) : (\n <SimpleTooltip\n content={t(\n 'customers.calendar.peek.editForbidden',\n \"You don't have permission to edit events\",\n )}\n >\n <span className=\"inline-flex\">\n <Button type=\"button\" size=\"sm\" variant=\"secondary\" disabled className=\"pointer-events-none\">\n {t('customers.calendar.peek.edit', 'Edit')}\n </Button>\n </span>\n </SimpleTooltip>\n )}\n </div>\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
async function failAbandonedRun(payload, info) {
|
|
2
|
+
const data = payload;
|
|
3
|
+
const runId = data?.payload?.runId;
|
|
4
|
+
const scope = data?.payload?.scope;
|
|
5
|
+
if (typeof runId !== "string" || typeof scope?.organizationId !== "string" || typeof scope?.tenantId !== "string") return;
|
|
6
|
+
const runScope = { organizationId: scope.organizationId, tenantId: scope.tenantId };
|
|
7
|
+
const errorMessage = `the queue abandoned this run's job without running it: ${info.reason}`;
|
|
8
|
+
const { createRequestContainer } = await import("@open-mercato/shared/lib/di/container");
|
|
9
|
+
const container = await createRequestContainer();
|
|
10
|
+
const runService = container.resolve("dataSyncRunService");
|
|
11
|
+
const run = await runService.markStatus(runId, "failed", runScope, errorMessage);
|
|
12
|
+
if (!run?.progressJobId) return;
|
|
13
|
+
const progressService = container.resolve("progressService");
|
|
14
|
+
await progressService.failJob(run.progressJobId, { errorMessage }, runScope);
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
failAbandonedRun
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=abandoned-run.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/data_sync/lib/abandoned-run.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AbandonedJobInfo } from '@open-mercato/queue'\n\n/**\n * Fail the run behind a job the queue gave up on.\n *\n * A resumable data_sync job is ONE long-lived job for a whole run \u2014 `runImport` is entered once and\n * stays there for the duration, which for a full backfill is days. Every worker death (a deploy, an\n * OOM) therefore stalls that job, and BullMQ's stalled counter is cumulative for the job's life:\n * past `DATA_SYNC_MAX_STALLED_COUNT` it writes a deferred failure and the next worker fails the job\n * BEFORE calling the processor.\n *\n * So `runImport` never runs, never throws, and never finalizes \u2014 and `sync_runs` is left saying\n * `running` for a run nothing is running, forever. That is the residual state `queue-policy.ts`\n * describes as what remains after a job exhausts its stall budget; this repairs it.\n *\n * It only ever moves a non-terminal run: `markStatus` refuses to overwrite `completed` / `failed` /\n * `cancelled`, so a job abandoned after its run has ended is a no-op. `onJobAbandoned` may deliver\n * the same job more than once, which for the same reason is also a no-op.\n *\n * The run's progress job is failed alongside it, the way the worker's own catch path does in\n * `workers/sync-import.ts`. Without that the two halves disagree \u2014 the run reads `failed` while the\n * operation still shows as in flight in the top bar \u2014 until the progress staleness sweep eventually\n * fails it with a generic no-heartbeat message instead of the reason the queue recorded.\n */\nexport async function failAbandonedRun(payload: unknown, info: AbandonedJobInfo): Promise<void> {\n const data = payload as { payload?: { runId?: unknown; scope?: { organizationId?: unknown; tenantId?: unknown } } } | undefined\n const runId = data?.payload?.runId\n const scope = data?.payload?.scope\n // Nothing to repair without both: the run row is keyed by id AND tenant scope.\n if (typeof runId !== 'string' || typeof scope?.organizationId !== 'string' || typeof scope?.tenantId !== 'string') return\n\n const runScope = { organizationId: scope.organizationId, tenantId: scope.tenantId }\n const errorMessage = `the queue abandoned this run's job without running it: ${info.reason}`\n\n const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')\n const container = await createRequestContainer()\n const runService = container.resolve('dataSyncRunService') as {\n markStatus(\n runId: string,\n status: string,\n scope: { organizationId: string; tenantId: string },\n error?: string,\n ): Promise<{ progressJobId?: string | null } | null>\n }\n const run = await runService.markStatus(runId, 'failed', runScope, errorMessage)\n if (!run?.progressJobId) return\n\n const progressService = container.resolve('progressService') as {\n failJob(\n jobId: string,\n input: { errorMessage: string },\n ctx: { organizationId: string; tenantId: string },\n ): Promise<unknown>\n }\n await progressService.failJob(run.progressJobId, { errorMessage }, runScope)\n}\n"],
|
|
5
|
+
"mappings": "AAwBA,eAAsB,iBAAiB,SAAkB,MAAuC;AAC9F,QAAM,OAAO;AACb,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,QAAQ,MAAM,SAAS;AAE7B,MAAI,OAAO,UAAU,YAAY,OAAO,OAAO,mBAAmB,YAAY,OAAO,OAAO,aAAa,SAAU;AAEnH,QAAM,WAAW,EAAE,gBAAgB,MAAM,gBAAgB,UAAU,MAAM,SAAS;AAClF,QAAM,eAAe,0DAA0D,KAAK,MAAM;AAE1F,QAAM,EAAE,uBAAuB,IAAI,MAAM,OAAO,uCAAuC;AACvF,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,aAAa,UAAU,QAAQ,oBAAoB;AAQzD,QAAM,MAAM,MAAM,WAAW,WAAW,OAAO,UAAU,UAAU,YAAY;AAC/E,MAAI,CAAC,KAAK,cAAe;AAEzB,QAAM,kBAAkB,UAAU,QAAQ,iBAAiB;AAO3D,QAAM,gBAAgB,QAAQ,IAAI,eAAe,EAAE,aAAa,GAAG,QAAQ;AAC7E;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DATA_SYNC_LOCK_DURATION_MS,
|
|
4
4
|
DATA_SYNC_MAX_STALLED_COUNT
|
|
5
5
|
} from "../lib/queue-policy.js";
|
|
6
|
+
import { failAbandonedRun } from "../lib/abandoned-run.js";
|
|
6
7
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
8
|
const logger = createLogger("data_sync").child({ component: "sync-export" });
|
|
8
9
|
const metadata = {
|
|
@@ -10,7 +11,10 @@ const metadata = {
|
|
|
10
11
|
id: "data-sync:export",
|
|
11
12
|
concurrency: 5,
|
|
12
13
|
lockDuration: DATA_SYNC_LOCK_DURATION_MS,
|
|
13
|
-
maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT
|
|
14
|
+
maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,
|
|
15
|
+
// Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker
|
|
16
|
+
// restart, and the process that restarts never constructs the producer's queue instance.
|
|
17
|
+
onJobAbandoned: failAbandonedRun
|
|
14
18
|
};
|
|
15
19
|
async function handle(job, ctx) {
|
|
16
20
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/data_sync/workers/sync-export.ts"],
|
|
4
|
-
"sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_EXPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-export' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_EXPORT_QUEUE,\n id: 'data-sync:export',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runExport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync export worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed export worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
|
|
5
|
-
"mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_EXPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { failAbandonedRun } from '../lib/abandoned-run'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-export' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_EXPORT_QUEUE,\n id: 'data-sync:export',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n // Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker\n // restart, and the process that restarts never constructs the producer's queue instance.\n onJobAbandoned: failAbandonedRun,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runExport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync export worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed export worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
|
|
5
|
+
"mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA;AAAA;AAAA,EAGjB,gBAAgB;AAClB;AAMA,eAAO,OAA8B,KAAgC,KAAoC;AACvG,MAAI;AACF,UAAM,SAAS,IAAI,QAAoB,gBAAgB;AACvD,UAAM,OAAO,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,EACpF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,aAAa,iBAAiB,QAAQ,MAAM,QAAQ;AAE1D,QAAI;AACF,YAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,YAAM,kBAAkB,IAAI,QAAyB,iBAAiB;AACtE,YAAM,MAAM,MAAM,eAAe,OAAO,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK;AAE5E,UAAI,OAAO,IAAI,WAAW,eAAe,IAAI,WAAW,YAAY,IAAI,WAAW,aAAa;AAC9F,cAAM,eAAe,WAAW,IAAI,IAAI,UAAU,IAAI,QAAQ,OAAO,OAAO;AAC5E,YAAI,IAAI,eAAe;AACrB,gBAAM,gBAAgB;AAAA,YACpB,IAAI;AAAA,YACJ;AAAA,cACE,cAAc;AAAA,cACd;AAAA,YACF;AAAA,YACA,IAAI,QAAQ;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,eAAe;AACtB,aAAO,MAAM,gDAAgD,EAAE,KAAK,cAAc,CAAC;AAAA,IACrF;AAEA,UAAM;AAAA,EACR;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DATA_SYNC_LOCK_DURATION_MS,
|
|
4
4
|
DATA_SYNC_MAX_STALLED_COUNT
|
|
5
5
|
} from "../lib/queue-policy.js";
|
|
6
|
+
import { failAbandonedRun } from "../lib/abandoned-run.js";
|
|
6
7
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
7
8
|
const logger = createLogger("data_sync").child({ component: "sync-import" });
|
|
8
9
|
const metadata = {
|
|
@@ -10,7 +11,10 @@ const metadata = {
|
|
|
10
11
|
id: "data-sync:import",
|
|
11
12
|
concurrency: 5,
|
|
12
13
|
lockDuration: DATA_SYNC_LOCK_DURATION_MS,
|
|
13
|
-
maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT
|
|
14
|
+
maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,
|
|
15
|
+
// Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker
|
|
16
|
+
// restart, and the process that restarts never constructs the producer's queue instance.
|
|
17
|
+
onJobAbandoned: failAbandonedRun
|
|
14
18
|
};
|
|
15
19
|
async function handle(job, ctx) {
|
|
16
20
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/data_sync/workers/sync-import.ts"],
|
|
4
|
-
"sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_IMPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-import' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_IMPORT_QUEUE,\n id: 'data-sync:import',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runImport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync import worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed import worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
|
|
5
|
-
"mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncEngine } from '../lib/sync-engine'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport {\n DATA_SYNC_IMPORT_QUEUE,\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n} from '../lib/queue-policy'\nimport { failAbandonedRun } from '../lib/abandoned-run'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'sync-import' })\n\ntype SyncJobPayload = {\n runId: string\n batchSize: number\n scope: {\n organizationId: string\n tenantId: string\n userId?: string | null\n }\n}\n\nexport const metadata: WorkerMeta = {\n queue: DATA_SYNC_IMPORT_QUEUE,\n id: 'data-sync:import',\n concurrency: 5,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n // Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker\n // restart, and the process that restarts never constructs the producer's queue instance.\n onJobAbandoned: failAbandonedRun,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(job: QueuedJob<SyncJobPayload>, ctx: HandlerContext): Promise<void> {\n try {\n const engine = ctx.resolve<SyncEngine>('dataSyncEngine')\n await engine.runImport(job.payload.runId, job.payload.batchSize, job.payload.scope)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Data sync import worker failed'\n const errorStack = error instanceof Error ? error.stack : undefined\n\n try {\n const syncRunService = ctx.resolve<SyncRunService>('dataSyncRunService')\n const progressService = ctx.resolve<ProgressService>('progressService')\n const run = await syncRunService.getRun(job.payload.runId, job.payload.scope)\n\n if (run && run.status !== 'completed' && run.status !== 'failed' && run.status !== 'cancelled') {\n await syncRunService.markStatus(run.id, 'failed', job.payload.scope, message)\n if (run.progressJobId) {\n await progressService.failJob(\n run.progressJobId,\n {\n errorMessage: message,\n errorStack,\n },\n job.payload.scope,\n )\n }\n }\n } catch (finalizeError) {\n logger.error('Failed to finalize crashed import worker job', { err: finalizeError })\n }\n\n throw error\n }\n}\n"],
|
|
5
|
+
"mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,WAAW,EAAE,MAAM,EAAE,WAAW,cAAc,CAAC;AAYpE,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AAAA,EACb,cAAc;AAAA,EACd,iBAAiB;AAAA;AAAA;AAAA,EAGjB,gBAAgB;AAClB;AAMA,eAAO,OAA8B,KAAgC,KAAoC;AACvG,MAAI;AACF,UAAM,SAAS,IAAI,QAAoB,gBAAgB;AACvD,UAAM,OAAO,UAAU,IAAI,QAAQ,OAAO,IAAI,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,EACpF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,UAAM,aAAa,iBAAiB,QAAQ,MAAM,QAAQ;AAE1D,QAAI;AACF,YAAM,iBAAiB,IAAI,QAAwB,oBAAoB;AACvE,YAAM,kBAAkB,IAAI,QAAyB,iBAAiB;AACtE,YAAM,MAAM,MAAM,eAAe,OAAO,IAAI,QAAQ,OAAO,IAAI,QAAQ,KAAK;AAE5E,UAAI,OAAO,IAAI,WAAW,eAAe,IAAI,WAAW,YAAY,IAAI,WAAW,aAAa;AAC9F,cAAM,eAAe,WAAW,IAAI,IAAI,UAAU,IAAI,QAAQ,OAAO,OAAO;AAC5E,YAAI,IAAI,eAAe;AACrB,gBAAM,gBAAgB;AAAA,YACpB,IAAI;AAAA,YACJ;AAAA,cACE,cAAc;AAAA,cACd;AAAA,YACF;AAAA,YACA,IAAI,QAAQ;AAAA,UACd;AAAA,QACF;AAAA,MACF;AAAA,IACF,SAAS,eAAe;AACtB,aAAO,MAAM,gDAAgD,EAAE,KAAK,cAAc,CAAC;AAAA,IACrF;AAEA,UAAM;AAAA,EACR;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
258
|
+
"@open-mercato/shared": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
259
|
+
"@open-mercato/ui": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
265
|
+
"@open-mercato/shared": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
266
|
+
"@open-mercato/ui": "0.6.8-develop.7019.1.f4c01c4b5c",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^7.0.0",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -72,7 +72,7 @@ export function CalendarSettingsModal({
|
|
|
72
72
|
|
|
73
73
|
const toggleRows: Array<{ key: ToggleKey; label: string }> = [
|
|
74
74
|
{ key: 'showCrmActivities', label: t('customers.calendar.settings.showCrmActivities', 'Show CRM activities on calendar') },
|
|
75
|
-
{ key: 'aiSummaries', label: t('customers.calendar.settings.aiSummaries', 'AI summaries
|
|
75
|
+
{ key: 'aiSummaries', label: t('customers.calendar.settings.aiSummaries', 'AI summaries') },
|
|
76
76
|
{ key: 'conflictWarnings', label: t('customers.calendar.settings.conflictWarnings', 'Conflict warnings') },
|
|
77
77
|
{ key: 'showWeekends', label: t('customers.calendar.settings.showWeekends', 'Show weekends') },
|
|
78
78
|
]
|
|
@@ -22,6 +22,7 @@ export type EventPeekPopoverProps = {
|
|
|
22
22
|
joinUrl: string | null
|
|
23
23
|
aiSummaries: boolean
|
|
24
24
|
canManage?: boolean
|
|
25
|
+
side?: React.ComponentProps<typeof PopoverContent>['side']
|
|
25
26
|
onOpenChange(open: boolean): void
|
|
26
27
|
onJoin(item: CalendarItem): void
|
|
27
28
|
onEdit(item: CalendarItem): void
|
|
@@ -34,6 +35,7 @@ export function EventPeekPopover({
|
|
|
34
35
|
joinUrl,
|
|
35
36
|
aiSummaries,
|
|
36
37
|
canManage = true,
|
|
38
|
+
side = 'right',
|
|
37
39
|
onOpenChange,
|
|
38
40
|
onJoin,
|
|
39
41
|
onEdit,
|
|
@@ -63,12 +65,12 @@ export function EventPeekPopover({
|
|
|
63
65
|
(part): part is string => Boolean(part),
|
|
64
66
|
)
|
|
65
67
|
const showSummary = aiSummaries && metaParts.length > 0
|
|
66
|
-
const showJoin =
|
|
68
|
+
const showJoin = Boolean(joinUrl)
|
|
67
69
|
|
|
68
70
|
return (
|
|
69
71
|
<Popover open={open} onOpenChange={onOpenChange}>
|
|
70
72
|
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
|
71
|
-
<PopoverContent align="start" side=
|
|
73
|
+
<PopoverContent align="start" side={side} sideOffset={8} className="w-56 p-3">
|
|
72
74
|
<div className="flex flex-col gap-2">
|
|
73
75
|
<p className="text-sm font-semibold leading-5 text-foreground">{title}</p>
|
|
74
76
|
<p className="text-xs leading-4 text-muted-foreground">{`${dateLabel} · ${timeRange}`}</p>
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
"customers.calendar.settings.activityTypesHint": "Die Aktivitätstypen, die Ihr Kalender beim Erstellen eines Ereignisses anbietet. Aus dem Workspace-Wörterbuch übernommen.",
|
|
355
355
|
"customers.calendar.settings.addCategory": "Kategorie hinzufügen…",
|
|
356
356
|
"customers.calendar.settings.addType": "Typ hinzufügen…",
|
|
357
|
-
"customers.calendar.settings.aiSummaries": "KI-Zusammenfassungen
|
|
357
|
+
"customers.calendar.settings.aiSummaries": "KI-Zusammenfassungen",
|
|
358
358
|
"customers.calendar.settings.cancel": "Abbrechen",
|
|
359
359
|
"customers.calendar.settings.close": "Schließen",
|
|
360
360
|
"customers.calendar.settings.conflictScope": "Konfliktbereich",
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
"customers.calendar.settings.activityTypesHint": "The activity types your calendar surfaces when creating an event. Seeded from your workspace dictionary.",
|
|
355
355
|
"customers.calendar.settings.addCategory": "Add a category…",
|
|
356
356
|
"customers.calendar.settings.addType": "Add a type…",
|
|
357
|
-
"customers.calendar.settings.aiSummaries": "AI summaries
|
|
357
|
+
"customers.calendar.settings.aiSummaries": "AI summaries",
|
|
358
358
|
"customers.calendar.settings.cancel": "Cancel",
|
|
359
359
|
"customers.calendar.settings.close": "Close",
|
|
360
360
|
"customers.calendar.settings.conflictScope": "Conflict scope",
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
"customers.calendar.settings.activityTypesHint": "Los tipos de actividad que muestra tu calendario al crear un evento. Tomados del diccionario del espacio de trabajo.",
|
|
355
355
|
"customers.calendar.settings.addCategory": "Añadir una categoría…",
|
|
356
356
|
"customers.calendar.settings.addType": "Añadir un tipo…",
|
|
357
|
-
"customers.calendar.settings.aiSummaries": "Resúmenes de IA
|
|
357
|
+
"customers.calendar.settings.aiSummaries": "Resúmenes de IA",
|
|
358
358
|
"customers.calendar.settings.cancel": "Cancelar",
|
|
359
359
|
"customers.calendar.settings.close": "Cerrar",
|
|
360
360
|
"customers.calendar.settings.conflictScope": "Ámbito de conflictos",
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
"customers.calendar.settings.activityTypesHint": "이벤트를 생성할 때 캘린더에 표시되는 활동 유형입니다. 워크스페이스 사전에서 초기화됩니다.",
|
|
355
355
|
"customers.calendar.settings.addCategory": "카테고리 추가…",
|
|
356
356
|
"customers.calendar.settings.addType": "유형 추가…",
|
|
357
|
-
"customers.calendar.settings.aiSummaries": "AI 요약
|
|
357
|
+
"customers.calendar.settings.aiSummaries": "AI 요약",
|
|
358
358
|
"customers.calendar.settings.cancel": "취소",
|
|
359
359
|
"customers.calendar.settings.close": "닫기",
|
|
360
360
|
"customers.calendar.settings.conflictScope": "충돌 범위",
|
|
@@ -354,7 +354,7 @@
|
|
|
354
354
|
"customers.calendar.settings.activityTypesHint": "Typy aktywności udostępniane przez kalendarz podczas tworzenia wydarzenia. Pobrane ze słownika obszaru roboczego.",
|
|
355
355
|
"customers.calendar.settings.addCategory": "Dodaj kategorię…",
|
|
356
356
|
"customers.calendar.settings.addType": "Dodaj typ…",
|
|
357
|
-
"customers.calendar.settings.aiSummaries": "Podsumowania AI
|
|
357
|
+
"customers.calendar.settings.aiSummaries": "Podsumowania AI",
|
|
358
358
|
"customers.calendar.settings.cancel": "Anuluj",
|
|
359
359
|
"customers.calendar.settings.close": "Zamknij",
|
|
360
360
|
"customers.calendar.settings.conflictScope": "Zakres konfliktów",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AbandonedJobInfo } from '@open-mercato/queue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fail the run behind a job the queue gave up on.
|
|
5
|
+
*
|
|
6
|
+
* A resumable data_sync job is ONE long-lived job for a whole run — `runImport` is entered once and
|
|
7
|
+
* stays there for the duration, which for a full backfill is days. Every worker death (a deploy, an
|
|
8
|
+
* OOM) therefore stalls that job, and BullMQ's stalled counter is cumulative for the job's life:
|
|
9
|
+
* past `DATA_SYNC_MAX_STALLED_COUNT` it writes a deferred failure and the next worker fails the job
|
|
10
|
+
* BEFORE calling the processor.
|
|
11
|
+
*
|
|
12
|
+
* So `runImport` never runs, never throws, and never finalizes — and `sync_runs` is left saying
|
|
13
|
+
* `running` for a run nothing is running, forever. That is the residual state `queue-policy.ts`
|
|
14
|
+
* describes as what remains after a job exhausts its stall budget; this repairs it.
|
|
15
|
+
*
|
|
16
|
+
* It only ever moves a non-terminal run: `markStatus` refuses to overwrite `completed` / `failed` /
|
|
17
|
+
* `cancelled`, so a job abandoned after its run has ended is a no-op. `onJobAbandoned` may deliver
|
|
18
|
+
* the same job more than once, which for the same reason is also a no-op.
|
|
19
|
+
*
|
|
20
|
+
* The run's progress job is failed alongside it, the way the worker's own catch path does in
|
|
21
|
+
* `workers/sync-import.ts`. Without that the two halves disagree — the run reads `failed` while the
|
|
22
|
+
* operation still shows as in flight in the top bar — until the progress staleness sweep eventually
|
|
23
|
+
* fails it with a generic no-heartbeat message instead of the reason the queue recorded.
|
|
24
|
+
*/
|
|
25
|
+
export async function failAbandonedRun(payload: unknown, info: AbandonedJobInfo): Promise<void> {
|
|
26
|
+
const data = payload as { payload?: { runId?: unknown; scope?: { organizationId?: unknown; tenantId?: unknown } } } | undefined
|
|
27
|
+
const runId = data?.payload?.runId
|
|
28
|
+
const scope = data?.payload?.scope
|
|
29
|
+
// Nothing to repair without both: the run row is keyed by id AND tenant scope.
|
|
30
|
+
if (typeof runId !== 'string' || typeof scope?.organizationId !== 'string' || typeof scope?.tenantId !== 'string') return
|
|
31
|
+
|
|
32
|
+
const runScope = { organizationId: scope.organizationId, tenantId: scope.tenantId }
|
|
33
|
+
const errorMessage = `the queue abandoned this run's job without running it: ${info.reason}`
|
|
34
|
+
|
|
35
|
+
const { createRequestContainer } = await import('@open-mercato/shared/lib/di/container')
|
|
36
|
+
const container = await createRequestContainer()
|
|
37
|
+
const runService = container.resolve('dataSyncRunService') as {
|
|
38
|
+
markStatus(
|
|
39
|
+
runId: string,
|
|
40
|
+
status: string,
|
|
41
|
+
scope: { organizationId: string; tenantId: string },
|
|
42
|
+
error?: string,
|
|
43
|
+
): Promise<{ progressJobId?: string | null } | null>
|
|
44
|
+
}
|
|
45
|
+
const run = await runService.markStatus(runId, 'failed', runScope, errorMessage)
|
|
46
|
+
if (!run?.progressJobId) return
|
|
47
|
+
|
|
48
|
+
const progressService = container.resolve('progressService') as {
|
|
49
|
+
failJob(
|
|
50
|
+
jobId: string,
|
|
51
|
+
input: { errorMessage: string },
|
|
52
|
+
ctx: { organizationId: string; tenantId: string },
|
|
53
|
+
): Promise<unknown>
|
|
54
|
+
}
|
|
55
|
+
await progressService.failJob(run.progressJobId, { errorMessage }, runScope)
|
|
56
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
DATA_SYNC_LOCK_DURATION_MS,
|
|
8
8
|
DATA_SYNC_MAX_STALLED_COUNT,
|
|
9
9
|
} from '../lib/queue-policy'
|
|
10
|
+
import { failAbandonedRun } from '../lib/abandoned-run'
|
|
10
11
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
11
12
|
|
|
12
13
|
const logger = createLogger('data_sync').child({ component: 'sync-export' })
|
|
@@ -27,6 +28,9 @@ export const metadata: WorkerMeta = {
|
|
|
27
28
|
concurrency: 5,
|
|
28
29
|
lockDuration: DATA_SYNC_LOCK_DURATION_MS,
|
|
29
30
|
maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,
|
|
31
|
+
// Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker
|
|
32
|
+
// restart, and the process that restarts never constructs the producer's queue instance.
|
|
33
|
+
onJobAbandoned: failAbandonedRun,
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
type HandlerContext = JobContext & {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
DATA_SYNC_LOCK_DURATION_MS,
|
|
8
8
|
DATA_SYNC_MAX_STALLED_COUNT,
|
|
9
9
|
} from '../lib/queue-policy'
|
|
10
|
+
import { failAbandonedRun } from '../lib/abandoned-run'
|
|
10
11
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
11
12
|
|
|
12
13
|
const logger = createLogger('data_sync').child({ component: 'sync-import' })
|
|
@@ -27,6 +28,9 @@ export const metadata: WorkerMeta = {
|
|
|
27
28
|
concurrency: 5,
|
|
28
29
|
lockDuration: DATA_SYNC_LOCK_DURATION_MS,
|
|
29
30
|
maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,
|
|
31
|
+
// Declared on the worker, not on the enqueueing queue: the abandonment this reports is a worker
|
|
32
|
+
// restart, and the process that restarts never constructs the producer's queue instance.
|
|
33
|
+
onJobAbandoned: failAbandonedRun,
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
type HandlerContext = JobContext & {
|