Files
wakapi/views/summary.tpl.html
2025-10-27 14:41:41 +01:00

419 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="en">
{{ template "head.tpl.html" . }}
<script src="assets/js/components/time-picker.js?v={{ getCacheBuster }}"></script>
<script src="assets/js/components/entity-filter.js?v={{ getCacheBuster }}"></script>
<script type="module" src="assets/js/components/summary.js?v={{ getCacheBuster }}"></script>
<body class="relative bg-background text-foreground p-4 pt-10 flex flex-col min-h-screen max-w-screen-xl mx-auto">
{{ template "menu-main.tpl.html" . }}
{{ template "alerts.tpl.html" . }}
{{ if and .Summary .SharedLoggedInViewModel.User.HasData }}
{{ template "time-picker.tpl.html" . }}
{{ template "entity-filter.tpl.html" . }}
{{ end }}
{{ if .Summary }}
<div id="summary-page" class="grow" v-scope @vue:mounted="mounted({ userId: '{{ .SharedLoggedInViewModel.User.ID }}'})">
<div class="flex justify-end md:space-x-8 mt-12 flex-wrap md:flex-nowrap relative items-center no-print">
{{ if $.UserDataExpiring }}
<div class="flex-grow justify-start">
<div class="flex-grow p-4 text-sm border-2 border-orange-500 rounded shadow text-foreground align-middle mb-4 md:mb-0">
<span class="iconify inline mr-1" data-icon="emojione-v1:warning"></span>
Some of&nbsp;&nbsp;your data is older than this instance's data retention period. This will cause old data to be deleted, unless you opt for a subscription. Go to <a class="font-semibold text-accent" href="settings#subscription">Settings → Subscription</a> for more details.
</div>
</div>
{{ end }}
<div class="flex-grow flex-shrink hidden md:flex justify-start gap-x-4 flex-wrap">
<div v-scope="EntityFilter({
type: 'project',
options: wakapiData.projects.map(p => p.key).toSorted(),
selection: null,
})" @vue:mounted="mounted"></div>
<div v-scope="EntityFilter({
type: 'language',
options: wakapiData.languages.map(p => p.key).toSorted(),
selection: null,
})" @vue:mounted="mounted"></div>
<div v-scope="EntityFilter({
type: 'machine',
options: wakapiData.machines.map(p => p.key).toSorted(),
selection: null,
})" @vue:mounted="mounted"></div>
<div v-scope="EntityFilter({
type: 'label',
options: wakapiData.labels.map(p => p.key).toSorted(),
selection: null,
})" @vue:mounted="mounted"></div>
<div v-scope="EntityFilter({
type: 'category',
options: wakapiData.categories.map(p => p.key).toSorted(),
selection: null,
})" @vue:mounted="mounted"></div>
</div>
<div class="flex-shrink-1 sm:flex-shrink-0" v-scope="TimePicker({
fromDate: '{{ .From | simpledate }}',
toDate: '{{ .To | ceildate | simpledate }}',
timeSelection: '{{ .From | date }} - {{ .To | ceildate | date }}'
})" @vue:mounted="mounted"></div>
</div>
<main class="flex flex-col items-center mt-4 md:mt-16 grow">
{{ if .SharedLoggedInViewModel.User.HasData }}
{{ if not .IsProjectDetails }}
<!-- KPIs -->
<div class="w-full mb-4 grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2 no-break">
<div class="flex flex-col w-full p-4 pt-2 rounded-md text-foreground bg-card leading-none border-2 border-accent">
<span class="text-xs text-muted font-semibold">Total Time</span>
<span class="font-semibold text-xl truncate" title="{{ .TotalTime | duration }}">{{ .TotalTime | duration }}</span>
<span class="text-xs text-muted" title="(your oldest heartbeat in selected range)" style="margin-bottom: -8px">after {{ .FromTime.T | datetime }}</span>
</div>
<div class="flex flex-col w-full p-4 pt-2 rounded-md text-foreground bg-card leading-none border-2 border-accent">
<span class="text-xs text-muted font-semibold">Total Heartbeats</span>
<span class="font-semibold text-xl truncate" title="{{ .NumHeartbeats }}">{{ .NumHeartbeats }}</span>
</div>
<div class="flex flex-col w-full p-4 pt-2 rounded-md text-foreground bg-card leading-none border-2 border-accent">
<span class="text-xs text-muted font-semibold">Top Project</span>
<span class="font-semibold text-xl truncate" title="{{ .MaxByToString 0 }}">{{ .MaxByToString 0 }}</span>
</div>
<div class="flex flex-col w-full p-4 pt-2 rounded-md text-foreground bg-card leading-none border-2 border-accent">
<span class="text-xs text-muted font-semibold">Top Language</span>
<span class="font-semibold text-xl truncate" title="{{ .MaxByToString 1 }}">{{ .MaxByToString 1 }}</span>
</div>
<div class="flex flex-col w-full p-4 pt-2 rounded-md text-foreground bg-card leading-none border-2 border-accent">
<span class="text-xs text-muted font-semibold">Top OS</span>
<span class="font-semibold text-xl truncate" title="{{ .MaxByToString 3 }}">{{ .MaxByToString 3 }}</span>
</div>
<div class="flex flex-col w-full p-4 pt-2 rounded-md text-foreground bg-card leading-none border-2 border-accent">
<span class="text-xs text-muted font-semibold">Top Editor</span>
<span class="font-semibold text-xl truncate" title="{{ .MaxByToString 2 }}">{{ .MaxByToString 2 }}</span>
</div>
</div>
{{ else }}
<div class="mb-8 w-full">
<h1 class="font-semibold text-3xl text-white">
{{ if eq .GetProjectFilter "-" }}
Unknown project
{{ else }}
Project "{{ .GetProjectFilter }}"
{{ end }}
</h1>
<div class="flex space-x-4 items-center">
<h4 class="font-semibold text-lg text-muted">{{ .TotalTime | duration }}</h4>
<div v-cloak v-show="currentInterval">
<img :src="'api/badge/{{ .SharedLoggedInViewModel.User.ID }}/interval:' + currentInterval + '/project:{{ .GetProjectFilter }}'" alt="Coding Time Badge">
</div>
</div>
</div>
{{ end }}
<div class="grid gap-2 grid-cols-1 md:grid-cols-3 w-full mt-4">
<!-- Projects -->
<div class="row-span-1 col-span-1 md:col-span-2 md:row-span-2 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break {{ if .IsProjectDetails }} hidden {{ end }}" id="project-container" style="max-height: 608px;">
<div class="flex justify-between">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Projects</span>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="project-top-picker" data-entity="0" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="0"></span></span>
</div>
</div>
<canvas id="chart-projects" class="mt-2"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
<!-- Branches -->
<div class="row-span-1 col-span-1 md:col-span-2 md:row-span-2 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break {{ if not .IsProjectDetails }} hidden {{ end }}" id="branch-container" style="max-height: 608px;">
<div class="flex justify-between">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Branches</span>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="branch-top-picker" data-entity="6" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="6"></span></span>
</div>
</div>
<canvas id="chart-branches" class="mt-2"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
<!-- Languages -->
<div class="row-span-1 col-span-1 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break" id="language-container" style="max-height: 300px">
<div class="flex justify-between">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Languages</span>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="language-top-picker" data-entity="3" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="3"></span></span>
</div>
</div>
<canvas id="chart-language" class="mt-4"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
<!-- Editors -->
<div class="row-span-1 col-span-1 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break" id="editor-container" style="max-height: 300px">
<div class="flex justify-between">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Editors</span>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="editor-top-picker" data-entity="2" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="2"></span></span>
</div>
</div>
<canvas id="chart-editor" class="mt-4"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
<!-- OS -->
<div class="{{ if .IsProjectDetails }} hidden {{ end }} md:col-span-2 md:row-span-2 w-full no-break">
<div class="p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full" id="os-container" style="max-height: 300px">
<div class="flex justify-between">
<div>
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap mr-1 cursor-pointer">Operating Systems</span>
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap ml-1 cursor-pointer text-muted" onclick="swapCharts('machine', 'os')">Machines</span>
</div>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="os-top-picker" data-entity="1" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="1"></span></span>
</div>
</div>
<canvas id="chart-os" class="mt-4"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
</div>
<!-- Machines -->
<div class="hidden md:col-span-2 md:row-span-2 w-full no-break">
<div class="p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full" id="machine-container" style="max-height: 300px">
<div class="flex justify-between">
<div>
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap mr-1 cursor-pointer text-muted" onclick="swapCharts('os', 'machine')">Operating Systems</span>
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap ml-1 cursor-pointer">Machines</span>
</div>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="machine-top-picker" data-entity="4" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="4"></span></span>
</div>
</div>
<canvas id="chart-machine" class="mt-4"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
</div>
<!-- Labels -->
<div class="{{ if .IsProjectDetails }} hidden {{ end }} row-span-1 col-span-1 w-full no-break">
<div class="p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full {{ if .IsProjectDetails }} hidden {{ end }}" id="label-container" style="max-height: 300px">
<div class="flex justify-between text-lg" style="margin-bottom: -10px">
<span class="font-semibold whitespace-nowrap">Labels</span>
<a href="settings#data" class="ml-4 inline p-2 hover:bg-focused rounded" style="margin-top: -5px">
<span class="iconify inline" data-icon="twemoji:gear"></span>
</a>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="label-top-picker" data-entity="5" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="5"></span></span>
</div>
</div>
<canvas id="chart-label" class="mt-4"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
</div>
<!-- Files -->
<div class="row-span-1 col-span-1 md:col-span-3 md:row-span-3 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break {{ if not .IsProjectDetails }} hidden {{ end }}" id="entity-container" style="max-height: 500px">
<div class="flex justify-between">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Files</span>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="entity-top-picker" data-entity="7" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="7"></span></span>
</div>
</div>
<canvas id="chart-entities" class="mt-4"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
<!-- Categories -->
<div class="row-span-1 col-span-1 md:col-span-3 md:row-span-3 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break {{ if .IsProjectDetails }} hidden {{ end }}" id="category-container" style="max-height: 224px; margin-top: -8px;">
<div class="flex justify-between">
<div class="flex items-center gap-x-2">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Categories</span>
<span
class="iconify inline text-2xl text-secondary p-1 cursor-help" data-icon="octicon:info-16"
title="After the category chart was introduced as a new feature in March '24, you will have to run &quot;Settings → Danger Zone → Regenerate Summaries&quot; once to have your categories reflected properly. This may take a few minutes."
></span>
</div>
<div class="flex justify-end flex-1 text-xs items-center">
<span class="mr-1">Top </span>
<input type="number" min="1" id="category-top-picker" data-entity="8" class="top-picker bg-focused rounded-md text-center w-12" value="10">
<span class="ml-1">of&nbsp;&nbsp;<span class="num-total-items" data-entity="8"></span></span>
</div>
</div>
<canvas id="chart-categories" class="mt-2"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
<!-- Timeline -->
<div class="row-span-1 col-span-1 md:col-span-3 md:row-span-3 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break" id="timeline-container" style="max-height: 224px;">
<div class="flex justify-between">
<div class="flex items-center gap-x-2">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">Timeline</span>
</div>
</div>
<canvas id="chart-timeline" class="mt-2"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
{{ if lt .SummaryParams.RangeDays 3 }}
<span class="text-md font-semibold text-muted mt-4">Only available for intervals >= 3 days</span>
{{ else if gt .SummaryParams.RangeDays 31 }}
<span class="text-md font-semibold text-muted mt-4">Only available for intervals <= 30 days</span>
{{ else }}
<span class="text-md font-semibold text-muted mt-4">No data</span>
{{ end }}
</div>
</div>
<!-- Hourly Breakdown -->
<div class="row-span-1 col-span-1 md:col-span-3 md:row-span-3 p-4 px-6 pb-10 bg-card text-foreground rounded-md shadow flex flex-col w-full no-break cursor-move" id="hourly-container" style="max-height: 224px;">
<div class="flex justify-between">
<div class="flex items-center gap-x-2">
<span class="font-semibold text-lg w-1/2 flex-1 whitespace-nowrap">
Hourly Breakdown
</span>
<span class="iconify inline text-2xl text-secondary p-1 cursor-help" data-icon="octicon:info-16"
title="Only the last 24 hours within the selected interval are shown in this chart. Use Ctrl + wheel to zoom, hold and click to drag."></span>
</div>
</div>
<canvas id="chart-hourly" class="mt-2"></canvas>
<div class="hidden placeholder-container flex items-center justify-center h-full flex-col">
<span class="text-md font-semibold text-muted mt-4">No data</span>
</div>
</div>
</div>
<div class="mt-12 flex flex-col space-y-2 text-foreground w-full no-break">
<div class="flex justify-start space-x-2 items-center">
<h2 class="text-lg font-semibold">Activity</h2>
<a v-cloak v-show="activityChartSvg" href="/api/activity/chart/{{ .SharedLoggedInViewModel.User.ID }}.svg" target="_blank" rel="noreferrer noopener" class="p-1 rounded hover:bg-card" title="Share...">
<span class="iconify inline text-xl text-muted p-px" data-icon="octicon:share-16"></span>
</a>
</div>
<span v-show="!activityChartSvg" class="text-md font-semibold text-muted mt-4">Loading activity chart ...</span>
<div v-html="activityChartSvg" class="w-full overflow-x-auto"></div>
</div>
{{ else }}
<div class="max-w-screen-sm flex flex-col items-center mt-12 space-y-8 text-foreground">
<div class="pb-4">
<img src="assets/images/welcome.svg" width="200px" alt="User welcome illustration">
</div>
<h1 class="font-semibold text-3xl text-white m-0 w-full">Welcome to Wakapi!</h1>
<p>
It looks like there is no data available for the specified time range. If you logged in to Wakapi for the first time, see the setup instructions below on how to get started.<br>
Check out <a href="settings#integrations" rel="noreferrer noopener" class="text-secondary hover:text-foreground"><i>Settings &rarr; Integrations</i></a> for instructions on how to use Wakapi and WakaTime in parallel.
</p>
{{ if gt .DataRetentionMonths 0 }}
<p>
By default, Wakapi will keep your coding activity for {{ .DataRetentionMonths }} months. If you want <strong>unlimited data storage</strong> or simply <strong>support the project</strong> (which the authors would very much appreciate 😊), you can opt for a paid subscription for <strong>only 5 € / month</strong>. Go to <a href="settings#subscription" class="text-secondary hover:text-foreground" rel="noreferrer noopener">Settings → Subscription</a> to purchase one.
</p>
{{ end }}
<div class="w-full pt-10 flex flex-col space-y-4">
<h1 class="font-semibold text-3xl text-white m-0 mb-2">Setup Instructions</h1>
<div class="w-full bg-card text-left rounded-md py-4 px-8 text-xs font-mono shadow-md">
# <strong>Step 1:</strong> Download WakaTime plugin for your IDE<br>
# See: https://wakatime.com/plugins<br><br>
# <strong>Step 2:</strong> Set your ~/.wakatime.cfg to this:<br><br>
<!-- https://github.com/muety/wakapi/issues/224#issuecomment-890855563 -->
[settings]<br>
api_url = <span class="with-url-inner">%s/api</span><br>
api_key = <span id="api-key-instruction">{{ .ApiKey }}</span><br><br>
# <strong>Step 3:</strong> Start coding and then check back here!
</div>
</div>
</div>
{{ end }}
</main>
</div>
{{ end }}
{{ template "footer.tpl.html" . }}
{{ template "foot.tpl.html" . }}
{{ if .Summary }}
<script>
const editorColors = {{ .EditorColors | json }}
const languageColors = {{ .LanguageColors | json }}
const osColors = {{ .OSColors | json }}
const wakapiData = {}
wakapiData.projects = {{ .Projects | json }}
wakapiData.operatingSystems = {{ .OperatingSystems | json }}
wakapiData.editors = {{ .Editors | json }}
wakapiData.languages = {{ .Languages | json }}
wakapiData.machines = {{ .Machines | json }}
wakapiData.labels = {{ .Labels | json }}
wakapiData.categories = {{ .Categories | json }}
wakapiData.timelineStats = {{ .Timeline | json }}
wakapiData.hourlyBreakdown = {{ .HourlyBreakdown | json }}
wakapiData.hourlyBreakdownFromTime = {{ .HourlyBreakdownFrom | json }}
wakapiData.hourlyBreakdownToTime = {{ .SummaryParams.To | json }}
{{ if .IsProjectDetails }}
wakapiData.branches = {{ .Branches | json }}
wakapiData.entities = {{ .Entities | json }}
{{ else }}
wakapiData.branches = []
wakapiData.entities = []
{{ end }}
</script>
<script src="assets/vendor/hammer.min.js"></script>
<script src="assets/vendor/chartjs-plugin-zoom.min.js"></script>
<script src="assets/js/summary.js?v={{ getCacheBuster }}"></script>
<script src="assets/js/summaryColors.js?v={{ getCacheBuster }}"></script>
{{ end }}
</body>
</html>