├── client ├── public │ ├── sw.js │ ├── favicon-64x64.png │ ├── favicon-128x128.png │ ├── favicon-512x512.png │ ├── goals-onboarding.png │ ├── tags-onboarding.png │ ├── integration-fitbit.png │ ├── analytics-onboarding.png │ ├── dashboard-onboarding.png │ ├── integration-todoist.png │ ├── integration-weather.png │ ├── trackables-onboarding.png │ ├── goals-onboarding-mobile.png │ ├── tags-onboarding-mobile.png │ ├── analytics-onboarding-mobile.png │ ├── dashboard-onboarding-mobile.png │ ├── trackables-onboarding-mobile.png │ └── dashboard-onboarding-mobile (copy 1).png ├── android │ ├── app │ │ ├── .gitignore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── drawable │ │ │ │ │ │ ├── splash.png │ │ │ │ │ │ └── small_icon.png │ │ │ │ │ ├── drawable-night │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── mipmap-ldpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── drawable-land-hdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-ldpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-mdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-hdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-ldpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-mdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── drawable-land-xhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-xxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-xxxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-xhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-xxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-xxxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ ├── ic_launcher_round.png │ │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ │ └── ic_launcher_foreground.png │ │ │ │ │ ├── drawable-land-night-hdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-night-ldpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-night-mdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-night-xhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-night-xxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-night-hdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-night-ldpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-night-mdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-night-xhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-night-xxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-land-night-xxxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── drawable-port-night-xxxhdpi │ │ │ │ │ │ └── splash.png │ │ │ │ │ ├── values │ │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── xml │ │ │ │ │ │ └── file_paths.xml │ │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ │ └── layout │ │ │ │ │ │ └── activity_main.xml │ │ │ │ └── java │ │ │ │ │ └── dev │ │ │ │ │ └── adoe │ │ │ │ │ └── perfice │ │ │ │ │ └── MainActivity.java │ │ │ ├── test │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── getcapacitor │ │ │ │ │ └── myapp │ │ │ │ │ └── ExampleUnitTest.java │ │ │ └── androidTest │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── getcapacitor │ │ │ │ └── myapp │ │ │ │ └── ExampleInstrumentedTest.java │ │ ├── capacitor.build.gradle │ │ └── proguard-rules.pro │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── variables.gradle │ ├── build.gradle │ ├── gradle.properties │ └── capacitor.settings.gradle ├── .vscode │ └── extensions.json ├── assets │ ├── splash.png │ ├── icon-only.png │ ├── screenshot.png │ ├── splash-dark.png │ ├── icon-background.png │ └── icon-foreground.png ├── src │ ├── util │ │ ├── window.ts │ │ ├── local.ts │ │ ├── math.ts │ │ ├── event.ts │ │ ├── promise.ts │ │ └── perf.ts │ ├── vite-env.d.ts │ ├── model │ │ ├── ui │ │ │ ├── button.ts │ │ │ ├── router.svelte.ts │ │ │ ├── segmented.ts │ │ │ ├── dropdown.ts │ │ │ ├── dynamicInput.ts │ │ │ └── sidebar.ts │ │ ├── auth │ │ │ └── auth.ts │ │ ├── goal │ │ │ └── goal.ts │ │ ├── analytics │ │ │ ├── analytics.ts │ │ │ └── ui.ts │ │ ├── integration │ │ │ └── ui.ts │ │ ├── tag │ │ │ ├── tag.ts │ │ │ └── suggestions.ts │ │ ├── journal │ │ │ ├── search │ │ │ │ ├── ui.ts │ │ │ │ └── date.ts │ │ │ └── journal.ts │ │ └── sharedWidgets │ │ │ └── table │ │ │ └── table.ts │ ├── assets │ │ ├── OpenMoji-black-glyf.woff2 │ │ └── goal_suggestions.json │ ├── components │ │ ├── base │ │ │ ├── modal │ │ │ │ ├── ModalFooterContainer.svelte │ │ │ │ └── generic │ │ │ │ │ ├── GenericInfoModal.svelte │ │ │ │ │ ├── GenericDeleteModal.svelte │ │ │ │ │ └── GenericEntityModal.svelte │ │ │ ├── dynamic │ │ │ │ └── DynamicLabel.svelte │ │ │ ├── icon │ │ │ │ └── Icon.svelte │ │ │ ├── color │ │ │ │ └── ColorPickerButton.svelte │ │ │ ├── iconLabel │ │ │ │ ├── IconLabel.svelte │ │ │ │ └── IconLabelBetween.svelte │ │ │ ├── button │ │ │ │ ├── LineButton.svelte │ │ │ │ ├── HorizontalPlusButton.svelte │ │ │ │ ├── IconButton.svelte │ │ │ │ ├── Button.svelte │ │ │ │ └── PopupIconButton.svelte │ │ │ ├── title │ │ │ │ ├── Title.svelte │ │ │ │ └── TitleAndCalendar.svelte │ │ │ ├── timeScope │ │ │ │ ├── SimpleTimeScopePicker.svelte │ │ │ │ └── RangedTimeScopePicker.svelte │ │ │ ├── gesture │ │ │ │ └── SwipeDetector.svelte │ │ │ ├── card │ │ │ │ ├── GenericActionsCard.svelte │ │ │ │ ├── GenericEditDeleteCard.svelte │ │ │ │ └── TitledCard.svelte │ │ │ ├── inline │ │ │ │ ├── InlineCreateLineButton.svelte │ │ │ │ └── InlineCreateInput.svelte │ │ │ ├── dropdown │ │ │ │ └── BindableDropdownButton.svelte │ │ │ ├── calendarScroll │ │ │ │ └── CalendarScrollItem.svelte │ │ │ ├── invertedSegmented │ │ │ │ └── InvertedSegment.svelte │ │ │ ├── contextMenu │ │ │ │ └── ContextMenuButtons.svelte │ │ │ └── weekDays │ │ │ │ └── WeekDays.svelte │ │ ├── journal │ │ │ ├── day │ │ │ │ ├── JournalEntryTimestamp.svelte │ │ │ │ ├── JournalSummaryContainer.svelte │ │ │ │ ├── JournalCardBase.svelte │ │ │ │ ├── JournalDayDate.svelte │ │ │ │ └── JournalTagEntries.svelte │ │ │ └── search │ │ │ │ ├── types │ │ │ │ ├── freeText │ │ │ │ │ └── FreeTextSearchOptions.svelte │ │ │ │ ├── tag │ │ │ │ │ ├── filters │ │ │ │ │ │ ├── TagOneOfFilterCard.svelte │ │ │ │ │ │ └── TagByCategoryFilterCard.svelte │ │ │ │ │ └── TagSearchActions.svelte │ │ │ │ ├── trackable │ │ │ │ │ └── filters │ │ │ │ │ │ ├── TrackableOneOfFilterCard.svelte │ │ │ │ │ │ └── TrackableByCategoryFilterCard.svelte │ │ │ │ ├── GenericFilterContainer.svelte │ │ │ │ └── date │ │ │ │ │ └── DateSearchOptions.svelte │ │ │ │ └── common │ │ │ │ ├── OneOfFilterCard.svelte │ │ │ │ └── ByCategoryFilterCard.svelte │ │ ├── sidebar │ │ │ └── drawer │ │ │ │ ├── DrawerOpenButton.svelte │ │ │ │ └── DrawerButton.svelte │ │ ├── form │ │ │ ├── fields │ │ │ │ ├── input │ │ │ │ │ ├── DateInputFormField.svelte │ │ │ │ │ ├── TimeOfDayInputFormField.svelte │ │ │ │ │ ├── DateTimeInputFormField.svelte │ │ │ │ │ ├── TimeElapsedInputFormField.svelte │ │ │ │ │ ├── BooleanInputFormField.svelte │ │ │ │ │ └── VanillaInputFormField.svelte │ │ │ │ ├── richInput │ │ │ │ │ └── RichInputFormField.svelte │ │ │ │ ├── textArea │ │ │ │ │ └── TextAreaFormField.svelte │ │ │ │ ├── segmented │ │ │ │ │ └── SegmentedFormField.svelte │ │ │ │ ├── hierarchy │ │ │ │ │ └── HierarchyButton.svelte │ │ │ │ ├── range │ │ │ │ │ └── RangeFormField.svelte │ │ │ │ └── select │ │ │ │ │ └── SelectOptionButton.svelte │ │ │ └── editor │ │ │ │ ├── display │ │ │ │ ├── segmented │ │ │ │ │ └── EditSegmentedOptionCard.svelte │ │ │ │ ├── select │ │ │ │ │ ├── EditSelectOptionCard.svelte │ │ │ │ │ └── EditSelectGrid.svelte │ │ │ │ ├── range │ │ │ │ │ └── EditRangeQuestionSettings.svelte │ │ │ │ └── hierarchy │ │ │ │ │ └── EditHierarchyQuestionDisplaySettings.svelte │ │ │ │ ├── sidebar │ │ │ │ └── SidebarDropdownHeader.svelte │ │ │ │ └── data │ │ │ │ └── hierarchy │ │ │ │ └── EditHierarchyQuestionSettings.svelte │ │ ├── trackable │ │ │ ├── edit │ │ │ │ ├── EditTrackableForm.svelte │ │ │ │ ├── EditTrackableIntegrations.svelte │ │ │ │ └── general │ │ │ │ │ ├── EditTrackableAnalytics.svelte │ │ │ │ │ ├── value │ │ │ │ │ └── EditTrackableValueRepresentation.svelte │ │ │ │ │ ├── habit │ │ │ │ │ └── EditTrackableHabitCard.svelte │ │ │ │ │ └── EditTrackableCategory.svelte │ │ │ └── card │ │ │ │ ├── value │ │ │ │ └── table │ │ │ │ │ ├── TableTrackableRenderer.svelte │ │ │ │ │ └── TrackableTableEntry.svelte │ │ │ │ └── habit │ │ │ │ └── HabitTrackableRenderer.svelte │ │ ├── dashboard │ │ │ ├── types │ │ │ │ ├── table │ │ │ │ │ ├── TableWidgetEntry.svelte │ │ │ │ │ ├── TableWidgetGroupHeader.svelte │ │ │ │ │ └── DashboardTableWidget.svelte │ │ │ │ ├── entryRow │ │ │ │ │ └── EntryRowItem.svelte │ │ │ │ ├── checkList │ │ │ │ │ └── ChecklistEntry.svelte │ │ │ │ ├── welcome │ │ │ │ │ └── DashboardWelcomeWidget.svelte │ │ │ │ └── goal │ │ │ │ │ └── DashboardGoalWidget.svelte │ │ │ └── sidebar │ │ │ │ ├── edit │ │ │ │ └── types │ │ │ │ │ ├── welcome │ │ │ │ │ └── EditWelcomeWidgetSidebar.svelte │ │ │ │ │ ├── newCorrelations │ │ │ │ │ └── EditNewCorrelationsWidgetSidebar.svelte │ │ │ │ │ ├── entryRow │ │ │ │ │ └── EditEntryRowWidgetSidebar.svelte │ │ │ │ │ ├── checklist │ │ │ │ │ └── EditChecklistWidgetSidebar.svelte │ │ │ │ │ ├── table │ │ │ │ │ └── EditTableWidgetSidebar.svelte │ │ │ │ │ └── insights │ │ │ │ │ └── EditInsightsWidgetSidebar.svelte │ │ │ │ └── add │ │ │ │ └── AddWidgetSidebar.svelte │ │ ├── goal │ │ │ ├── GoalMetIndicator.svelte │ │ │ ├── multi │ │ │ │ ├── GoalMetConditionEntry.svelte │ │ │ │ ├── MultiConditionRenderer.svelte │ │ │ │ └── ConditionEntry.svelte │ │ │ ├── single │ │ │ │ ├── GoalMetSingleCondition.svelte │ │ │ │ ├── ComparisonSingleCondition.svelte │ │ │ │ └── SingleConditionRenderer.svelte │ │ │ ├── GoalNewCard.svelte │ │ │ └── editor │ │ │ │ ├── condition │ │ │ │ └── comparison │ │ │ │ │ └── AddSourceButton.svelte │ │ │ │ └── sidebar │ │ │ │ └── AddSourceSidebar.svelte │ │ ├── settings │ │ │ ├── SettingsIntegrations.svelte │ │ │ ├── SettingsDataExport.svelte │ │ │ ├── auth │ │ │ │ └── login │ │ │ │ │ └── ResendConfirmation.svelte │ │ │ ├── SettingsDeleteData.svelte │ │ │ ├── ConfigureUrlModal.svelte │ │ │ └── DeleteAccountModal.svelte │ │ ├── variable │ │ │ └── edit │ │ │ │ ├── EditBackButton.svelte │ │ │ │ └── EditVariableName.svelte │ │ ├── reflection │ │ │ ├── GlobalReflectionModal.svelte │ │ │ ├── modal │ │ │ │ └── ReflectionPageButton.svelte │ │ │ └── editor │ │ │ │ └── sidebar │ │ │ │ └── widget │ │ │ │ ├── ReflectionEditTableWidget.svelte │ │ │ │ ├── ReflectionEditChecklistWidget.svelte │ │ │ │ └── ReflectionEditFormWidget.svelte │ │ ├── tag │ │ │ ├── TagButtonBase.svelte │ │ │ ├── TagValueCard.svelte │ │ │ └── TagCard.svelte │ │ ├── sharedWidgets │ │ │ └── checklist │ │ │ │ ├── EditChecklistConditionCard.svelte │ │ │ │ └── EditTagChecklistCondition.svelte │ │ ├── integration │ │ │ ├── EditIntegrationWebhook.svelte │ │ │ ├── modals │ │ │ │ ├── GlobalIntegrationModals.svelte │ │ │ │ └── IntegrationUnauthenticatedModal.svelte │ │ │ └── IntegrationEntityCard.svelte │ │ ├── analytics │ │ │ ├── details │ │ │ │ ├── CorrelationMessage.svelte │ │ │ │ ├── trackable │ │ │ │ │ └── BasicCategoricalAnalyticsRow.svelte │ │ │ │ ├── CorrelationBar.svelte │ │ │ │ └── tag │ │ │ │ │ └── TagWeekDayAnalytics.svelte │ │ │ ├── QuestionLabel.svelte │ │ │ ├── trackable │ │ │ │ └── AnalyticsTrackableLineChart.svelte │ │ │ └── tag │ │ │ │ └── AnalyticsTagView.svelte │ │ ├── mobile │ │ │ └── MobileTopBar.svelte │ │ └── onboarding │ │ │ ├── OnboardingImage.svelte │ │ │ └── OnboardingRegister.svelte │ ├── main.ts │ ├── stores │ │ ├── ui │ │ │ └── drawer.ts │ │ ├── deletion │ │ │ └── deletion.ts │ │ ├── export │ │ │ ├── complete.ts │ │ │ └── formEntry.ts │ │ ├── feedback │ │ │ └── feedback.ts │ │ ├── import │ │ │ └── complete.ts │ │ ├── dashboard │ │ │ └── widget │ │ │ │ ├── welcome.ts │ │ │ │ ├── trackable.ts │ │ │ │ └── goal.ts │ │ ├── remote │ │ │ └── remote.ts │ │ └── journal │ │ │ └── tag.ts │ ├── services │ │ ├── deletion │ │ │ └── deletion.ts │ │ ├── notification │ │ │ └── web.ts │ │ └── observer.ts │ ├── swSetup.ts │ ├── db │ │ ├── migration │ │ │ └── migrations │ │ │ │ ├── chartTitles.ts │ │ │ │ └── defaultQuestionValues.ts │ │ └── dexie │ │ │ ├── encryption.ts │ │ │ ├── search.ts │ │ │ ├── migration.ts │ │ │ ├── variable.ts │ │ │ └── reflection.ts │ └── views │ │ └── feedback │ │ └── FeedbackView.svelte ├── tsconfig.json ├── svelte.config.js ├── docker-compose.yml ├── nginx.conf ├── Dockerfile ├── tailwind.config.js ├── capacitor.config.ts ├── tests │ ├── common.ts │ ├── time-scope.test.ts │ ├── primitive.test.ts │ └── simple-time.test.ts ├── tsconfig.app.json ├── tsconfig.node.json └── index.html ├── server ├── .gitignore ├── build-auth.sh ├── build-sync.sh ├── util │ ├── go.mod │ ├── number.go │ ├── slice.go │ ├── rand.go │ └── map.go ├── build-gateway.sh ├── build-integration.sh ├── build.sh ├── integration │ ├── internal │ │ ├── constants │ │ │ └── constants.go │ │ ├── controller │ │ │ ├── util.go │ │ │ └── integration_webhook.go │ │ ├── util │ │ │ └── http.go │ │ ├── collection │ │ │ ├── integration_entity.go │ │ │ └── integration_type.go │ │ └── service │ │ │ └── auth_test.go │ ├── cmd │ │ └── integration │ │ │ └── integration.go │ └── Dockerfile ├── sync │ ├── internal │ │ ├── constants.go │ │ ├── validate.go │ │ ├── salt_controller.go │ │ ├── key_service.go │ │ └── salt_service.go │ ├── cmd │ │ └── sync │ │ │ └── sync.go │ └── Dockerfile ├── auth │ ├── cmd │ │ └── auth │ │ │ └── auth.go │ ├── internal │ │ ├── model.go │ │ └── kafka.go │ └── Dockerfile ├── gateway │ └── Dockerfile ├── README.md ├── proto │ ├── go.mod │ └── go.sum └── mongoutil │ ├── go.mod │ └── bson.go ├── docs ├── selfhost │ ├── sync │ │ ├── index.md │ │ └── _category_.json │ ├── integrations │ │ ├── _category_.json │ │ └── index.md │ └── _category_.json └── index.md ├── .gitignore └── LICENSE /client/public/sw.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build/* 2 | !/build/.npmkeep 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | deploy-*.sh 3 | *-deployment.yaml -------------------------------------------------------------------------------- /docs/selfhost/sync/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: sync 3 | --- 4 | 5 | # Sync -------------------------------------------------------------------------------- /server/build-auth.sh: -------------------------------------------------------------------------------- 1 | docker build -f auth/Dockerfile -t auth:latest . -------------------------------------------------------------------------------- /server/build-sync.sh: -------------------------------------------------------------------------------- 1 | docker build -f sync/Dockerfile -t sync:latest . 2 | -------------------------------------------------------------------------------- /server/util/go.mod: -------------------------------------------------------------------------------- 1 | module perfice.adoe.dev/util 2 | 3 | go 1.24.3 4 | -------------------------------------------------------------------------------- /server/build-gateway.sh: -------------------------------------------------------------------------------- 1 | docker build -f gateway/Dockerfile -t gateway:latest . -------------------------------------------------------------------------------- /docs/selfhost/sync/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Sync", 3 | "position": 2 4 | } -------------------------------------------------------------------------------- /client/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /client/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/assets/splash.png -------------------------------------------------------------------------------- /server/build-integration.sh: -------------------------------------------------------------------------------- 1 | docker build -f integration/Dockerfile -t integration:latest . 2 | -------------------------------------------------------------------------------- /client/src/util/window.ts: -------------------------------------------------------------------------------- 1 | export function isMobile() { 2 | return window.innerWidth < 768; 3 | } -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/selfhost/integrations/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Integrations", 3 | "position": 2 4 | } -------------------------------------------------------------------------------- /client/assets/icon-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/assets/icon-only.png -------------------------------------------------------------------------------- /client/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/assets/screenshot.png -------------------------------------------------------------------------------- /client/assets/splash-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/assets/splash-dark.png -------------------------------------------------------------------------------- /client/src/model/ui/button.ts: -------------------------------------------------------------------------------- 1 | export enum ButtonColor { 2 | GREEN, 3 | RED, 4 | WHITE 5 | } 6 | -------------------------------------------------------------------------------- /server/build.sh: -------------------------------------------------------------------------------- 1 | sh build-integration.sh 2 | sh build-sync.sh 3 | sh build-gateway.sh 4 | sh build-auth.sh -------------------------------------------------------------------------------- /client/public/favicon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/favicon-64x64.png -------------------------------------------------------------------------------- /client/assets/icon-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/assets/icon-background.png -------------------------------------------------------------------------------- /client/assets/icon-foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/assets/icon-foreground.png -------------------------------------------------------------------------------- /client/public/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/favicon-128x128.png -------------------------------------------------------------------------------- /client/public/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/favicon-512x512.png -------------------------------------------------------------------------------- /client/public/goals-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/goals-onboarding.png -------------------------------------------------------------------------------- /client/public/tags-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/tags-onboarding.png -------------------------------------------------------------------------------- /client/src/model/auth/auth.ts: -------------------------------------------------------------------------------- 1 | export interface AuthenticatedUser { 2 | id: string; 3 | timezone: string; 4 | } -------------------------------------------------------------------------------- /server/integration/internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | var UserIdLocal string = "userId" 4 | -------------------------------------------------------------------------------- /client/public/integration-fitbit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/integration-fitbit.png -------------------------------------------------------------------------------- /client/public/analytics-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/analytics-onboarding.png -------------------------------------------------------------------------------- /client/public/dashboard-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/dashboard-onboarding.png -------------------------------------------------------------------------------- /client/public/integration-todoist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/integration-todoist.png -------------------------------------------------------------------------------- /client/public/integration-weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/integration-weather.png -------------------------------------------------------------------------------- /client/public/trackables-onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/trackables-onboarding.png -------------------------------------------------------------------------------- /client/public/goals-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/goals-onboarding-mobile.png -------------------------------------------------------------------------------- /client/public/tags-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/tags-onboarding-mobile.png -------------------------------------------------------------------------------- /client/src/assets/OpenMoji-black-glyf.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/src/assets/OpenMoji-black-glyf.woff2 -------------------------------------------------------------------------------- /client/public/analytics-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/analytics-onboarding-mobile.png -------------------------------------------------------------------------------- /client/public/dashboard-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/dashboard-onboarding-mobile.png -------------------------------------------------------------------------------- /client/public/trackables-onboarding-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/trackables-onboarding-mobile.png -------------------------------------------------------------------------------- /server/sync/internal/constants.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | var userIdLocal string = "userId" 4 | var sessionIdLocal string = "sessionId" 5 | -------------------------------------------------------------------------------- /client/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/selfhost/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Self hosted", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable/splash.png -------------------------------------------------------------------------------- /client/public/dashboard-onboarding-mobile (copy 1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/public/dashboard-onboarding-mobile (copy 1).png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable/small_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable/small_icon.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-night/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-night/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-ldpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-ldpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-hdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-ldpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-mdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-hdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-ldpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-mdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /server/auth/cmd/auth/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "perfice.adoe.dev/auth/internal" 4 | 5 | func main() { 6 | app := internal.NewAuthApp() 7 | app.Init() 8 | } 9 | -------------------------------------------------------------------------------- /server/sync/cmd/sync/sync.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "perfice.adoe.dev/sync/internal" 4 | 5 | func main() { 6 | app := internal.NewSyncApp() 7 | app.Init() 8 | } 9 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-xhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-xxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-xxxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-xhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-xxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-xxxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_round.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-night-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-night-hdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-night-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-night-ldpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-night-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-night-mdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-night-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-night-xhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-night-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-night-xxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-night-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-night-hdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-night-ldpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-night-ldpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-night-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-night-mdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-night-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-night-xhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-night-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-night-xxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-land-night-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-land-night-xxxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/drawable-port-night-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/drawable-port-night-xxxhdpi/splash.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_background.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-ldpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /client/src/model/goal/goal.ts: -------------------------------------------------------------------------------- 1 | export interface Goal { 2 | id: string; 3 | name: string; 4 | color: string; 5 | variableId: string; 6 | streakVariableId: string; 7 | } 8 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0lloc/perfice/HEAD/client/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /client/android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /server/integration/cmd/integration/integration.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "perfice.adoe.dev/integration/internal" 4 | 5 | func main() { 6 | app := internal.NewIntegrationApp() 7 | app.Init() 8 | } 9 | -------------------------------------------------------------------------------- /client/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':capacitor-cordova-android-plugins' 3 | project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') 4 | 5 | apply from: 'capacitor.settings.gradle' -------------------------------------------------------------------------------- /client/src/components/base/modal/ModalFooterContainer.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Welcome 3 | sidebar_position: 1 4 | slug: / 5 | --- 6 | 7 | # Welcome 8 | Welcome to the documentation for the open-source self-tracking platform Perfice! 9 | This documentation is a work in progress and is currently very incomplete. -------------------------------------------------------------------------------- /client/android/app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.css' 2 | import {mount} from "svelte"; 3 | import App from "@perfice/App.svelte"; 4 | import 'es-iterator-helpers/auto'; 5 | 6 | const app = mount(App, { 7 | target: document.getElementById('app')!, 8 | }); 9 | 10 | export default app -------------------------------------------------------------------------------- /client/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /client/src/model/ui/router.svelte.ts: -------------------------------------------------------------------------------- 1 | import {goto} from "@mateothegreat/svelte5-router"; 2 | 3 | export const routingNavigatorState = $state([]); 4 | 5 | export function getCurrentRoute(state: string[]) { 6 | return state.length > 0 ? state[state.length - 1] : "/"; 7 | } -------------------------------------------------------------------------------- /server/auth/internal/model.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type User struct { 4 | Id string `bson:"_id"` 5 | Email string `bson:"email"` 6 | Password string `bson:"password"` 7 | Confirmed bool `bson:"confirmed"` 8 | Timezone string `bson:"timezone"` 9 | } 10 | -------------------------------------------------------------------------------- /client/src/stores/ui/drawer.ts: -------------------------------------------------------------------------------- 1 | import {writable} from "svelte/store"; 2 | 3 | export const drawerOpen = writable(false); 4 | 5 | export function closeDrawer() { 6 | drawerOpen.set(false); 7 | } 8 | 9 | export function toggleDrawer() { 10 | drawerOpen.update(v => !v); 11 | } 12 | -------------------------------------------------------------------------------- /client/src/components/base/dynamic/DynamicLabel.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | {@render children()} 9 | 10 | -------------------------------------------------------------------------------- /client/src/model/analytics/analytics.ts: -------------------------------------------------------------------------------- 1 | export interface AnalyticsSettings { 2 | id: string; 3 | questionId: string; 4 | useMeanValue: Record; // Question id -> boolean 5 | // Whether to create values between all entries with the last value 6 | interpolate: boolean; 7 | } -------------------------------------------------------------------------------- /client/src/model/ui/segmented.ts: -------------------------------------------------------------------------------- 1 | import type {IconDefinition} from "@fortawesome/free-solid-svg-icons"; 2 | 3 | export interface SegmentedItem { 4 | name: string; 5 | value?: T; 6 | prefix?: IconDefinition; 7 | suffix?: IconDefinition; 8 | onClick?: () => void; 9 | } 10 | -------------------------------------------------------------------------------- /server/integration/internal/controller/util.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "perfice.adoe.dev/integration/internal/constants" 6 | ) 7 | 8 | func getUserId(ctx *fiber.Ctx) string { 9 | return ctx.Locals(constants.UserIdLocal).(string) 10 | } 11 | -------------------------------------------------------------------------------- /client/src/model/integration/ui.ts: -------------------------------------------------------------------------------- 1 | export interface SelectedField { 2 | integrationField: string | null; 3 | questionId: string; 4 | } 5 | 6 | export interface UnauthenticatedIntegrationError { 7 | integrationTypeName: string; 8 | integrationType: string; 9 | forms: string[]; 10 | } -------------------------------------------------------------------------------- /client/src/util/local.ts: -------------------------------------------------------------------------------- 1 | export function parseJsonFromLocalStorage(key: string): T | null { 2 | try { 3 | let raw = localStorage.getItem(key); 4 | if (raw == null) return null; 5 | 6 | return JSON.parse(raw); 7 | } catch (e) { 8 | return null; 9 | } 10 | } -------------------------------------------------------------------------------- /client/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | client: 3 | image: ghcr.io/p0lloc/perfice:latest 4 | build: 5 | context: https://github.com/p0lloc/perfice.git#main:client 6 | dockerfile: Dockerfile 7 | container_name: client 8 | ports: 9 | - "80:80" 10 | restart: unless-stopped 11 | -------------------------------------------------------------------------------- /client/src/components/base/icon/Icon.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | {name !== "" ? name : "\u2b50\ufe0f"} 7 | 8 | -------------------------------------------------------------------------------- /client/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /client/src/util/math.ts: -------------------------------------------------------------------------------- 1 | export function calculateProgressSafe(first: number, total: number): number { 2 | if (total == 0) return 0; 3 | 4 | return first / total; 5 | } 6 | 7 | export function numberToMaxDecimals(value: number, decimals: number): string { 8 | return Number.isInteger(value) ? value.toString() : value.toFixed(decimals) 9 | } -------------------------------------------------------------------------------- /client/src/components/base/color/ColorPickerButton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | onChange?.(e.currentTarget.value)}> 7 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Perfice 4 | Perfice 5 | io.perfice.app 6 | io.perfice.app 7 | 8 | -------------------------------------------------------------------------------- /client/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name frontend; 4 | 5 | location /new/ { 6 | root /usr/share/nginx/html; 7 | try_files $uri @new; 8 | } 9 | 10 | location /new { 11 | return 301 /new/; 12 | } 13 | 14 | location @new { 15 | root /usr/share/nginx/html; 16 | rewrite ^(.+)$ /new/index.html; 17 | } 18 | } -------------------------------------------------------------------------------- /client/src/components/journal/day/JournalEntryTimestamp.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |

8 | {formatTimestampHHMM(timestamp)} 9 |

10 | -------------------------------------------------------------------------------- /client/src/model/tag/tag.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | id: string; 3 | name: string; 4 | variableId: string; 5 | order: number; 6 | categoryId: string | null; 7 | } 8 | 9 | export const UNCATEGORIZED_TAG_CATEGORY_ID = ""; 10 | 11 | export interface TagCategory { 12 | id: string; 13 | name: string; 14 | order: number; 15 | } 16 | -------------------------------------------------------------------------------- /server/gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | WORKDIR /app 3 | 4 | COPY gateway/ ./gateway 5 | COPY proto/ ./proto 6 | COPY util/ ./util 7 | 8 | WORKDIR /app/gateway 9 | RUN go build -o gateway . 10 | 11 | FROM alpine:latest 12 | WORKDIR /root/ 13 | COPY --from=builder /app/gateway/gateway . 14 | 15 | EXPOSE 3000 16 | CMD ["./gateway"] 17 | 18 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # server 2 | Perfice can be run completely locally without a server, but using a server allows some extra features. 3 | The server component is responsible for synchronizing data between devices and pulling data automatically from integrations like Fitbit, Todoist into the platform. 4 | 5 | More information can be read in the [docs](https://perfice.adoe.dev/docs/). -------------------------------------------------------------------------------- /client/src/components/sidebar/drawer/DrawerOpenButton.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | setTimeout(toggleDrawer)}/> 8 | -------------------------------------------------------------------------------- /client/src/model/tag/suggestions.ts: -------------------------------------------------------------------------------- 1 | import tagSuggestionsAsset from '@perfice/assets/tag_suggestions.json?raw' 2 | 3 | export const TAG_SUGGESTIONS: TagSuggestionGroup[] = JSON.parse(tagSuggestionsAsset); 4 | 5 | export interface TagSuggestionGroup { 6 | name: string; 7 | suggestions: TagSuggestion[]; 8 | } 9 | 10 | export type TagSuggestion = { 11 | name: string; 12 | } -------------------------------------------------------------------------------- /client/src/components/form/fields/input/DateInputFormField.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/src/components/form/fields/input/TimeOfDayInputFormField.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/src/components/form/fields/input/DateTimeInputFormField.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/src/components/form/fields/input/TimeElapsedInputFormField.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/src/components/trackable/edit/EditTrackableForm.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /server/sync/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | WORKDIR /app 3 | 4 | COPY proto/ ./proto 5 | COPY sync/ ./sync 6 | COPY util/ ./util 7 | COPY mongoutil/ ./mongoutil 8 | 9 | WORKDIR /app/sync 10 | RUN go build -o sync cmd/sync/sync.go 11 | 12 | FROM alpine:latest 13 | WORKDIR /root/ 14 | RUN apk add --no-cache tzdata 15 | COPY --from=builder /app/sync/sync . 16 | 17 | EXPOSE 8082 18 | CMD ["./sync"] 19 | -------------------------------------------------------------------------------- /server/proto/go.mod: -------------------------------------------------------------------------------- 1 | module perfice.adoe.dev/proto 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | google.golang.org/grpc v1.72.1 7 | google.golang.org/protobuf v1.36.6 8 | ) 9 | 10 | require ( 11 | golang.org/x/net v0.35.0 // indirect 12 | golang.org/x/sys v0.30.0 // indirect 13 | golang.org/x/text v0.22.0 // indirect 14 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /client/src/components/dashboard/types/table/TableWidgetEntry.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

9 | {entry.prefix} 10 |

11 |

{entry.suffix}

12 |
-------------------------------------------------------------------------------- /client/src/model/ui/dropdown.ts: -------------------------------------------------------------------------------- 1 | import type {IconDefinition} from "@fortawesome/free-solid-svg-icons"; 2 | 3 | export interface DropdownMenuItemDetails { 4 | name: string; 5 | value: T; 6 | icon?: IconDefinition; 7 | separated?: boolean; 8 | } 9 | 10 | export type DropdownMenuItem = DropdownMenuItemDetails & { 11 | action?: () => void; 12 | } 13 | 14 | export const DROPDOWN_BUTTON_HEIGHT = 40; 15 | -------------------------------------------------------------------------------- /client/src/components/trackable/edit/EditTrackableIntegrations.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-alpine AS build 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | ARG VITE_BACKEND_URL="http://localhost:3000" 5 | RUN npm install 6 | COPY . . 7 | RUN npm run build 8 | FROM nginx:alpine 9 | RUN rm -rf /usr/share/nginx/html/* 10 | WORKDIR / 11 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 12 | COPY --from=build /app/dist /usr/share/nginx/html/new 13 | EXPOSE 80 14 | CMD ["nginx", "-g", "daemon off;"] 15 | -------------------------------------------------------------------------------- /client/src/components/base/iconLabel/IconLabel.svelte: -------------------------------------------------------------------------------- 1 | 8 |
9 | 10 |

{title}

11 |
12 | -------------------------------------------------------------------------------- /client/src/components/form/fields/richInput/RichInputFormField.svelte: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /server/auth/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | WORKDIR /app 3 | 4 | COPY auth/ ./auth 5 | COPY proto/ ./proto 6 | COPY util/ ./util 7 | COPY mongoutil/ ./mongoutil 8 | 9 | WORKDIR /app/auth 10 | RUN go build -o auth cmd/auth/auth.go 11 | 12 | FROM alpine:latest 13 | WORKDIR /root/ 14 | RUN apk add --no-cache tzdata 15 | COPY --from=builder /app/auth/auth . 16 | 17 | EXPOSE 5001 18 | EXPOSE 8081 19 | CMD ["./auth"] 20 | 21 | -------------------------------------------------------------------------------- /client/src/components/goal/GoalMetIndicator.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#if value} 11 | 12 | {:else} 13 | 14 | {/if} 15 | -------------------------------------------------------------------------------- /client/src/stores/deletion/deletion.ts: -------------------------------------------------------------------------------- 1 | import type {DeletionService} from "@perfice/services/deletion/deletion"; 2 | 3 | export class DeletionStore { 4 | 5 | private readonly deletionService: DeletionService; 6 | 7 | constructor(deletionService: DeletionService) { 8 | this.deletionService = deletionService; 9 | } 10 | 11 | async deleteAllData() { 12 | await this.deletionService.deleteAllData(); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /server/integration/internal/util/http.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/gofiber/fiber/v2" 6 | ) 7 | 8 | func ParseAndValidate(ctx *fiber.Ctx, validator *validator.Validate, request any) error { 9 | if err := ctx.BodyParser(request); err != nil { 10 | return err 11 | } 12 | 13 | if err := validator.Struct(request); err != nil { 14 | return err 15 | } 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /server/sync/internal/validate.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/go-playground/validator/v10" 5 | "github.com/gofiber/fiber/v2" 6 | ) 7 | 8 | func ParseAndValidate(ctx *fiber.Ctx, validator *validator.Validate, request any) error { 9 | if err := ctx.BodyParser(request); err != nil { 10 | return err 11 | } 12 | 13 | if err := validator.Struct(request); err != nil { 14 | return err 15 | } 16 | 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /client/src/components/goal/multi/GoalMetConditionEntry.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | {value.name} 11 |
12 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/src/stores/export/complete.ts: -------------------------------------------------------------------------------- 1 | import type {CompleteExportService} from "@perfice/services/export/complete/complete"; 2 | 3 | export class CompleteExportStore { 4 | 5 | private readonly exportService: CompleteExportService; 6 | 7 | constructor(exportService: CompleteExportService) { 8 | this.exportService = exportService; 9 | } 10 | 11 | async export() { 12 | return await this.exportService.export(); 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /client/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/src/components/settings/SettingsIntegrations.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | {#if $auth} 8 |
9 | 10 |
11 | {/if} 12 | -------------------------------------------------------------------------------- /client/src/util/event.ts: -------------------------------------------------------------------------------- 1 | import {type Writable} from "svelte/store"; 2 | 3 | export function subscribeToEventStore(list: T[], writable: Writable, handler: (e: T) => void) { 4 | if(list.length == 0) return; 5 | 6 | for(let event of list) { 7 | handler(event); 8 | } 9 | 10 | writable.set([]); 11 | } 12 | 13 | export function publishToEventStore(writable: Writable, event: T) { 14 | writable.update(v => [...v, event]); 15 | } 16 | -------------------------------------------------------------------------------- /client/src/model/journal/search/ui.ts: -------------------------------------------------------------------------------- 1 | import type { Form } from "@perfice/model/form/form"; 2 | import type { Tag, TagCategory } from "@perfice/model/tag/tag"; 3 | import type { Trackable, TrackableCategory } from "@perfice/model/trackable/trackable"; 4 | 5 | export interface JournalSearchUiDependencies { 6 | forms: Form[]; 7 | trackables: Trackable[]; 8 | tags: Tag[]; 9 | trackableCategories: TrackableCategory[]; 10 | tagCategories: TagCategory[]; 11 | } 12 | -------------------------------------------------------------------------------- /client/src/services/deletion/deletion.ts: -------------------------------------------------------------------------------- 1 | import type {Table} from "dexie"; 2 | 3 | export class DeletionService { 4 | 5 | private readonly tables: Record; 6 | 7 | constructor(tables: Record) { 8 | this.tables = tables; 9 | } 10 | 11 | async deleteAllData() { 12 | localStorage.clear(); 13 | for (let table of Object.values(this.tables)) { 14 | await table.clear(); 15 | } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /client/src/components/base/button/LineButton.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /server/integration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | WORKDIR /app 3 | 4 | COPY integration ./integration 5 | COPY util/ ./util 6 | COPY mongoutil/ ./mongoutil 7 | COPY proto/ ./proto 8 | 9 | WORKDIR /app/integration 10 | RUN go build -o integration cmd/integration/integration.go 11 | 12 | FROM alpine:latest 13 | WORKDIR /root/ 14 | RUN apk add --no-cache tzdata 15 | COPY --from=builder /app/integration/integration . 16 | 17 | EXPOSE 8080 18 | CMD ["./integration"] 19 | -------------------------------------------------------------------------------- /client/src/components/form/editor/display/segmented/EditSegmentedOptionCard.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/src/components/journal/day/JournalSummaryContainer.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | {@render children()} 9 |
10 | 11 | 22 | -------------------------------------------------------------------------------- /client/src/components/trackable/edit/general/EditTrackableAnalytics.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /client/src/model/ui/dynamicInput.ts: -------------------------------------------------------------------------------- 1 | export interface DynamicInputEntity { 2 | id: string; 3 | type: string; 4 | name: string; 5 | fields: DynamicInputField[]; 6 | } 7 | 8 | export interface DynamicInputField { 9 | id: string; 10 | name: string; 11 | nested?: boolean; 12 | fields?: DynamicInputField[]; 13 | } 14 | 15 | export interface DynamicInputAnswer { 16 | id: string; 17 | type: string; 18 | name: string; 19 | answers: string[]; 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/goal/single/GoalMetSingleCondition.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 |

{value.name}

11 |
12 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/freeText/FreeTextSearchOptions.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 10 |
-------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import plugin from "tailwindcss/plugin"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | "index.html", 7 | "./src/**/*.{svelte,js,ts,jsx,tsx}" 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [ 13 | plugin(function({ addVariant, e }) { 14 | addVariant('pointer-feedback', ['@media (pointer: fine) { &:hover }','@media (pointer: coarse) { &:active }']); 15 | }) 16 | , 17 | ], 18 | } 19 | 20 | -------------------------------------------------------------------------------- /client/src/stores/feedback/feedback.ts: -------------------------------------------------------------------------------- 1 | import ky, {type KyInstance} from "ky"; 2 | 3 | export class FeedbackStore { 4 | private httpClient: KyInstance; 5 | 6 | constructor() { 7 | this.httpClient = ky.extend({ 8 | prefixUrl: import.meta.env.VITE_BACKEND_URL 9 | }) 10 | } 11 | 12 | async sendFeedback(feedback: string): Promise { 13 | return (await this.httpClient.post("feedback", { 14 | body: feedback 15 | })).ok; 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /client/src/util/promise.ts: -------------------------------------------------------------------------------- 1 | export function emptyPromise(): Promise { 2 | return new Promise((_) => { 3 | }); 4 | } 5 | 6 | export function resolvedPromise(v: T): Promise { 7 | return new Promise((resolve) => resolve(v)); 8 | } 9 | 10 | export function resolvedUpdatePromise(promise: Promise, updater: (v: T) => T): Promise { 11 | return new Promise(async (resolve) => { 12 | let existing = await promise; 13 | resolve(updater(existing)); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/base/button/HorizontalPlusButton.svelte: -------------------------------------------------------------------------------- 1 | 8 | 14 | -------------------------------------------------------------------------------- /client/src/components/dashboard/types/table/TableWidgetGroupHeader.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {name} 10 | 11 |
12 | -------------------------------------------------------------------------------- /client/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | 14 | @Test 15 | public void addition_isCorrect() throws Exception { 16 | assertEquals(4, 2 + 2); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/capacitor.config.ts: -------------------------------------------------------------------------------- 1 | import type {CapacitorConfig} from '@capacitor/cli'; 2 | 3 | const config: CapacitorConfig = { 4 | appId: 'io.perfice.app', 5 | appName: 'Perfice', 6 | webDir: 'dist', 7 | android: { 8 | adjustMarginsForEdgeToEdge: 'force', 9 | }, 10 | plugins: { 11 | LocalNotifications: { 12 | smallIcon: "res://drawable/small_icon", 13 | largeIcon: "res://drawable/splash", 14 | iconColor: "#16A34A" 15 | } 16 | } 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /client/src/components/base/iconLabel/IconLabelBetween.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 12 | {@render children()} 13 |
14 | -------------------------------------------------------------------------------- /client/src/components/dashboard/sidebar/edit/types/welcome/EditWelcomeWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/base/title/Title.svelte: -------------------------------------------------------------------------------- 1 | 11 | 15 | -------------------------------------------------------------------------------- /client/src/services/notification/web.ts: -------------------------------------------------------------------------------- 1 | import type {NotificationScheduler} from "@perfice/services/notification/notification"; 2 | import type {StoredNotification} from "@perfice/model/notification/notification"; 3 | 4 | export class WebNotificationScheduler implements NotificationScheduler { 5 | 6 | async scheduleStoredNotifications(stored: StoredNotification[]) { 7 | 8 | } 9 | 10 | async scheduleNotification(notification: StoredNotification) { 11 | 12 | } 13 | 14 | async unscheduleNotification(nativeId: number) { 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /client/src/components/journal/day/JournalCardBase.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /client/src/components/goal/multi/MultiConditionRenderer.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {#each value as val} 10 | 11 | {/each} 12 |
13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | Perfice.drawio 27 | .$Perfice.drawio.bkp 28 | 29 | dev-dist 30 | deploy-apk.sh 31 | build-signed.sh 32 | build-local.sh 33 | deploy.sh 34 | *.drawio 35 | *.drawio.bkp 36 | 37 | .env.development 38 | .env -------------------------------------------------------------------------------- /client/src/components/base/timeScope/SimpleTimeScopePicker.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /client/src/components/form/fields/input/BooleanInputFormField.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/components/variable/edit/EditBackButton.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /client/android/variables.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | minSdkVersion = 23 3 | compileSdkVersion = 35 4 | targetSdkVersion = 35 5 | androidxActivityVersion = '1.9.2' 6 | androidxAppCompatVersion = '1.7.0' 7 | androidxCoordinatorLayoutVersion = '1.2.0' 8 | androidxCoreVersion = '1.15.0' 9 | androidxFragmentVersion = '1.8.4' 10 | coreSplashScreenVersion = '1.0.1' 11 | androidxWebkitVersion = '1.12.1' 12 | junitVersion = '4.13.2' 13 | androidxJunitVersion = '1.2.1' 14 | androidxEspressoCoreVersion = '3.6.1' 15 | cordovaAndroidVersion = '10.1.1' 16 | } -------------------------------------------------------------------------------- /client/src/components/reflection/GlobalReflectionModal.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/components/tag/TagButtonBase.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /client/src/stores/import/complete.ts: -------------------------------------------------------------------------------- 1 | import type {CompleteImportService} from "@perfice/services/import/complete/complete"; 2 | import {BASE_URL} from "@perfice/app"; 3 | 4 | export class CompleteImportStore { 5 | 6 | private readonly importService: CompleteImportService; 7 | 8 | constructor(importService: CompleteImportService) { 9 | this.importService = importService; 10 | } 11 | 12 | async import(file: File, newFormat: boolean) { 13 | await this.importService.import(file, newFormat); 14 | window.location.href = BASE_URL + "/"; 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /client/src/components/dashboard/sidebar/edit/types/newCorrelations/EditNewCorrelationsWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /client/src/components/goal/GoalNewCard.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /client/src/components/sharedWidgets/checklist/EditChecklistConditionCard.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/stores/export/formEntry.ts: -------------------------------------------------------------------------------- 1 | import type {EntryExportService} from "@perfice/services/export/formEntries/export"; 2 | 3 | export class EntryExportStore { 4 | 5 | private readonly exportService: EntryExportService; 6 | 7 | constructor(exportService: EntryExportService) { 8 | this.exportService = exportService; 9 | } 10 | 11 | async exportJson(formId: string) { 12 | return await this.exportService.exportJson(formId); 13 | } 14 | 15 | async exportCsv(formId: string) { 16 | return await this.exportService.exportCsv(formId); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /client/src/swSetup.ts: -------------------------------------------------------------------------------- 1 | import {BASE_URL} from "@perfice/app"; 2 | 3 | export function setupServiceWorker() { 4 | if ("serviceWorker" in navigator) { 5 | let registration = navigator.serviceWorker 6 | .register( 7 | BASE_URL + "/sw.js", 8 | // import.meta.env.MODE === "production" 9 | // ? "/app/sw.js" 10 | // : "/dev-sw.js?dev-sw", 11 | // {type: import.meta.env.MODE === 'production' ? 'classic' : 'module'}, 12 | ) 13 | .then((r) => { 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/components/journal/day/JournalDayDate.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /client/src/components/base/button/IconButton.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /server/sync/internal/salt_controller.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "github.com/gofiber/fiber/v2" 4 | 5 | type SaltController struct { 6 | saltService *SaltService 7 | } 8 | 9 | func NewSaltController(saltService *SaltService) *SaltController { 10 | return &SaltController{saltService} 11 | } 12 | 13 | type SaltResponse struct { 14 | Salt []byte `json:"salt"` 15 | } 16 | 17 | func (c *SaltController) GetSalt(ctx *fiber.Ctx) error { 18 | userId := getUserId(ctx) 19 | salt, err := c.saltService.GetSalt(userId) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | return ctx.JSON(SaltResponse{salt}) 25 | } 26 | -------------------------------------------------------------------------------- /client/src/components/integration/EditIntegrationWebhook.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |

Webhook

11 |

Updates should be sent to this URL:

12 | e.currentTarget.select()}/> -------------------------------------------------------------------------------- /client/android/app/src/main/java/dev/adoe/perfice/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.perfice.app; 2 | 3 | import android.os.Bundle; 4 | import android.webkit.WebView; 5 | 6 | import com.getcapacitor.BridgeActivity; 7 | 8 | public class MainActivity extends BridgeActivity { 9 | @Override 10 | public void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | } 13 | 14 | @Override 15 | public void onStart() { 16 | super.onStart(); 17 | WebView webview = getBridge().getWebView(); 18 | webview.setOverScrollMode(WebView.OVER_SCROLL_NEVER); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /client/src/model/analytics/ui.ts: -------------------------------------------------------------------------------- 1 | import type { SegmentedItem } from "@perfice/model/ui/segmented"; 2 | 3 | export enum AnalyticsViewType { 4 | TRACKABLES, 5 | TAGS, 6 | CORRELATIONS 7 | } 8 | 9 | export const ANALYTICS_SEGMENTED_ITEMS: SegmentedItem[] = [ 10 | { name: "Trackables", value: AnalyticsViewType.TRACKABLES }, 11 | { name: "Tags", value: AnalyticsViewType.TAGS }, 12 | { name: "Correlations", value: AnalyticsViewType.CORRELATIONS }, 13 | ]; 14 | 15 | export function getAnalyticsDetailsLink(type: string, id: string): string { 16 | return `/analytics/${type}:${id}`; 17 | } 18 | -------------------------------------------------------------------------------- /client/tests/common.ts: -------------------------------------------------------------------------------- 1 | import {JournalEntry} from "../src/model/journal/journal"; 2 | import {pDisplay, PrimitiveValue, pString} from "../src/model/primitive/primitive"; 3 | 4 | export function mockEntry(id: string, formId: string, answers: Record, timestamp: number = 0): JournalEntry { 5 | return { 6 | id, 7 | formId, 8 | snapshotId: "", 9 | timestamp, 10 | integration: null, 11 | displayValue: "", 12 | answers: Object.fromEntries(Object.entries(answers) 13 | .map(([k, v]) => [k, pDisplay(v, pString(v.value.toString()))])) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/analytics/details/CorrelationMessage.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

9 | 10 | {display.between} 11 | 12 |

-------------------------------------------------------------------------------- /client/src/components/form/editor/display/select/EditSelectOptionCard.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /client/src/components/reflection/modal/ReflectionPageButton.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /server/mongoutil/go.mod: -------------------------------------------------------------------------------- 1 | module perfice.adoe.dev/mongoutil 2 | 3 | go 1.24.3 4 | 5 | require go.mongodb.org/mongo-driver v1.17.3 6 | 7 | require ( 8 | github.com/golang/snappy v0.0.4 // indirect 9 | github.com/klauspost/compress v1.16.7 // indirect 10 | github.com/montanaflynn/stats v0.7.1 // indirect 11 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 12 | github.com/xdg-go/scram v1.1.2 // indirect 13 | github.com/xdg-go/stringprep v1.0.4 // indirect 14 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 15 | golang.org/x/crypto v0.26.0 // indirect 16 | golang.org/x/sync v0.8.0 // indirect 17 | golang.org/x/text v0.17.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /client/src/db/migration/migrations/chartTitles.ts: -------------------------------------------------------------------------------- 1 | import type {Migration} from "@perfice/db/migration/migration"; 2 | 3 | export class ChartTitlesMigration implements Migration { 4 | async apply(entity: any): Promise { 5 | if (entity.type != "CHART") { 6 | return entity; 7 | } 8 | 9 | return { 10 | ...entity, 11 | settings: { 12 | ...entity.settings, 13 | title: null 14 | } 15 | }; 16 | } 17 | 18 | getEntityType(): string { 19 | return "dashboardWidgets"; 20 | } 21 | 22 | getVersion(): number { 23 | return 1; 24 | } 25 | } -------------------------------------------------------------------------------- /client/src/assets/goal_suggestions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Daily steps", 4 | "color": "#ff0000", 5 | "conditions": [ 6 | { 7 | "type": "COMPARISON", 8 | "value": { 9 | "source": "steps", 10 | "operator": "GREATER_THAN_EQUAL", 11 | "target": 5000 12 | } 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "Good sleep", 18 | "color": "#ff0000", 19 | "conditions": [ 20 | { 21 | "type": "COMPARISON", 22 | "value": { 23 | "source": "steps", 24 | "operator": "GREATER_THAN_EQUAL", 25 | "target": 5000 26 | } 27 | } 28 | ] 29 | } 30 | ] -------------------------------------------------------------------------------- /client/src/stores/dashboard/widget/welcome.ts: -------------------------------------------------------------------------------- 1 | import quotesUrl from '/quotes.json?url'; 2 | 3 | const quotesStr = await fetch(quotesUrl).then(r => r.text()); 4 | export const quotes = JSON.parse(quotesStr); 5 | 6 | export function fetchRandomQuote(): string { 7 | return quotes[Math.floor(Math.random() * quotes.length)].quote; 8 | } 9 | 10 | 11 | export function getTimeOfDayText(): string { 12 | const now = new Date(); 13 | if (now.getHours() >= 0 && now.getHours() < 6) return "night"; 14 | if (now.getHours() >= 6 && now.getHours() < 13) return "morning"; 15 | if (now.getHours() >= 13 && now.getHours() < 17) return "afternoon"; 16 | return "evening"; 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/reflection/editor/sidebar/widget/ReflectionEditTableWidget.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/components/dashboard/types/entryRow/EntryRowItem.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | {#if entry.icon} 11 | 12 | {:else} 13 | {entry.value} 14 | {/if} 15 | {formatTimestampHHMM(entry.timestamp)} 16 |
17 | -------------------------------------------------------------------------------- /server/integration/internal/collection/integration_entity.go: -------------------------------------------------------------------------------- 1 | package collection 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/mongo" 5 | "perfice.adoe.dev/integration/internal/model" 6 | "perfice.adoe.dev/mongoutil" 7 | ) 8 | 9 | type IntegrationEntityCollection struct { 10 | collection *mongo.Collection 11 | } 12 | 13 | func NewIntegrationEntityCollection(collection *mongo.Collection) *IntegrationEntityCollection { 14 | return &IntegrationEntityCollection{collection} 15 | } 16 | 17 | func (c *IntegrationEntityCollection) FindIntegrationEntities() ([]model.IntegrationEntityDefinition, error) { 18 | return mongoutil.Find[model.IntegrationEntityDefinition](c.collection, nil) 19 | } 20 | -------------------------------------------------------------------------------- /client/src/components/form/fields/textArea/TextAreaFormField.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 21 | -------------------------------------------------------------------------------- /client/src/components/settings/SettingsDataExport.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |

Export data

14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /client/src/components/settings/auth/login/ResendConfirmation.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if !sent} 17 | 18 | {:else} 19 |

Successfully resent email

20 | {/if} 21 | -------------------------------------------------------------------------------- /client/src/components/form/fields/input/VanillaInputFormField.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /server/util/number.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func CastToFloat(value any) float64 { 4 | switch v := value.(type) { 5 | case float64: 6 | case float32: 7 | return float64(v) 8 | case int: 9 | return float64(v) 10 | case int64: 11 | return float64(v) 12 | case uint: 13 | return float64(v) 14 | case uint64: 15 | return float64(v) 16 | } 17 | 18 | return 0.0 19 | } 20 | 21 | func CastToInt(value any) int { 22 | switch v := value.(type) { 23 | case int: 24 | return v 25 | case int64: 26 | return int(v) 27 | case uint: 28 | return int(v) 29 | case uint64: 30 | return int(v) 31 | case float64: 32 | return int(v) 33 | case float32: 34 | return int(v) 35 | } 36 | 37 | return 0 38 | } 39 | -------------------------------------------------------------------------------- /server/integration/internal/collection/integration_type.go: -------------------------------------------------------------------------------- 1 | package collection 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson" 5 | "go.mongodb.org/mongo-driver/mongo" 6 | "perfice.adoe.dev/integration/internal/model" 7 | "perfice.adoe.dev/mongoutil" 8 | ) 9 | 10 | type IntegrationTypeCollection struct { 11 | collection *mongo.Collection 12 | } 13 | 14 | func NewIntegrationTypeCollection(collection *mongo.Collection) *IntegrationTypeCollection { 15 | return &IntegrationTypeCollection{collection} 16 | } 17 | 18 | func (c *IntegrationTypeCollection) FindIntegrationTypes() ([]model.IntegrationTypeDefinition, error) { 19 | return mongoutil.Find[model.IntegrationTypeDefinition](c.collection, bson.M{}) 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/base/modal/generic/GenericInfoModal.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 23 |

{message}

24 |
-------------------------------------------------------------------------------- /client/src/db/migration/migrations/defaultQuestionValues.ts: -------------------------------------------------------------------------------- 1 | import type {Migration} from "@perfice/db/migration/migration"; 2 | import type {Form} from "@perfice/model/form/form"; 3 | 4 | export class FormQuestionDefaultValuesMigration implements Migration { 5 | async apply(entity: Form): Promise { 6 | return { 7 | ...entity, questions: entity.questions.map(q => { 8 | return { 9 | ...q, 10 | defaultValue: null 11 | } 12 | }) 13 | }; 14 | } 15 | 16 | getEntityType(): string { 17 | return "forms"; 18 | } 19 | 20 | getVersion(): number { 21 | return 2; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/reflection/editor/sidebar/widget/ReflectionEditChecklistWidget.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/components/form/fields/segmented/SegmentedFormField.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | { 13 | return { 14 | name: o.text, 15 | value: o.value.value, 16 | } 17 | })} {onChange}/> 18 | -------------------------------------------------------------------------------- /docs/selfhost/integrations/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: integrations 3 | --- 4 | 5 | # Integrations 6 | Integrations are what allow you to synchronize data from remote providers (like Fitbit, Todoist, etc.) directly into the platform. This greatly automates things and leaves the burden of having to manually type in all your data. 7 | 8 | ## Pulling data to Perfice 9 | Pull data to Perfice by setting up an [Integration type](./types) and [Integration entities](./entities). 10 | 11 | ## Pushing data to Perfice 12 | Pushing data to Perfice is possible by configuring a `push` source in an [Integration entity](./entities). 13 | 14 | ## Local integrations 15 | Local integrations like Android HealthConnect or Apple Health are currently not yet supported. -------------------------------------------------------------------------------- /client/src/components/goal/single/ComparisonSingleCondition.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {formatComparisonNumberValues(first, second, dataType, unit)} 13 | 14 | -------------------------------------------------------------------------------- /client/src/components/analytics/QuestionLabel.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 26 | -------------------------------------------------------------------------------- /client/src/components/base/modal/generic/GenericDeleteModal.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /client/src/components/dashboard/sidebar/edit/types/entryRow/EditEntryRowWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "moduleDetection": "force", 18 | "paths": { 19 | "@perfice/*": ["./src/*"] 20 | } 21 | }, 22 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] 23 | } 24 | -------------------------------------------------------------------------------- /server/integration/internal/controller/integration_webhook.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v2" 5 | "perfice.adoe.dev/integration/internal/service" 6 | ) 7 | 8 | type IntegrationWebhookController struct { 9 | service *service.IntegrationWebhookService 10 | } 11 | 12 | func NewIntegrationWebhookController(service *service.IntegrationWebhookService) *IntegrationWebhookController { 13 | return &IntegrationWebhookController{service} 14 | } 15 | 16 | func (c *IntegrationWebhookController) HandleWebhook(ctx *fiber.Ctx) error { 17 | token := ctx.Params("token") 18 | body := ctx.Body() 19 | 20 | if err := (*c.service).HandleWebhook(token, body); err != nil { 21 | return err 22 | } 23 | return ctx.SendStatus(fiber.StatusOK) 24 | } 25 | -------------------------------------------------------------------------------- /client/src/stores/dashboard/widget/trackable.ts: -------------------------------------------------------------------------------- 1 | import {type Readable, writable} from "svelte/store"; 2 | import type {DashboardTrackableWidgetSettings} from "@perfice/model/dashboard/widgets/trackable"; 3 | import type {Trackable} from "@perfice/model/trackable/trackable"; 4 | import {trackables} from "@perfice/stores"; 5 | 6 | export interface TrackableWidgetResult { 7 | trackable: Trackable; 8 | } 9 | 10 | export function TrackableWidget(settings: DashboardTrackableWidgetSettings): Readable> { 11 | return writable(new Promise(async (resolve) => { 12 | let trackable = await trackables.getTrackableById(settings.trackableId, true); 13 | if (trackable == null) return; 14 | 15 | resolve({trackable}); 16 | })); 17 | } -------------------------------------------------------------------------------- /client/tests/time-scope.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from "vitest"; 2 | import {dateToEndOfTimeScope, dateToStartOfTimeScope} from "../src/util/time/simple"; 3 | import {SimpleTimeScopeType, WeekStart} from "../src/model/variable/time/time"; 4 | 5 | test("date to start of week monday", () => { 6 | let date = new Date(2025, 0, 16); 7 | expect(dateToStartOfTimeScope(date, SimpleTimeScopeType.WEEKLY, WeekStart.MONDAY).getTime()) 8 | .toEqual(new Date(2025, 0, 13, 0, 0, 0, 0).getTime()) 9 | }) 10 | 11 | test("date to end of week monday", () => { 12 | let date = new Date(2025, 0, 16); 13 | expect(dateToEndOfTimeScope(date, SimpleTimeScopeType.WEEKLY, WeekStart.MONDAY).getTime()) 14 | .toEqual(new Date(2025, 0, 19, 23, 59, 59, 999).getTime()) 15 | }) 16 | -------------------------------------------------------------------------------- /client/src/components/base/gesture/SwipeDetector.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true, 22 | "paths": { 23 | "@perfice/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["vite.config.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /client/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:8.7.2' 11 | classpath 'com.google.gms:google-services:4.4.2' 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | apply from: "variables.gradle" 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | mavenCentral() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/base/title/TitleAndCalendar.svelte: -------------------------------------------------------------------------------- 1 | 13 |
14 | 15 | <CalendarScroll value={date} onChange={onDateChange}/> 16 | </div> 17 | -------------------------------------------------------------------------------- /client/src/components/base/card/GenericActionsCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {type IconDefinition} from "@fortawesome/free-solid-svg-icons"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | import type {Snippet} from "svelte"; 6 | 7 | let {icon, text, actions}: { 8 | icon?: IconDefinition, 9 | text: string, 10 | actions: Snippet 11 | } = $props(); 12 | </script> 13 | 14 | <div class="border p-2 rounded-xl flex justify-between items-center bg-white"> 15 | <div class="row-gap"> 16 | {#if icon != null} 17 | <Fa icon={icon}/> 18 | {/if} 19 | 20 | {text} 21 | </div> 22 | <div class="row-gap text-gray-500"> 23 | {@render actions()} 24 | </div> 25 | </div> 26 | -------------------------------------------------------------------------------- /client/src/components/sidebar/drawer/DrawerButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | // noinspection ES6UnusedImports 3 | import Fa from "svelte-fa"; 4 | import type {SidebarLink} from "@perfice/model/ui/sidebar"; 5 | import {toggleDrawer} from "@perfice/stores/ui/drawer"; 6 | import {navigate} from "@perfice/app"; 7 | 8 | let {link, active}: { link: SidebarLink, active: boolean } = $props(); 9 | 10 | function onClick() { 11 | navigate(link.path); 12 | toggleDrawer(); 13 | } 14 | </script> 15 | 16 | <button class="flex p-3 row-gap hover-feedback w-full" 17 | class:text-gray-500={!active} class:text-green-500={active} 18 | onclick={onClick}> 19 | <Fa class="w-6" icon={link.icon}/> 20 | <span>{link.title}</span> 21 | </button> 22 | -------------------------------------------------------------------------------- /client/src/util/perf.ts: -------------------------------------------------------------------------------- 1 | import {parseJsonFromLocalStorage} from "@perfice/util/local"; 2 | 3 | interface PerfEntry<D> { 4 | data: D; 5 | time: number; 6 | } 7 | 8 | let perf = parseJsonFromLocalStorage<PerfEntry<any>[]>("perf") ?? []; 9 | 10 | export function getPerfSummary() { 11 | let top = perf.sort((a, b) => b.time - a.time).slice(0, 10); 12 | return top.map(e => `${e.data}: ${e.time}ms`).join("\n"); 13 | } 14 | 15 | export function debugPerformance<T, D>(data: D, callback: () => T): T { 16 | let start = performance.now(); 17 | let value = callback(); 18 | let time = performance.now() - start; 19 | 20 | perf.push({ 21 | data, 22 | time 23 | }); 24 | 25 | localStorage.setItem("perf", JSON.stringify(perf)); 26 | return value; 27 | } -------------------------------------------------------------------------------- /client/src/components/base/inline/InlineCreateLineButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import InlineCreateInput from "@perfice/components/base/inline/InlineCreateInput.svelte"; 3 | import LineButton from "@perfice/components/base/button/LineButton.svelte"; 4 | 5 | let {onSubmit}: { 6 | onSubmit: (name: string) => void 7 | } = $props(); 8 | 9 | let addingCategory = $state(false); 10 | 11 | function addCategory() { 12 | addingCategory = true; 13 | } 14 | </script> 15 | {#if addingCategory} 16 | <div class="mt-4"> 17 | <InlineCreateInput class="w-full" onBlur={() => addingCategory = false} 18 | onSubmit={(name) => onSubmit(name)}/> 19 | </div> 20 | {:else} 21 | <LineButton onClick={addCategory}/> 22 | {/if} 23 | -------------------------------------------------------------------------------- /client/src/components/dashboard/sidebar/edit/types/checklist/EditChecklistWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Form} from "@perfice/model/form/form"; 3 | import type { 4 | DashboardChecklistWidgetSettings 5 | } from "@perfice/model/dashboard/widgets/checklist"; 6 | import EditChecklistWidgetSettings 7 | from "@perfice/components/sharedWidgets/checklist/EditChecklistWidgetSettings.svelte"; 8 | 9 | let {settings, onChange, forms}: { 10 | settings: DashboardChecklistWidgetSettings, 11 | onChange: (settings: DashboardChecklistWidgetSettings) => void, 12 | forms: Form[], 13 | dependencies: Record<string, string> 14 | } = $props(); 15 | </script> 16 | 17 | <EditChecklistWidgetSettings {settings} {onChange} {forms}/> -------------------------------------------------------------------------------- /client/src/components/dashboard/types/checkList/ChecklistEntry.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faCheckCircle} from "@fortawesome/free-solid-svg-icons"; 3 | import {faCheckCircle as regularCheckCircle} from "@fortawesome/free-regular-svg-icons"; 4 | import IconButton from "@perfice/components/base/button/IconButton.svelte"; 5 | 6 | let {checked, name, onClick}: { checked: boolean, name: string, onClick: () => void } = $props(); 7 | </script> 8 | 9 | <div class="flex gap-1 items-center [&:not(:last-child)]:border-b px-1"> 10 | <IconButton icon={checked ? faCheckCircle : regularCheckCircle} 11 | {onClick} 12 | class="text-xl {checked ? 'text-green-500' : 'text-gray-500'}" 13 | /> 14 | <span class="text-sm">{name}</span> 15 | </div> 16 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/tag/filters/TagOneOfFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type { OneOfFilter } from "@perfice/model/journal/search/search"; 3 | import type { JournalSearchUiDependencies } from "@perfice/model/journal/search/ui"; 4 | import OneOfFilterCard from "@perfice/components/journal/search/common/OneOfFilterCard.svelte"; 5 | 6 | let { 7 | filter, 8 | onChange, 9 | onDelete, 10 | dependencies, 11 | }: { 12 | filter: OneOfFilter; 13 | onChange: (filter: OneOfFilter) => void; 14 | onDelete: () => void; 15 | dependencies: JournalSearchUiDependencies; 16 | } = $props(); 17 | </script> 18 | 19 | <OneOfFilterCard 20 | items={dependencies.tags.map((t) => ({ name: t.name, value: t.id }))} 21 | {onDelete} 22 | {onChange} 23 | {filter} 24 | /> 25 | -------------------------------------------------------------------------------- /client/src/db/dexie/encryption.ts: -------------------------------------------------------------------------------- 1 | import type {EncryptionKey} from "@perfice/model/sync/sync"; 2 | import type {EntityTable} from "dexie"; 3 | import type {EncryptionKeyCollection} from "@perfice/db/collections"; 4 | 5 | const AUTH_KEY_ID = 'main'; 6 | 7 | export class DexieEncryptionKeyCollection implements EncryptionKeyCollection { 8 | private table: EntityTable<EncryptionKey, 'id'>; 9 | 10 | constructor(table: EntityTable<EncryptionKey, 'id'>) { 11 | this.table = table; 12 | } 13 | 14 | async getKey(): Promise<CryptoKey | null> { 15 | const key = await this.table.get(AUTH_KEY_ID); 16 | return key?.key ?? null; 17 | } 18 | 19 | async put(key: CryptoKey): Promise<void> { 20 | await this.table.put({id: AUTH_KEY_ID, key}); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /client/android/app/capacitor.build.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | 3 | android { 4 | compileOptions { 5 | sourceCompatibility JavaVersion.VERSION_21 6 | targetCompatibility JavaVersion.VERSION_21 7 | } 8 | } 9 | 10 | apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" 11 | dependencies { 12 | implementation project(':capacitor-app') 13 | implementation project(':capacitor-browser') 14 | implementation project(':capacitor-filesystem') 15 | implementation project(':capacitor-local-notifications') 16 | implementation project(':capacitor-share') 17 | implementation project(':capacitor-secure-storage') 18 | 19 | } 20 | 21 | 22 | if (hasProperty('postBuildExtras')) { 23 | postBuildExtras() 24 | } 25 | -------------------------------------------------------------------------------- /client/src/components/base/dropdown/BindableDropdownButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts" generics="T"> 2 | import DropdownButton from "./DropdownButton.svelte"; 3 | import type {DropdownMenuItemDetails} from "@perfice/model/ui/dropdown"; 4 | 5 | let {value = $bindable(), class: className = '', onChange, items, ...rest}: { 6 | value: T, 7 | items: DropdownMenuItemDetails<T>[], 8 | class?: string, 9 | rest?: any[], 10 | onChange?: (t: T) => void 11 | } = $props(); 12 | 13 | function change(i: T) { 14 | value = i; 15 | onChange?.(i); 16 | } 17 | </script> 18 | 19 | <DropdownButton class={className} {...rest} {value} items={items.map(i => { 20 | return { 21 | ...i, 22 | action: () => change(i.value), 23 | } 24 | })}/> 25 | -------------------------------------------------------------------------------- /client/src/components/dashboard/sidebar/edit/types/table/EditTableWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import EditTableWidgetSettings from "@perfice/components/sharedWidgets/table/EditTableWidgetSettings.svelte"; 3 | import type {TableWidgetSettings} from "@perfice/model/sharedWidgets/table/table.js"; 4 | import type {Form} from "@perfice/model/form/form.js"; 5 | import type {DashboardTableWidgetSettings} from "@perfice/model/dashboard/widgets/table"; 6 | 7 | let {settings, onChange, forms}: { 8 | settings: DashboardTableWidgetSettings, 9 | onChange: (settings: DashboardTableWidgetSettings) => void, 10 | forms: Form[], 11 | dependencies: Record<string, string> 12 | } = $props(); 13 | </script> 14 | 15 | <EditTableWidgetSettings {settings} {onChange} {forms}/> -------------------------------------------------------------------------------- /client/src/components/base/calendarScroll/CalendarScrollItem.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {WEEK_DAYS_SHORT} from "@perfice/util/time/format"; 3 | 4 | let {value, selectedValue, onClick}: { value: Date, selectedValue: Date, onClick: () => void } = $props(); 5 | 6 | let selected = $derived(selectedValue.getTime() == value.getTime()); 7 | </script> 8 | 9 | <button 10 | onclick={onClick} 11 | class="hover-feedback flex w-10 h-10 md:h-full h-full md:w-12 flex-1 md:flex-initial flex-col aspect-square 12 | justify-center items-center px-4 py-2 text-xs md:rounded-md md:border 13 | bg-white {selected ? 'md:border-green-500 text-green-500' : 'md:border'}" 14 | > 15 | <span class="text-[10px] font-bold ">{WEEK_DAYS_SHORT[value.getDay()]}</span> 16 | {value.getDate()} 17 | </button> 18 | -------------------------------------------------------------------------------- /client/src/components/base/inline/InlineCreateInput.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {onMount} from "svelte"; 3 | 4 | let {onSubmit, class: className = 'w-24', onBlur}: { 5 | onSubmit: (name: string) => void, 6 | onBlur: () => void, 7 | class?: string 8 | } = $props(); 9 | 10 | let input: HTMLInputElement; 11 | 12 | function onKeyDown(e: KeyboardEvent & { currentTarget: HTMLInputElement }) { 13 | if (e.key !== "Enter") return; 14 | 15 | onSubmit(e.currentTarget.value); 16 | e.currentTarget.value = ""; 17 | } 18 | 19 | onMount(() => { 20 | input.focus(); 21 | }); 22 | </script> 23 | 24 | <input type="text" bind:this={input} class="{className} rounded-xl text-sm" 25 | placeholder="Name" onblur={onBlur} onkeydown={onKeyDown}/> 26 | -------------------------------------------------------------------------------- /client/src/components/base/invertedSegmented/InvertedSegment.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {Snippet} from "svelte"; 3 | 4 | let {children, onClick, active = false}: { 5 | children: Snippet, 6 | onClick: () => void, 7 | active?: boolean 8 | } = $props(); 9 | </script> 10 | 11 | <button 12 | onclick={onClick} 13 | class="rounded-xl text-center flex gap-1 justify-center items-center {active ? 'active':'inactive'} text-ellipsis flex-1 px-1 md:px-2" 14 | > 15 | {@render children?.()} 16 | </button> 17 | 18 | <style> 19 | @reference "@perfice/app.css"; 20 | .active { 21 | @apply bg-white text-black; 22 | } 23 | 24 | .inactive { 25 | @apply text-white pointer-feedback:bg-white pointer-feedback:text-black; 26 | } 27 | </style> 28 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/trackable/filters/TrackableOneOfFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {OneOfFilter} from "@perfice/model/journal/search/search"; 3 | import type {JournalSearchUiDependencies} from "@perfice/model/journal/search/ui"; 4 | import OneOfFilterCard from "@perfice/components/journal/search/common/OneOfFilterCard.svelte"; 5 | 6 | let {filter, onChange, onDelete, dependencies}: { 7 | filter: OneOfFilter, 8 | onChange: (filter: OneOfFilter) => void, 9 | onDelete: () => void, 10 | dependencies: JournalSearchUiDependencies 11 | } = $props(); 12 | </script> 13 | 14 | <OneOfFilterCard items={dependencies.trackables.map(t => ({name: t.name, value: t.id}))} 15 | onDelete={onDelete} onChange={onChange} filter={filter}/> -------------------------------------------------------------------------------- /client/src/components/analytics/details/trackable/BasicCategoricalAnalyticsRow.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faArrowDown, faArrowUp} from "@fortawesome/free-solid-svg-icons"; 3 | import type {CategoricalBasicAnalytics} from "@perfice/services/analytics/analytics"; 4 | import TitledCard from "@perfice/components/base/card/TitledCard.svelte"; 5 | 6 | let {analytics}: { analytics: CategoricalBasicAnalytics } = $props(); 7 | </script> 8 | <TitledCard 9 | class="flex-1" 10 | title="Most common" 11 | icon={faArrowUp} 12 | description={analytics.mostCommon.category} 13 | ></TitledCard> 14 | <TitledCard 15 | class="flex-1 rounded-xl" 16 | title="Least common" 17 | icon={faArrowDown} 18 | description={analytics.leastCommon.category} 19 | ></TitledCard> 20 | -------------------------------------------------------------------------------- /client/src/components/dashboard/types/table/DashboardTableWidget.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardTableWidgetSettings} from "@perfice/model/dashboard/widgets/table"; 3 | import {dashboardDate} from "@perfice/stores/dashboard/dashboard"; 4 | import {type PrimitiveValue} from "@perfice/model/primitive/primitive"; 5 | import TableWidget from "@perfice/components/sharedWidgets/table/TableWidget.svelte"; 6 | 7 | let {dependencies, openFormModal, settings}: { 8 | settings: DashboardTableWidgetSettings, 9 | dependencies: Record<string, string>, 10 | openFormModal: (formId: string, answers?: Record<string, PrimitiveValue>) => void 11 | } = $props(); 12 | </script> 13 | 14 | <TableWidget {settings} date={$dashboardDate} {openFormModal} listVariableId={dependencies["list"]}/> 15 | -------------------------------------------------------------------------------- /client/src/components/goal/editor/condition/comparison/AddSourceButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faHashtag, faSquareRootVariable} from "@fortawesome/free-solid-svg-icons"; 3 | import PopupContextMenuButton from "@perfice/components/base/contextMenu/PopupContextMenuButton.svelte"; 4 | 5 | let {onAdd}: { onAdd: (constant: boolean) => void } = $props(); 6 | 7 | const GOAL_SOURCE_TYPES = [ 8 | { 9 | name: "Dynamic value", 10 | icon: faSquareRootVariable, 11 | action: () => onAdd(false) 12 | }, 13 | 14 | { 15 | name: "Constant", 16 | icon: faHashtag, 17 | action: () => onAdd(true) 18 | } 19 | ]; 20 | </script> 21 | 22 | <PopupContextMenuButton items={GOAL_SOURCE_TYPES}>Add source</PopupContextMenuButton> 23 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/GenericFilterContainer.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | 3 | import {faFilter, faTrash} from "@fortawesome/free-solid-svg-icons"; 4 | import IconButton from "@perfice/components/base/button/IconButton.svelte"; 5 | import type {Snippet} from "svelte"; 6 | // noinspection ES6UnusedImports 7 | import Fa from "svelte-fa"; 8 | 9 | let {name, onDelete, children}: { name: string, onDelete: () => void, children: Snippet } = $props(); 10 | 11 | </script> 12 | 13 | <div class="border"> 14 | <div class="row-between px-4 py-2"> 15 | <div class="row-gap flex-wrap"> 16 | <Fa icon={faFilter}></Fa> 17 | {name} 18 | {@render children()} 19 | </div> 20 | <IconButton icon={faTrash} onClick={onDelete}/> 21 | </div> 22 | </div> -------------------------------------------------------------------------------- /client/src/components/trackable/edit/general/value/EditTrackableValueRepresentation.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {TextOrDynamic} from "@perfice/model/variable/variable"; 3 | import type {FormQuestion} from "@perfice/model/form/form"; 4 | import EditTextOrDynamic from "@perfice/components/base/textOrDynamic/EditTextOrDynamic.svelte"; 5 | 6 | let {representation, availableQuestions, onChange}: { 7 | representation: TextOrDynamic[], 8 | availableQuestions: FormQuestion[], 9 | onChange: (v: TextOrDynamic[]) => void 10 | } = $props(); 11 | </script> 12 | 13 | <EditTextOrDynamic value={representation} availableDynamic={availableQuestions} 14 | {onChange} 15 | getDynamicId={(v) => v.id} 16 | getDynamicText={(v) => v.name}/> 17 | 18 | -------------------------------------------------------------------------------- /server/util/slice.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func CastSlice[T any](slice []any) []T { 4 | var casted []T 5 | for _, item := range slice { 6 | casted = append(casted, item.(T)) 7 | } 8 | 9 | return casted 10 | } 11 | 12 | func SliceFilter[T any](slice []T, predicate func(val T) bool) []T { 13 | var result = make([]T, 0) 14 | for _, v := range slice { 15 | if predicate(v) { 16 | result = append(result, v) 17 | } 18 | } 19 | 20 | return result 21 | } 22 | 23 | func SliceFind[T any](slice []T, predicate func(val T) bool) *T { 24 | for _, v := range slice { 25 | if predicate(v) { 26 | return &v 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func SliceContains[T comparable](slice []T, item T) bool { 34 | for _, v := range slice { 35 | if v == item { 36 | return true 37 | } 38 | } 39 | return false 40 | } 41 | -------------------------------------------------------------------------------- /client/src/components/base/contextMenu/ContextMenuButtons.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {ContextMenuButton} from "@perfice/model/ui/context-menu"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | 6 | let {buttons}: { buttons: ContextMenuButton[] } = $props(); 7 | </script> 8 | 9 | <div class="flex flex-col max-h-36 overflow-y-scroll scrollbar-hide"> 10 | {#each buttons as button} 11 | <button class="p-2 [&:first-child]:rounded-t-xl [&:last-child]:rounded-b-xl hover-feedback row-gap" 12 | class:border-t={button.separated} onclick={() => button.action()}> 13 | {#if button.icon != null} 14 | <Fa icon={button.icon} class="w-4 text-gray-500"/> 15 | {/if} 16 | {button.name} 17 | </button> 18 | {/each} 19 | </div> 20 | -------------------------------------------------------------------------------- /client/src/stores/remote/remote.ts: -------------------------------------------------------------------------------- 1 | import {type RemoteService, RemoteType} from "@perfice/services/remote/remote"; 2 | 3 | export class RemoteStore { 4 | private remoteService: RemoteService; 5 | 6 | constructor(remoteService: RemoteService) { 7 | this.remoteService = remoteService; 8 | } 9 | 10 | getRemoteUrl(type: RemoteType): string { 11 | return this.remoteService.getRemoteUrl(type); 12 | } 13 | 14 | setRemoteUrl(type: RemoteType, url: string) { 15 | this.remoteService.setRemoteUrl(type, url); 16 | } 17 | 18 | setRemoteEnabled(remoteType: RemoteType, checked: boolean) { 19 | this.remoteService.setRemoteEnabled(remoteType, checked); 20 | } 21 | 22 | isRemoteEnabled(remoteType: RemoteType) { 23 | return this.remoteService.isRemoteEnabled(remoteType); 24 | } 25 | } -------------------------------------------------------------------------------- /client/src/components/form/fields/hierarchy/HierarchyButton.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {HierarchyOption} from "@perfice/model/form/data/hierarchy"; 3 | import {sanitizeColor} from "@perfice/util/color"; 4 | 5 | let {option, onClick, selected}: { option: HierarchyOption, onClick: () => void, selected: boolean } = $props(); 6 | </script> 7 | 8 | <button class="flex-col-center aspect-square rounded-xl border-2 border-transparent " class:selected={selected} 9 | style:background-color={sanitizeColor(option.color)} 10 | onclick={onClick}> 11 | <p class="whitespace-pre-line text-center overflow-hidden text-ellipsis w-full text-sm md:text-base">{option.text}</p> 12 | </button> 13 | 14 | <style> 15 | @reference "../../../../app.css"; 16 | .selected { 17 | @apply border-green-600; 18 | } 19 | </style> 20 | -------------------------------------------------------------------------------- /server/util/rand.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "math/big" 7 | ) 8 | 9 | var characters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 10 | 11 | func GenerateAlphanumericString(keyLength int) (string, error) { 12 | token := make([]rune, keyLength) 13 | for j := 0; j < keyLength; j++ { 14 | index, err := rand.Int(rand.Reader, big.NewInt(int64(len(characters)))) 15 | if err != nil { 16 | return "", err 17 | } 18 | 19 | token[j] = characters[index.Int64()] 20 | } 21 | 22 | return string(token), nil 23 | } 24 | 25 | func GenerateRandomBytesString(keyLength int) (string, error) { 26 | token := make([]byte, keyLength) 27 | _, err := rand.Read(token) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | return base64.StdEncoding.EncodeToString(token), nil 33 | } 34 | -------------------------------------------------------------------------------- /client/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /client/src/components/base/card/GenericEditDeleteCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {faPen, faTrash, type IconDefinition} from "@fortawesome/free-solid-svg-icons"; 3 | import IconButton from "@perfice/components/base/button/IconButton.svelte"; 4 | // noinspection ES6UnusedImports 5 | import Fa from "svelte-fa"; 6 | import GenericActionsCard from "@perfice/components/base/card/GenericActionsCard.svelte"; 7 | 8 | let {icon, text, onDelete, onEdit}: { 9 | icon?: IconDefinition, 10 | text: string, 11 | onDelete: () => void, 12 | onEdit: () => void 13 | } = $props(); 14 | </script> 15 | 16 | <GenericActionsCard {icon} {text}> 17 | {#snippet actions()} 18 | <IconButton icon={faPen} onClick={onEdit}/> 19 | <IconButton icon={faTrash} onClick={onDelete}/> 20 | {/snippet} 21 | </GenericActionsCard> -------------------------------------------------------------------------------- /client/src/components/form/editor/display/range/EditRangeQuestionSettings.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {FormQuestionDataType} from "@perfice/model/form/form"; 3 | import type {RangeFormQuestionSettings} from "@perfice/model/form/display/range"; 4 | 5 | let {settings, onChange, dataType, dataSettings}: { 6 | settings: RangeFormQuestionSettings, 7 | onChange: (settings: RangeFormQuestionSettings) => void, 8 | dataType: FormQuestionDataType, 9 | dataSettings: any 10 | } = $props(); 11 | 12 | function onStepChange(e: { currentTarget: HTMLInputElement }) { 13 | onChange({...settings, step: parseInt(e.currentTarget.value)}); 14 | } 15 | </script> 16 | 17 | <div class="row-between"> 18 | Step 19 | <input type="number" class="border" value={settings.step} onchange={onStepChange} min="1"/> 20 | </div> 21 | -------------------------------------------------------------------------------- /client/src/components/analytics/trackable/AnalyticsTrackableLineChart.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {categoryToCssRgb, getChartColors} from "@perfice/util/color"; 3 | import type {LineAnalyticsChartData} from "@perfice/stores/analytics/trackable"; 4 | import SingleChart from "@perfice/components/chart/SingleChart.svelte"; 5 | 6 | let {data, name}: { data: LineAnalyticsChartData, name: string } = $props(); 7 | 8 | let {fillColor, borderColor} = $derived(getChartColors(categoryToCssRgb(name))); 9 | </script> 10 | 11 | <SingleChart 12 | type="line" 13 | hideLabels={true} 14 | hideGrid={true} 15 | minimal={false} 16 | legend={false} 17 | dataPoints={data.values} 18 | blur={data.blur} 19 | labelFormatter={data.labelFormatter} 20 | {fillColor} 21 | {borderColor} 22 | labels={data.labels}/> 23 | -------------------------------------------------------------------------------- /client/src/components/form/editor/display/hierarchy/EditHierarchyQuestionDisplaySettings.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {FormQuestionDataType} from "@perfice/model/form/form"; 3 | import type {HierarchyFormDisplaySettings} from "@perfice/model/form/display/hierarchy"; 4 | 5 | let {settings, onChange}: { 6 | settings: HierarchyFormDisplaySettings, 7 | onChange: (settings: HierarchyFormDisplaySettings) => void, 8 | dataType: FormQuestionDataType, 9 | dataSettings: any 10 | } = $props(); 11 | 12 | function onOnlyLeafOptionChange(e: { currentTarget: HTMLInputElement }) { 13 | onChange({...settings, onlyLeafOption: e.currentTarget.checked}); 14 | } 15 | </script> 16 | 17 | <div class="row-between"> 18 | Show only last option 19 | <input type="checkbox" checked={settings.onlyLeafOption} onchange={onOnlyLeafOptionChange}/> 20 | </div> 21 | -------------------------------------------------------------------------------- /client/src/components/settings/SettingsDeleteData.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import Button from "@perfice/components/base/button/Button.svelte"; 3 | import {ButtonColor} from "@perfice/model/ui/button"; 4 | import {deletion} from "@perfice/stores"; 5 | import DeleteAccountModal from "@perfice/components/settings/DeleteAccountModal.svelte"; 6 | 7 | let modal: DeleteAccountModal; 8 | 9 | function startDelete() { 10 | modal.open(); 11 | } 12 | 13 | async function onDeleteData() { 14 | await deletion.deleteAllData(); 15 | window.location.reload(); 16 | } 17 | </script> 18 | 19 | <DeleteAccountModal bind:this={modal} onConfirm={onDeleteData}/> 20 | 21 | <h3 class="settings-label">Delete data</h3> 22 | <div class="row-gap mt-2"> 23 | <Button class="md:w-auto w-full" color={ButtonColor.RED} onClick={startDelete}>Delete local data</Button> 24 | </div> 25 | -------------------------------------------------------------------------------- /client/src/components/journal/search/common/OneOfFilterCard.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {OneOfFilter} from "@perfice/model/journal/search/search"; 3 | import GenericFilterContainer from "@perfice/components/journal/search/types/GenericFilterContainer.svelte"; 4 | import MultiSelectDropdownButton from "@perfice/components/base/dropdown/MultiSelectDropdownButton.svelte"; 5 | 6 | let {filter, onChange, onDelete, items}: { 7 | filter: OneOfFilter, 8 | onChange: (filter: OneOfFilter) => void, 9 | onDelete: () => void, 10 | items: { name: string, value: string }[] 11 | } = $props(); 12 | </script> 13 | 14 | <GenericFilterContainer name="One of" {onDelete}> 15 | <MultiSelectDropdownButton onChange={(v) => onChange({...filter, values: v})} noneText="None" value={filter.values} 16 | items={items}/> 17 | </GenericFilterContainer> 18 | -------------------------------------------------------------------------------- /client/src/components/dashboard/sidebar/add/AddWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import type {DashboardAddWidgetAction} from "@perfice/model/dashboard/ui"; 3 | import {getDashboardWidgetDefinitions} from "@perfice/model/dashboard/dashboard"; 4 | import DashboardDragInCard from "@perfice/components/dashboard/sidebar/add/DashboardDragInCard.svelte"; 5 | 6 | let {action}: { action: DashboardAddWidgetAction } = $props(); 7 | 8 | let definitions = getDashboardWidgetDefinitions(); 9 | let mobile = window.innerWidth < 768; 10 | </script> 11 | 12 | {#if mobile} 13 | <p class="text-sm mb-4">The new widget will be added to the bottom of the dashboard.</p> 14 | {/if} 15 | 16 | <div class="grid grid-cols-2 gap-2"> 17 | {#each definitions as definition} 18 | <DashboardDragInCard {definition} {mobile} onClick={() => action.onClick(definition.getType(), mobile)}/> 19 | {/each} 20 | </div> -------------------------------------------------------------------------------- /client/src/components/integration/modals/GlobalIntegrationModals.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts"> 2 | import {subscribeToEventStore} from "@perfice/util/event"; 3 | import IntegrationUnauthenticatedModal 4 | from "@perfice/components/integration/modals/IntegrationUnauthenticatedModal.svelte"; 5 | import {unauthenticatedIntegrationEvents} from "@perfice/stores/remote/integration"; 6 | import type {UnauthenticatedIntegrationError} from "@perfice/model/integration/ui"; 7 | 8 | let unauthenticatedIntegrationModal: IntegrationUnauthenticatedModal; 9 | 10 | $effect(() => { 11 | subscribeToEventStore($unauthenticatedIntegrationEvents, unauthenticatedIntegrationEvents, (errors: UnauthenticatedIntegrationError[]) => { 12 | unauthenticatedIntegrationModal.open(errors); 13 | }); 14 | }); 15 | </script> 16 | 17 | <IntegrationUnauthenticatedModal bind:this={unauthenticatedIntegrationModal}/> -------------------------------------------------------------------------------- /client/src/components/form/editor/sidebar/SidebarDropdownHeader.svelte: -------------------------------------------------------------------------------- 1 | <script lang="ts" generics="T"> 2 | import {type IconDefinition} from "@fortawesome/free-solid-svg-icons"; 3 | // noinspection ES6UnusedImports 4 | import Fa from "svelte-fa"; 5 | import DropdownButton from "@perfice/components/base/dropdown/DropdownButton.svelte"; 6 | import type {DropdownMenuItem} from "@perfice/model/ui/dropdown"; 7 | 8 | 9 | let {icon, title, value, items}: { 10 | icon: IconDefinition, 11 | title: string, 12 | value: T, 13 | items: DropdownMenuItem<T>[] 14 | } = $props(); 15 | </script> 16 | 17 | <div class="mt-4 row-between bg-green-600 px-4 py-2"> 18 | <div class="row-gap font-bold text-white"> 19 | <Fa icon={icon} class="w-4"/> 20 | <p>{title}</p> 21 | </div> 22 | <DropdownButton class="min-w-40 bg-white text-black" value={value} items={items}/> 23 | </div> 24 | -------------------------------------------------------------------------------- /server/util/map.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func GetFromMapOrNil[K comparable, R any](mapping map[K]R, key K) *R { 4 | if val, ok := mapping[key]; ok { 5 | return &val 6 | } 7 | 8 | return nil 9 | } 10 | 11 | func GetFromMapOrDefault[K comparable, R any](mapping map[K]R, key K, defaultValue R) *R { 12 | if val, ok := mapping[key]; ok { 13 | return &val 14 | } 15 | 16 | return &defaultValue 17 | } 18 | 19 | func SliceMap[T any, R any](slice []T, mapper func(val T) R) []R { 20 | mapped, _ := SliceMapErr[T, R](slice, func(val T) (R, error) { 21 | return mapper(val), nil 22 | }) 23 | 24 | return mapped 25 | } 26 | 27 | func SliceMapErr[T any, R any](slice []T, mapper func(val T) (R, error)) ([]R, error) { 28 | var result = make([]R, len(slice)) 29 | for i, v := range slice { 30 | val, err := mapper(v) 31 | if err != nil { 32 | return nil, err 33 | } 34 | result[i] = val 35 | } 36 | 37 | return result, nil 38 | } 39 | -------------------------------------------------------------------------------- /client/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <resources> 3 | 4 | <!-- Base application theme. --> 5 | <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> 6 | <!-- Customize your theme here. --> 7 | <item name="colorPrimary">@color/colorPrimary</item> 8 | <item name="colorPrimaryDark">@color/colorPrimaryDark</item> 9 | <item name="colorAccent">@color/colorAccent</item> 10 | </style> 11 | 12 | <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar"> 13 | <item name="windowActionBar">false</item> 14 | <item name="windowNoTitle">true</item> 15 | <item name="android:background">@null</item> 16 | </style> 17 | 18 | 19 | <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen"> 20 | <item name="android:background">@drawable/splash</item> 21 | </style> 22 | </resources> -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <link rel="icon" type="image/png" sizes="64x64" href="/favicon-64x64.png" /> 6 | <link rel="icon" type="image/png" sizes="128x128" href="/favicon-128x128.png" /> 7 | <link rel="icon" type="image/png" sizes="512x512" href="/favicon-512x512.png" /> 8 | <meta charset="utf-8" /> 9 | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> 10 | <meta name="description" content="Track everything and see how different choices affect your life. 11 | " /> 12 | <meta property="og:title" content="Perfice - Giving you the data to help you reach your life goals." /> 13 | <meta property="og:image" content="/favicon-128x128.png" /> 14 | <title>Perfice 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /client/src/components/analytics/details/CorrelationBar.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {#if !full && positive} 10 |
11 | {/if} 12 |
13 |
14 |
15 |
21 |
22 |
23 |
24 | 25 | {#if !full && negative} 26 |
27 | {/if} 28 |
29 | -------------------------------------------------------------------------------- /client/tests/primitive.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from "vitest"; 2 | import {comparePrimitives, pList, pString} from "../src/model/primitive/primitive"; 3 | 4 | test("string primitives equal", () => { 5 | expect(comparePrimitives(pString("test"), pString("test"))).toBeTruthy() 6 | }) 7 | 8 | test("string primitives inequal", () => { 9 | expect(comparePrimitives(pString("test"), pString("not the same"))).toBeFalsy() 10 | }) 11 | 12 | test("list primitives equal", () => { 13 | expect(comparePrimitives(pList([pString("test")]), pList([pString("test")]))).toBeTruthy() 14 | }) 15 | 16 | test("list primitives inequal", () => { 17 | expect(comparePrimitives(pList([pString("test")]), pList([pString("ok")]))).toBeFalsy() 18 | }) 19 | 20 | test("list primitives wrong order inequal", () => { 21 | expect(comparePrimitives(pList([pString("test"), pString("test2")]), 22 | pList([pString("test2"), pString("test")]))).toBeFalsy() 23 | }) 24 | -------------------------------------------------------------------------------- /server/integration/internal/service/auth_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "go.mongodb.org/mongo-driver/bson" 8 | "perfice.adoe.dev/integration/internal/auth" 9 | ) 10 | 11 | func TestDeserializeAuthenticationSettings_OAuth(t *testing.T) { 12 | svc := &IntegrationAuthenticationService{} 13 | 14 | settings := map[string]any{ 15 | "authorize_url": "https://example.com/auth", 16 | "token_url": "https://example.com/token", 17 | "scopes": bson.A{"read", "write"}, 18 | "client_id": "my-client-id", 19 | "client_secret": "my-secret", 20 | "pkce": true, 21 | } 22 | 23 | redirectUrl := "https://myapp.com/redirect" 24 | 25 | method := svc.deserializeAuthenticationSettings(settings, "oauth", redirectUrl) 26 | 27 | _, ok := method.(*auth.OAuthAuthenticationMethod) 28 | assert.True(t, ok, "method should be of type *OAuthAuthenticationMethod") 29 | } 30 | -------------------------------------------------------------------------------- /client/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.getcapacitor.myapp; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import android.content.Context; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | import androidx.test.platform.app.InstrumentationRegistry; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 23 | 24 | assertEquals("com.getcapacitor.app", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/components/base/button/Button.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /client/src/components/tag/TagValueCard.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | {#await $tagEntry then entryId} 22 | onClick(entryId)}/> 23 | {/await} 24 | 25 | -------------------------------------------------------------------------------- /client/src/model/journal/search/date.ts: -------------------------------------------------------------------------------- 1 | import {type SearchDefinition, type SearchDependencies, SearchEntityMode} from "@perfice/model/journal/search/search"; 2 | import type {JournalEntry, TagEntry} from "@perfice/model/journal/journal"; 3 | import {isTimestampInRange, type TimeRange} from "@perfice/model/variable/time/time"; 4 | 5 | export interface DateSearch { 6 | range: TimeRange; 7 | } 8 | 9 | export class DateSearchDefinition implements SearchDefinition { 10 | matchesJournalEntry(search: DateSearch, _dependencies: SearchDependencies, entry: JournalEntry): boolean { 11 | return isTimestampInRange(entry.timestamp, search.range); 12 | } 13 | 14 | matchesTagEntry(search: DateSearch, _dependencies: SearchDependencies, entry: TagEntry): boolean { 15 | return isTimestampInRange(entry.timestamp, search.range); 16 | } 17 | 18 | getDefaultSearchMode(): SearchEntityMode { 19 | return SearchEntityMode.MUST_MATCH; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/components/base/modal/generic/GenericEntityModal.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 30 | {message} 31 | 32 | -------------------------------------------------------------------------------- /client/src/components/goal/editor/sidebar/AddSourceSidebar.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | {#each COMPARISON_SOURCE_TYPES as type} 22 | onSelect(type.type)} 27 | /> 28 | {/each} 29 |
30 | -------------------------------------------------------------------------------- /client/src/components/tag/TagCard.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | {tag.name} 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/src/components/journal/search/common/ByCategoryFilterCard.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | onChange({...filter, categories: v})} noneText="None" 16 | value={filter.categories} 17 | items={items}/> 18 | 19 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/tag/filters/TagByCategoryFilterCard.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /client/src/db/dexie/search.ts: -------------------------------------------------------------------------------- 1 | import type {SavedSearchCollection} from "../collections"; 2 | import type {JournalSearch} from "@perfice/model/journal/search/search"; 3 | import type {SyncedTable} from "@perfice/services/sync/sync"; 4 | 5 | export class DexieSavedSearchCollection implements SavedSearchCollection { 6 | 7 | private readonly table: SyncedTable; 8 | 9 | constructor(table: SyncedTable) { 10 | this.table = table; 11 | } 12 | 13 | async getSavedSearches(): Promise { 14 | return this.table.getAll(); 15 | } 16 | 17 | async getSavedSearchById(id: string): Promise { 18 | return this.table.getById(id); 19 | } 20 | 21 | async putSavedSearch(search: JournalSearch): Promise { 22 | await this.table.put(search); 23 | } 24 | 25 | async deleteSavedSearchById(id: string): Promise { 26 | await this.table.deleteById(id); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /client/src/components/trackable/card/value/table/TableTrackableRenderer.svelte: -------------------------------------------------------------------------------- 1 | 13 |
14 | {#each values as value} 15 | 16 | {:else} 17 |
18 | No values 19 |
20 | {/each} 21 |
22 | -------------------------------------------------------------------------------- /server/auth/internal/kafka.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "os" 6 | ) 7 | import "github.com/segmentio/kafka-go" 8 | 9 | type KafkaService struct { 10 | conn *kafka.Writer 11 | } 12 | 13 | func (a *AuthApp) setupKafka() { 14 | w := &kafka.Writer{ 15 | Addr: kafka.TCP(os.Getenv("KAFKA_URL")), 16 | Topic: "my-topic", 17 | Balancer: &kafka.LeastBytes{}, 18 | } 19 | 20 | a.kafkaService = &KafkaService{w} 21 | } 22 | 23 | func (a *KafkaService) NotifyTimezoneChange(userId string, timezone string) error { 24 | return a.sendBasicMessage("timezoneChange", userId+":"+timezone) 25 | } 26 | 27 | func (a *KafkaService) NotifyUserDeleted(userId string) error { 28 | return a.sendBasicMessage("userDeleted", userId) 29 | } 30 | 31 | func (a *KafkaService) sendBasicMessage(topic string, value string) error { 32 | return a.conn.WriteMessages( 33 | context.Background(), 34 | kafka.Message{ 35 | Key: []byte(topic), 36 | Value: []byte(value), 37 | }, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /client/src/components/integration/IntegrationEntityCard.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /client/src/components/mobile/MobileTopBar.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | {#if leading != null} 16 | {@render leading()} 17 | {:else} 18 | 19 | {/if} 20 |
21 |
22 | {@render actions?.()} 23 |
24 |
25 | {title} 26 |
27 |
28 | -------------------------------------------------------------------------------- /client/src/views/feedback/FeedbackView.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

Send feedback

17 |

Found any bugs or want to see a new feature added? Let us know below!

18 | 19 |
20 | 22 |
23 | 24 | {#if !sent} 25 | 26 | {:else} 27 |

Thank you for your feedback! ❤️

28 | {/if} 29 |
-------------------------------------------------------------------------------- /client/src/stores/journal/tag.ts: -------------------------------------------------------------------------------- 1 | import type {TagEntry} from "@perfice/model/journal/journal"; 2 | import {AsyncStore} from "@perfice/stores/store"; 3 | import type {TagEntryService} from "@perfice/services/tag/entry"; 4 | import {emptyPromise} from "@perfice/util/promise"; 5 | 6 | export class TagEntryStore extends AsyncStore { 7 | 8 | private tagEntryService: TagEntryService; 9 | 10 | constructor(tagEntryService: TagEntryService) { 11 | super(emptyPromise()); 12 | this.tagEntryService = tagEntryService; 13 | } 14 | 15 | async init() { 16 | this.setResolved([]); 17 | } 18 | 19 | async nextPage(page: number, size: number, lastId: string): Promise { 20 | return await this.tagEntryService.getEntriesUntilTimeAndLimit(page, size, lastId); 21 | } 22 | 23 | async deleteEntryById(id: string) { 24 | await this.tagEntryService.deleteEntryById(id); 25 | this.updateResolved(v => v.filter(e => e.id != id)); 26 | } 27 | } -------------------------------------------------------------------------------- /client/src/components/analytics/details/tag/TagWeekDayAnalytics.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |

Week days

11 |

12 | Most tagged on {WEEK_DAYS_SHORT[analytics.max]}, least tagged on {WEEK_DAYS_SHORT[analytics.min]} 13 |

14 |
15 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /client/src/components/dashboard/sidebar/edit/types/insights/EditInsightsWidgetSidebar.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | Time scope 18 | onChange({...settings, timeScope: v})} 20 | items={SIMPLE_TIME_SCOPE_TYPES}/> 21 |
-------------------------------------------------------------------------------- /client/src/components/reflection/editor/sidebar/widget/ReflectionEditFormWidget.svelte: -------------------------------------------------------------------------------- 1 | 20 |
21 | Form 22 | 24 |
25 | -------------------------------------------------------------------------------- /client/src/components/onboarding/OnboardingImage.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | Onboarding 11 |
12 |
13 |
14 |

{page.title}

15 |

{page.description}

16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /client/src/components/trackable/card/habit/HabitTrackableRenderer.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | {#if goalResult != null} 20 | 21 | {:else} 22 | No goal set 23 | {/if} 24 | -------------------------------------------------------------------------------- /client/src/components/form/editor/display/select/EditSelectGrid.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |

Items per row

18 | 19 |
20 |
21 |

Border

22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /client/src/components/goal/multi/ConditionEntry.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /client/src/components/settings/ConfigureUrlModal.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | 25 |
26 |

Configure your own self-hosted URL or leave blank to use the default.

27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /client/src/db/dexie/migration.ts: -------------------------------------------------------------------------------- 1 | import type {DexieDB} from "@perfice/db/dexie/db"; 2 | import type {Table} from "dexie"; 3 | import type {Migration, Migrator} from "@perfice/db/migration/migration"; 4 | 5 | export class DexieMigrator implements Migrator { 6 | private readonly db: DexieDB; 7 | 8 | constructor(db: DexieDB) { 9 | this.db = db; 10 | } 11 | 12 | async applyMigration(migration: Migration): Promise { 13 | // @ts-ignore 14 | let collection: Table | undefined = this.db[migration.getEntityType()]; 15 | if (collection == undefined) { 16 | console.error("Missing collection for entity type", migration.getEntityType()); 17 | return; 18 | } 19 | 20 | let allEntities = await collection.toArray(); 21 | let result: object[] = []; 22 | for (let entity of allEntities) { 23 | result.push(await migration.apply(structuredClone(entity))); 24 | } 25 | 26 | await collection.bulkPut(result); 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /client/src/model/ui/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | faBook, 3 | faBullseye, 4 | faCog, 5 | faHome, 6 | faLineChart, 7 | faSquarePlus, 8 | faSun, 9 | faTags, 10 | type IconDefinition 11 | } from "@fortawesome/free-solid-svg-icons"; 12 | 13 | export interface SidebarLink { 14 | icon: IconDefinition, 15 | path: string, 16 | title: string, 17 | showOnMobile?: boolean 18 | bottom?: boolean 19 | } 20 | 21 | export const SIDEBAR_LINKS: SidebarLink[] = [ 22 | {icon: faHome, path: "/", title: "Home"}, 23 | {icon: faSquarePlus, path: "/trackables", title: "Track"}, 24 | {icon: faBook, path: "/journal", title: "Journal"}, 25 | {icon: faBullseye, path: "/goals", title: "Goals"}, 26 | {icon: faTags, path: "/tags", title: "Tags"}, 27 | {icon: faLineChart, path: "/analytics", title: "Analytics", showOnMobile: false}, 28 | {icon: faSun, path: "/reflections", title: "Reflections", showOnMobile: false}, 29 | {icon: faCog, path: "/settings", title: "Settings", bottom: true}, 30 | ]; 31 | -------------------------------------------------------------------------------- /client/src/components/goal/single/SingleConditionRenderer.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /server/sync/internal/key_service.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | type KeyVerificationService struct { 4 | keyVerificationCollection *KeyVerificationCollection 5 | } 6 | 7 | func NewKeyVerificationService(keyVerificationCollection *KeyVerificationCollection) *KeyVerificationService { 8 | return &KeyVerificationService{keyVerificationCollection} 9 | } 10 | 11 | func (s *KeyVerificationService) GetKeyByUser(user string) ([]byte, error) { 12 | verification, err := s.keyVerificationCollection.FindByUser(user) 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | if verification == nil { 18 | return nil, nil 19 | } 20 | 21 | return verification.Key, nil 22 | } 23 | 24 | func (s *KeyVerificationService) SetKey(user string, key []byte) error { 25 | verification := KeyVerification{ 26 | User: user, 27 | Key: key, 28 | } 29 | 30 | return s.keyVerificationCollection.Upsert(verification) 31 | } 32 | 33 | func (s *KeyVerificationService) OnUserDeleted(userId string) error { 34 | return s.keyVerificationCollection.DeleteByUser(userId) 35 | } 36 | -------------------------------------------------------------------------------- /client/src/components/base/timeScope/RangedTimeScopePicker.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 22 | 23 |
24 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/trackable/filters/TrackableByCategoryFilterCard.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/date/DateSearchOptions.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | {#if converted.type !== TimeRangeType.ALL && ranged.getStart() === ranged.getEnd()} 14 | 15 | Empty date range, to select a single date, set "To" as the next day. 16 | 17 | {/if} 18 | onChange({...options, range: v.convertToRange()})}/> 20 |
-------------------------------------------------------------------------------- /client/src/components/form/fields/range/RangeFormField.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/components/dashboard/types/welcome/DashboardWelcomeWidget.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

Good {getTimeOfDayText()}!

17 |

{fetchRandomQuote()}

18 |
19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /client/src/components/trackable/edit/general/habit/EditTrackableHabitCard.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /client/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # AndroidX package structure to make it clearer which packages are bundled with the 20 | # Android operating system, and which are packaged with your app's APK 21 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 22 | android.useAndroidX=true 23 | -------------------------------------------------------------------------------- /client/tests/simple-time.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from "vitest"; 2 | import {dateToWeekEnd, dateToWeekStart} from "../src/util/time/simple"; 3 | import {WeekStart} from "../src/model/variable/time/time"; 4 | 5 | test("date to week start sunday", () => { 6 | let date = new Date(2025, 0, 16); 7 | expect(dateToWeekStart(date, WeekStart.SUNDAY).getTime()) 8 | .toEqual(new Date(2025, 0, 12, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()).getTime()) 9 | }) 10 | 11 | test("date to week start monday", () => { 12 | let date = new Date(2025, 0, 16); 13 | expect(dateToWeekStart(date, WeekStart.MONDAY).getTime()) 14 | .toEqual(new Date(2025, 0, 13, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()).getTime()) 15 | }) 16 | 17 | test("date to week end monday", () => { 18 | let date = new Date(2025, 0, 16); 19 | expect(dateToWeekEnd(date, WeekStart.MONDAY).getTime()) 20 | .toEqual(new Date(2025, 0, 19, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()).getTime()) 21 | }) 22 | 23 | -------------------------------------------------------------------------------- /client/src/components/analytics/tag/AnalyticsTagView.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | {#await $res} 12 | Loading... 13 | {:then data} 14 | {#each data.results as value(value.tag.id)} 15 |
16 |

17 | 19 |

20 |
21 | 22 |
23 |
24 | {/each} 25 | {/await} 26 |
27 | -------------------------------------------------------------------------------- /server/proto/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 2 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 3 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 4 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 5 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 6 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 7 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= 8 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= 9 | google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= 10 | google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 11 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 12 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 13 | -------------------------------------------------------------------------------- /client/src/components/journal/search/types/tag/TagSearchActions.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /client/src/db/dexie/variable.ts: -------------------------------------------------------------------------------- 1 | import type {VariableCollection} from "@perfice/db/collections"; 2 | import type {StoredVariable} from "@perfice/model/variable/variable"; 3 | import type {SyncedTable} from "@perfice/services/sync/sync"; 4 | 5 | export class DexieVariableCollection implements VariableCollection { 6 | 7 | private table: SyncedTable; 8 | 9 | constructor(table: SyncedTable) { 10 | this.table = table; 11 | } 12 | 13 | getVariableById(id: string): Promise { 14 | return this.table.getById(id); 15 | } 16 | 17 | async getVariables(): Promise { 18 | return this.table.getAll(); 19 | } 20 | 21 | async createVariable(variable: StoredVariable): Promise { 22 | await this.table.create(variable); 23 | } 24 | 25 | async updateVariable(variable: StoredVariable): Promise { 26 | await this.table.put(variable); 27 | } 28 | 29 | async deleteVariableById(id: string): Promise { 30 | await this.table.deleteById(id); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /client/src/model/journal/journal.ts: -------------------------------------------------------------------------------- 1 | import type { PrimitiveValue } from "@perfice/model/primitive/primitive"; 2 | 3 | export interface JournalEntry { 4 | id: string; 5 | timestamp: number; 6 | 7 | formId: string; 8 | snapshotId: string; 9 | integration: string | null; 10 | 11 | displayValue: string; 12 | answers: Record; 13 | } 14 | 15 | export interface TagEntry { 16 | id: string; 17 | timestamp: number; 18 | tagId: string; 19 | } 20 | 21 | export enum JournalEntityType { 22 | FORM_ENTRY, 23 | TAG_ENTRY 24 | } 25 | 26 | export type JournalEntity = { 27 | type: JournalEntityType.FORM_ENTRY, 28 | entry: JournalEntry 29 | } | { 30 | type: JournalEntityType.TAG_ENTRY, 31 | entry: TagEntry 32 | } 33 | 34 | export function jeForm(entry: JournalEntry): JournalEntity { 35 | return { 36 | type: JournalEntityType.FORM_ENTRY, 37 | entry 38 | } 39 | } 40 | 41 | export function jeTag(entry: TagEntry): JournalEntity { 42 | return { 43 | type: JournalEntityType.TAG_ENTRY, 44 | entry 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/mongoutil/bson.go: -------------------------------------------------------------------------------- 1 | package mongoutil 2 | 3 | import "go.mongodb.org/mongo-driver/bson/primitive" 4 | 5 | func CleanBSON(v any) any { 6 | switch val := v.(type) { 7 | case primitive.D: 8 | return cleanDocument(val) 9 | case primitive.M: 10 | return cleanMap(val) 11 | case map[string]any: 12 | return cleanMap(val) 13 | case primitive.A: 14 | return cleanArray(val) 15 | case []any: 16 | return cleanArray(val) 17 | case primitive.ObjectID: 18 | return val.Hex() 19 | case primitive.DateTime: 20 | return val.Time() 21 | default: 22 | return val 23 | } 24 | } 25 | 26 | func cleanMap(m map[string]any) map[string]any { 27 | out := make(map[string]any) 28 | for k, v := range m { 29 | out[k] = CleanBSON(v) 30 | } 31 | return out 32 | } 33 | 34 | func cleanDocument(m primitive.D) map[string]any { 35 | out := make(map[string]any) 36 | for _, v := range m { 37 | out[v.Key] = CleanBSON(v.Value) 38 | } 39 | return out 40 | } 41 | 42 | func cleanArray(arr []any) []any { 43 | out := make([]any, len(arr)) 44 | for i, v := range arr { 45 | out[i] = CleanBSON(v) 46 | } 47 | return out 48 | } 49 | -------------------------------------------------------------------------------- /client/src/components/base/button/PopupIconButton.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /client/src/components/dashboard/types/goal/DashboardGoalWidget.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
20 | {#await $res} 21 | Please select a goal 22 | {:then value} 23 | 24 | 25 | {/await} 26 |
27 | -------------------------------------------------------------------------------- /client/src/db/dexie/reflection.ts: -------------------------------------------------------------------------------- 1 | import type {Reflection} from "@perfice/model/reflection/reflection"; 2 | import type {ReflectionCollection} from "@perfice/db/collections"; 3 | import type {SyncedTable} from "@perfice/services/sync/sync"; 4 | 5 | export class DexieReflectionCollection implements ReflectionCollection { 6 | 7 | private table: SyncedTable; 8 | 9 | constructor(table: SyncedTable) { 10 | this.table = table; 11 | } 12 | 13 | async getReflections(): Promise { 14 | return this.table.getAll(); 15 | } 16 | 17 | async getReflectionById(id: string): Promise { 18 | return this.table.getById(id); 19 | } 20 | 21 | async createReflection(reflection: Reflection): Promise { 22 | await this.table.create(reflection); 23 | } 24 | 25 | async updateReflection(reflection: Reflection): Promise { 26 | await this.table.put(reflection); 27 | } 28 | 29 | async deleteReflectionById(id: string): Promise { 30 | await this.table.deleteById(id); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /client/src/services/observer.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum EntityObserverType { 3 | CREATED, 4 | UPDATED, 5 | DELETED, 6 | ANY, 7 | } 8 | 9 | export type EntityObserverCallback = (e: T) => Promise; 10 | export interface EntityObserver { 11 | type: EntityObserverType; 12 | callback: EntityObserverCallback; 13 | } 14 | 15 | export class EntityObservers { 16 | private observers: EntityObserver[] = []; 17 | 18 | addObserver(type: EntityObserverType, callback: EntityObserverCallback) { 19 | this.observers.push({ type, callback }); 20 | } 21 | 22 | removeObserver(type: EntityObserverType, callback: EntityObserverCallback) { 23 | this.observers = this.observers.filter(o => o.type != type || o.callback != callback); 24 | } 25 | 26 | async notifyObservers(type: EntityObserverType, entity: T) { 27 | let observers = this.observers 28 | .filter(o => o.type == EntityObserverType.ANY || o.type == type); 29 | 30 | for (const observer of observers) { 31 | await observer.callback(entity); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/sync/internal/salt_service.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "crypto/rand" 4 | 5 | type SaltService struct { 6 | saltCollection *SaltCollection 7 | } 8 | 9 | func NewSaltService(saltCollection *SaltCollection) *SaltService { 10 | return &SaltService{ 11 | saltCollection: saltCollection, 12 | } 13 | } 14 | 15 | func (s *SaltService) generateSalt(user string) ([]byte, error) { 16 | bytes := make([]byte, 32) 17 | _, err := rand.Read(bytes) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | salt := Salt{ 23 | User: user, 24 | Salt: bytes, 25 | } 26 | 27 | err = s.saltCollection.Insert(salt) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return salt.Salt, nil 33 | } 34 | 35 | func (s *SaltService) GetSalt(user string) ([]byte, error) { 36 | salt, err := s.saltCollection.FindByUser(user) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if salt == nil { 42 | return s.generateSalt(user) 43 | } 44 | 45 | return salt.Salt, nil 46 | } 47 | 48 | func (s *SaltService) OnUserDeleted(userId string) error { 49 | return s.saltCollection.DeleteByUser(userId) 50 | } 51 | -------------------------------------------------------------------------------- /client/android/capacitor.settings.gradle: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN 2 | include ':capacitor-android' 3 | project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') 4 | 5 | include ':capacitor-app' 6 | project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') 7 | 8 | include ':capacitor-browser' 9 | project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android') 10 | 11 | include ':capacitor-filesystem' 12 | project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') 13 | 14 | include ':capacitor-local-notifications' 15 | project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android') 16 | 17 | include ':capacitor-share' 18 | project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') 19 | 20 | include ':capacitor-secure-storage' 21 | project(':capacitor-secure-storage').projectDir = new File('../node_modules/capacitor-secure-storage/android') 22 | -------------------------------------------------------------------------------- /client/src/components/form/editor/data/hierarchy/EditHierarchyQuestionSettings.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | {}}/> 24 | 25 | -------------------------------------------------------------------------------- /client/src/components/integration/modals/IntegrationUnauthenticatedModal.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 17 |
18 |
19 | {#each errors as error} 20 |

Integration {error.integrationTypeName} is not authenticated, please authenticate or remove it. 21 | 22 |
23 | Connected forms: {error.forms.join(", ")}

24 | {/each} 25 |
26 |
27 |
-------------------------------------------------------------------------------- /client/src/components/base/card/TitledCard.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 |
19 | 20 | {#if icon != null} 21 | 22 | {/if} 23 | 24 |
25 |

{title}

26 |

{description}

27 |
28 |
29 |
30 | {@render suffix?.()} 31 |
32 |
33 | -------------------------------------------------------------------------------- /client/src/components/variable/edit/EditVariableName.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 | 27 |
28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Polloc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/stores/dashboard/widget/goal.ts: -------------------------------------------------------------------------------- 1 | import {WeekStart} from "@perfice/model/variable/time/time"; 2 | import {derived, type Readable} from "svelte/store"; 3 | import type {DashboardGoalWidgetSettings} from "@perfice/model/dashboard/widgets/goal"; 4 | import type {GoalValueResult} from "@perfice/stores/goal/value"; 5 | import type {Goal} from "@perfice/model/goal/goal"; 6 | import {goals, goalValue} from "@perfice/stores"; 7 | 8 | export interface GoalWidgetResult { 9 | goal: Goal; 10 | value: GoalValueResult; 11 | } 12 | 13 | export function GoalWidget(settings: DashboardGoalWidgetSettings, date: Date, 14 | weekStart: WeekStart, key: string): Readable> { 15 | return derived(goalValue(settings.goalVariableId, settings.goalStreakVariableId, date, weekStart, key), (val, set) => { 16 | set(new Promise(async (resolve) => { 17 | let goal = await goals.getGoalByVariableId(settings.goalVariableId, true); 18 | if (goal == null) return; 19 | 20 | let value = await val; 21 | resolve({goal, value}); 22 | })); 23 | }); 24 | } -------------------------------------------------------------------------------- /client/src/components/base/weekDays/WeekDays.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | {#each Array(7) as _, i} 24 |
25 | onWeekDayChange(i)}/> 27 | {WEEK_DAYS_SHORT[i][0]} 28 |
29 | {/each} 30 |
-------------------------------------------------------------------------------- /client/src/components/journal/day/JournalTagEntries.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if tagEntries.length > 0} 17 |
18 | {#each tagEntries as entry (entry.id)} 19 | e.entry.id === entry.id)} 20 | onClick={() => onClick(entry)}> 21 | {entry.tag.name} 22 | 23 | 24 | {/each} 25 |
26 | {/if} 27 | -------------------------------------------------------------------------------- /client/src/components/trackable/edit/general/EditTrackableCategory.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /client/src/components/form/fields/select/SelectOptionButton.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | 33 | -------------------------------------------------------------------------------- /client/src/components/settings/DeleteAccountModal.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 30 |

Are you sure you want to delete all your data? This action is irreversible.

31 |

Type "{CONFIRMATION}" below to confirm.

32 | 33 |
34 | -------------------------------------------------------------------------------- /client/src/components/onboarding/OnboardingRegister.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |

Disclaimer

15 |

Perfice is free to use unlimited locally and can also be self-hosted.
To 18 | sync data 19 | across 20 | devices or access third-party 21 | integrations, you'll need to create an account.

22 | 23 |

You can create an account under the Settings icon after setup.

24 |
-------------------------------------------------------------------------------- /client/src/components/sharedWidgets/checklist/EditTagChecklistCondition.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | 28 | 29 |
30 | -------------------------------------------------------------------------------- /client/src/components/trackable/card/value/table/TrackableTableEntry.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 26 | {formatAnswersIntoRepresentation(entry.value, representation)} 27 | 28 | 29 | {formatTimestampHHMM(entry.timestamp)} 30 | 31 |
32 | -------------------------------------------------------------------------------- /client/src/model/sharedWidgets/table/table.ts: -------------------------------------------------------------------------------- 1 | import {type TextOrDynamic, type VariableTypeDef, VariableTypeName} from "@perfice/model/variable/variable"; 2 | import {ListVariableType} from "@perfice/services/variable/types/list"; 3 | import type {SimpleTimeScopeType} from "@perfice/model/variable/time/time"; 4 | 5 | export interface TableWidgetSettings { 6 | formId: string; 7 | prefix: TextOrDynamic[]; 8 | suffix: TextOrDynamic[]; 9 | timeScope: SimpleTimeScopeType; 10 | // Question id to optionally group by 11 | groupBy: string | null; 12 | } 13 | 14 | export function createTypeDefForTableWidget(settings: TableWidgetSettings): VariableTypeDef { 15 | let fields: Set = new Set(); 16 | [...settings.prefix, ...settings.suffix] 17 | .filter(v => v.dynamic) 18 | .forEach(v => fields.add(v.value)); 19 | 20 | if (settings.groupBy != null) { 21 | fields.add(settings.groupBy); 22 | } 23 | 24 | return { 25 | type: VariableTypeName.LIST, 26 | value: new ListVariableType(settings.formId, 27 | Object.fromEntries(fields.entries().map(([key]) => [key, true])), []) 28 | }; 29 | } 30 | --------------------------------------------------------------------------------