public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: Lukas Wagner <l.wagner@proxmox.com>,
	pbs-devel@lists.proxmox.com, pve-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-widget-toolkit v7 22/41] notifications: matcher: add better calendar editor
Date: Tue, 3 Nov 2026 14:56:11 +0100	[thread overview]
Message-ID: <8f8b7a19-02f9-4505-bad2-271631301381@proxmox.com> (raw)
In-Reply-To: <20261103104859.172466-23-l.wagner@proxmox.com>

a few high level ux things i noticed (some are pre-existing)

same start/end time is valid (mon 12-12), not sure if that makes sense
ranges are only ever from mon to sun
e.g. checking sun,mon,tue could be sun..tue but instead it's
mon,tue,sun (no biggie)

one issue though: the old input accepted times like: '0:1' e.g.
`mon 0:1-12:2`

this will not be correctly parsed and hold up the window
though it is quetsionable if that should be accepted in the
first place, as it's a big ambiguous what is meant

e.g. 12:1 -> 12:01 or 12:10?


code looks good to me

On 11/3/26 11:56 AM, Lukas Wagner wrote:
> This new editor allows one to enter the start time, end time and tick
> the matched week-days, instead of having to enter the appropriate string
> representation of the time range (e.g. 'mon..tue 08:00-12:00')
> 
> The general approach was copied from PBS's traffic rule edit panel, but
> it is too different to generalize this into a new, reusable component.
> 
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> Reviewed-by: Michael Köppl <m.koeppl@proxmox.com>
> Tested-by: Michael Köppl <m.koeppl@proxmox.com>
> ---
> 
> Notes:
>      Changes since v1:
>        - don't allow users to type anything but digits and : into the
>          timefields
>        - Show error message/icon in new match expression edit panel if
>          - start time is after end time
>          - no day is selected
>      
>      Changes since v2:
>        - In the time range selectors, set the altFormats property.
>          This allows users to enter time in varying formats and the
>          field will automatically normalize the format:
>            0:00 -> 00:00
>            730 -> 07:30
>            5  -> 05:00
>      
>        - Accept and normalize existing valid non-standard time durations,
>          e.g.
>      
>            - Mon..Fri 12:00-14:00
>            - mon..fri 12:00 - 14:00
>      
>      Change since v3:
>        - Even broader compatibility for other time range formats accepted
>          by proxmox-time, support both 'mon' as well as 'monday'
>      
>        - Code formatting with proxmox-biome
>      
>      Changes since v4:
>        - Properly parse 'mon8-12' (missing space between day and time)
>      
>      Changes since v5:
>        - Improve regex once again, fixing a regressing that
>          was introduced in v5 (e.g. 00:00-23:00 did not parse
>          properly anymore)
>        - Aligned time range validation with the backend.
>          The backend accepts start time and end time being the same,
>          while the UI did not. Changed the UI to allow the same
>          as the backend
> 
>   .../NotificationMatchExpressionEditPanel.js   |  27 +-
>   src/window/NotificationMatcherEdit.js         | 350 +++++++++++++++++-
>   2 files changed, 363 insertions(+), 14 deletions(-)
> 
> diff --git a/src/panel/NotificationMatchExpressionEditPanel.js b/src/panel/NotificationMatchExpressionEditPanel.js
> index ec9fd3c..cff5854 100644
> --- a/src/panel/NotificationMatchExpressionEditPanel.js
> +++ b/src/panel/NotificationMatchExpressionEditPanel.js
> @@ -87,7 +87,7 @@ Ext.define('Proxmox.panel.NotificationMatchExpressionEditPanel', {
>                               break;
>                           case 'match-calendar':
>                               data = {
> -                                value: '',
> +                                value: '00:00-23:59',
>                               };
>                               leaf = true;
>                               break;
> @@ -169,10 +169,19 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTreeExpression', {
>       mixins: ['Proxmox.Mixin.CBind'],
>       border: false,
>   
> +    // Converts a 'HH:MM' time into minutes since midnight, or NaN if it cannot
> +    // be parsed.
> +    parseTime: function (time) {
> +        let [hours, minutes] = time.split(':');
> +        return Number(hours) * 60 + Number(minutes ?? '0');
> +    },
> +
>       // Returns a message describing why a node is incomplete, or undefined if
>       // the node is fully configured. Incomplete nodes are marked in the tree and
>       // block submitting the form.
>       getNodeError: function (type, data, hasChildren) {
> +        let me = this;
> +
>           switch (type) {
>               case 'match-severity': {
>                   let v = Ext.isArray(data.value) ? data.value.join(', ') : data.value;
> @@ -192,7 +201,16 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTreeExpression', {
>               case 'match-calendar': {
>                   let v = data.value;
>                   if (!v || !v.length) {
> -                    return gettext('Calendar rules must have a schedule.');
> +                    return gettext('Calendar rules must match at least one day.');
> +                }
> +
> +                // Only schedules which the editor itself can produce are
> +                // checked, any other one is left to the backend. Unparsable
> +                // times compare as NaN and are thus not reported either.
> +                let [, start, end] = /^(?:\S*\s+)?([0-9:]+)-([0-9:]+)$/.exec(v) ?? [];
> +
> +                if (start !== undefined && (me.parseTime(start) > me.parseTime(end))) {
> +                    return gettext("A calendar rule's start time cannot be after the end time.");
>                   }
>                   break;
>               }
> @@ -330,7 +348,10 @@ Ext.define('Proxmox.panel.NotificationMatchRuleTreeExpression', {
>                   }
>               }
>               case 'match-calendar': {
> -                let value = data.value ? data.value : '?';
> +                // A schedule which is not fully specified, for instance because
> +                // no weekday is selected, renders as blank, so show it as unset
> +                // instead.
> +                let value = Ext.isString(data.value) && data.value.trim() ? data.value : '?';
>                   value = `<code>${Ext.String.htmlEncode(value)}</code>`;
>   
>                   if (invert) {
> diff --git a/src/window/NotificationMatcherEdit.js b/src/window/NotificationMatcherEdit.js
> index 893c3e3..4e1aa59 100644
> --- a/src/window/NotificationMatcherEdit.js
> +++ b/src/window/NotificationMatcherEdit.js
> @@ -361,7 +361,7 @@ Ext.define('Proxmox.panel.NotificationRulesEditPanel', {
>                               break;
>                           case 'match-calendar':
>                               data = {
> -                                value: '',
> +                                value: '00:00-23:59',
>                               };
>                               break;
>                       }
> @@ -977,6 +977,11 @@ Ext.define('Proxmox.panel.MatchCalendarSettings', {
>                   },
>                   set: function (value) {
>                       let me = this;
> +
> +                    if (!me.get('typeIsMatchCalendar')) {
> +                        return;
> +                    }
> +
>                       let record = me.get('selectedRecord');
>                       let currentData = record.get('data');
>                       record.set({
> @@ -992,23 +997,346 @@ Ext.define('Proxmox.panel.MatchCalendarSettings', {
>               },
>           },
>       },
> +    controller: {
> +        xclass: 'Ext.app.ViewController',
> +        control: {
> +            'grid checkbox': {
> +                change: 'dowChanged',
> +            },
> +            timefield: {
> +                change: 'timeChanged',
> +            },
> +            'field[reference=timeframe]': {
> +                change: 'setGridData',
> +            },
> +        },
> +
> +        weekdays: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
> +
> +        setGridData: function (field, value) {
> +            let me = this;
> +
> +            let record = me.parseTimeframe(value);
> +
> +            me.lookup('weekdayGrid').getStore().setData([record]);
> +            me.lookup('timeStart').setValue(record.start);
> +            me.lookup('timeEnd').setValue(record.end);
> +        },
> +
> +        parseTimeframe: function (timeframe) {
> +            let me = this;
> +
> +            let matches = /^(?:([A-Za-z.,]+)\s*)?([0-9:]+)\s*-\s*([0-9:]+)$/.exec(timeframe);
> +
> +            // Schedules which the editor cannot represent, an empty one in
> +            // particular, start out without any selected day and are thus
> +            // shown as invalid.
> +            if (!matches) {
> +                return {
> +                    start: '00:00',
> +                    end: '23:59',
> +                };
> +            }
> +
> +            let [, days, start, end] = matches;
> +
> +            if (start === '0') {
> +                start = '00:00';
> +            }
> +
> +            let record = {
> +                start,
> +                end,
> +            };
> +
> +            if (!days) {
> +                days = 'mon..sun';
> +            }
> +
> +            days = days.split(',');
> +            days.forEach((day) => {
> +                // We don't care about casing
> +                day = day.toLowerCase();
> +
> +                if (record[day]) {
> +                    return;
> +                }
> +
> +                if (day.includes('..')) {
> +                    // we have a range 'xxx..yyy'
> +                    let [startDay, endDay] = day.split('..');
> +
> +                    // We accept both 'monday' and 'mon'
> +                    startDay = startDay.substring(0, 3);
> +                    endDay = endDay.substring(0, 3);
> +
> +                    let startIdx = me.weekdays.indexOf(startDay);
> +                    let endIdx = me.weekdays.indexOf(endDay);
> +
> +                    if (endIdx < startIdx) {
> +                        endIdx += me.weekdays.length;
> +                    }
> +
> +                    for (let dayIdx = startIdx; dayIdx <= endIdx; dayIdx++) {
> +                        let curDay = me.weekdays[dayIdx % me.weekdays.length];
> +                        if (!record[curDay]) {
> +                            record[curDay] = true;
> +                        }
> +                    }
> +                } else {
> +                    // We accept both 'monday' and 'mon'
> +                    let dayTrimmed = day.substring(0, 3);
> +                    if (me.weekdays.indexOf(dayTrimmed) !== -1) {
> +                        record[dayTrimmed] = true;
> +                    }
> +                }
> +            });
> +
> +            return record;
> +        },
> +
> +        dowChanged: function (field, value) {
> +            let me = this;
> +            let record = field.getWidgetRecord();
> +            if (record === undefined) {
> +                // this is sometimes called before a record/column is initialized
> +                return;
> +            }
> +            let col = field.getWidgetColumn();
> +            record.set(col.dataIndex, value);
> +            record.commit();
> +
> +            let startField = me.lookup('timeStart');
> +            let endField = me.lookup('timeEnd');
> +
> +            me.updateTimeframeField(startField, endField);
> +        },
> +
> +        timeChanged: function (field, value) {
> +            let me = this;
> +
> +            let startField = me.lookup('timeStart');
> +            let endField = me.lookup('timeEnd');
> +
> +            let start = startField.getValue();
> +            let end = endField.getValue();
> +
> +            let valid = !(start && end && start > end);
> +
> +            if (!valid) {
> +                startField.markInvalid(gettext('Start time must be before end time'));
> +                endField.markInvalid(gettext('End time must be after start time'));
> +            } else {
> +                startField.clearInvalid();
> +                endField.clearInvalid();
> +            }
> +
> +            me.updateTimeframeField(startField, endField);
> +        },
> +
> +        updateTimeframeField: function (startField, endField) {
> +            let me = this;
> +
> +            let data = me.lookup('weekdayGrid').getStore().getData().getAt(0);
> +
> +            let days = me.formatSelectedDays(data.data);
> +
> +            let start = me.formatTime(startField);
> +            let end = me.formatTime(endField);
> +
> +            // An empty selection cannot be expressed, as a schedule without
> +            // weekdays already matches every day. Store an empty schedule
> +            // instead, which is reported as invalid by the rule tree.
> +            let timeframe = '';
> +
> +            if (days !== undefined) {
> +                timeframe = days ? `${days} ${start}-${end}` : `${start}-${end}`;
> +            }
> +
> +            let field = me.lookup('timeframe');
> +            field.suspendEvent('change');
> +            field.setValue(timeframe);
> +
> +            me.getViewModel().set('matchCalendarValue', timeframe);
> +
> +            field.resumeEvent('change');
> +        },
> +
> +        // Returns the weekday part of a schedule, which is empty if every day is
> +        // selected, or `undefined` if no day is selected at all.
> +        formatSelectedDays: function (days) {
> +            let me = this;
> +            let selected = me.weekdays.filter((day) => days[day]);
> +
> +            if (selected.length === 0) {
> +                return undefined;
> +            }
> +            if (selected.length === me.weekdays.length) {
> +                return '';
> +            }
> +            if (selected.length === 1) {
> +                return selected[0];
> +            }
> +
> +            // Check if selected days are a contiguous block in weekday order
> +            let indices = selected.map((day) => me.weekdays.indexOf(day));
> +            let isContiguous = indices.every((idx, i) => i === 0 || idx === indices[i - 1] + 1);
> +
> +            if (isContiguous) {
> +                return `${selected[0]}..${selected[selected.length - 1]}`;
> +            }
> +
> +            return selected.join(',');
> +        },
> +
> +        formatTime: function (timefield) {
> +            let value = timefield.getValue();
> +
> +            if (!value) {
> +                return '';
> +            }
> +
> +            let hours = value.getHours().toString().padStart(2, '0');
> +            let minutes = value.getMinutes().toString().padStart(2, '0');
> +            return `${hours}:${minutes}`;
> +        },
> +    },
>       items: [
>           {
> -            xtype: 'proxmoxKVComboBox',
> -            fieldLabel: gettext('Timespan to match'),
> +            xtype: 'hidden',
> +            reference: 'timeframe',
>               isFormField: false,
>               allowBlank: false,
> -            editable: true,
> -            displayField: 'key',
> -            field: 'value',
>               bind: {
>                   value: '{matchCalendarValue}',
> -                disabled: '{!typeIsMatchCalender}',
>               },
> -
> -            comboItems: [
> -                ['mon 8-12', ''],
> -                ['tue..fri,sun 0:00-23:59', ''],
> +        },
> +        {
> +            xtype: 'timefield',
> +            reference: 'timeStart',
> +            fieldLabel: gettext('Time Start'),
> +            isFormField: false,
> +            format: 'H:i',
> +            altFormats: 'G:i|Gi|G',
> +            formatText: 'HH:MM',
> +            maskRe: /[\d:]/,
> +            stripCharsRe: /[^\d:]+/g,
> +            allowBlank: false,
> +        },
> +        {
> +            xtype: 'timefield',
> +            reference: 'timeEnd',
> +            fieldLabel: gettext('Time End'),
> +            isFormField: false,
> +            format: 'H:i',
> +            altFormats: 'G:i|Gi|G',
> +            formatText: 'HH:MM',
> +            maxValue: '23:59',
> +            maskRe: /[\d:]/,
> +            stripCharsRe: /[^\d:]+/g,
> +            allowBlank: false,
> +        },
> +        {
> +            xtype: 'fieldcontainer',
> +            items: [
> +                {
> +                    xtype: 'grid',
> +                    margin: '10 0 0 0',
> +                    reference: 'weekdayGrid',
> +                    store: {
> +                        fields: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
> +                        data: [
> +                            {
> +                                mon: true,
> +                                tue: true,
> +                                wed: true,
> +                                thu: true,
> +                                fri: true,
> +                                sat: true,
> +                                sun: true,
> +                            },
> +                        ],
> +                    },
> +                    enableColumnMove: false,
> +                    columns: {
> +                        defaults: {
> +                            menuDisabled: true,
> +                            resizable: false,
> +                        },
> +                        items: [
> +                            {
> +                                text: gettext('Mon'),
> +                                xtype: 'widgetcolumn',
> +                                dataIndex: 'mon',
> +                                flex: 1,
> +                                widget: {
> +                                    xtype: 'checkbox',
> +                                    isFormField: false,
> +                                },
> +                            },
> +                            {
> +                                text: gettext('Tue'),
> +                                xtype: 'widgetcolumn',
> +                                dataIndex: 'tue',
> +                                flex: 1,
> +                                widget: {
> +                                    xtype: 'checkbox',
> +                                    isFormField: false,
> +                                },
> +                            },
> +                            {
> +                                text: gettext('Wed'),
> +                                xtype: 'widgetcolumn',
> +                                dataIndex: 'wed',
> +                                flex: 1,
> +                                widget: {
> +                                    xtype: 'checkbox',
> +                                    isFormField: false,
> +                                },
> +                            },
> +                            {
> +                                text: gettext('Thu'),
> +                                xtype: 'widgetcolumn',
> +                                dataIndex: 'thu',
> +                                flex: 1,
> +                                widget: {
> +                                    xtype: 'checkbox',
> +                                    isFormField: false,
> +                                },
> +                            },
> +                            {
> +                                text: gettext('Fri'),
> +                                xtype: 'widgetcolumn',
> +                                dataIndex: 'fri',
> +                                flex: 1,
> +                                widget: {
> +                                    xtype: 'checkbox',
> +                                    isFormField: false,
> +                                },
> +                            },
> +                            {
> +                                text: gettext('Sat'),
> +                                xtype: 'widgetcolumn',
> +                                dataIndex: 'sat',
> +                                flex: 1,
> +                                widget: {
> +                                    xtype: 'checkbox',
> +                                    isFormField: false,
> +                                },
> +                            },
> +                            {
> +                                text: gettext('Sun'),
> +                                xtype: 'widgetcolumn',
> +                                dataIndex: 'sun',
> +                                flex: 1,
> +                                widget: {
> +                                    xtype: 'checkbox',
> +                                    isFormField: false,
> +                                },
> +                            },
> +                        ],
> +                    },
> +                },
>               ],
>           },
>       ],





  reply	other threads:[~2026-11-03 13:56 UTC|newest]

Thread overview: 48+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-03 10:48 [PATCH many v7 00/41] notifications: add nested match expressions Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 01/41] add new proxmox-match-expression crate Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 02/41] notify: log which matchers matched on a notification Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 03/41] notify: promote matcher to dir-style module Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 04/41] notify: fix doc comment Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 05/41] notify: matcher: break out severity matcher into submodule Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 06/41] notify: matcher: break out field " Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 07/41] notify: matcher: break out calendar " Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 08/41] notify: matcher: calendar: add basic unit test Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 09/41] notify: matcher: add InlineSeverityMatcher Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 10/41] notify: matcher: add InlineFieldMatcher Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 11/41] notify: matcher: add InlineCalendarMatcher Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 12/41] notify: matcher: add expression support Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 13/41] notify: api: support new expression parameter Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 14/41] notify: api: add `get_matcher_as_expression` Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 15/41] notify: move legacy matcher keys behind feature flag Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 16/41] notify: matcher: don't allow empty field/severity matchers Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 17/41] notify: matcher: re-serialize expression on create/update Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 18/41] notify: matcher: ensure that field names match SAFE_ID_REGEX Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox v7 19/41] notify: matcher: expression: reject ambiguous field matchers Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-widget-toolkit v7 20/41] notifications: increase matcher window width Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-widget-toolkit v7 21/41] notifications: matcher: add support for match expressions Lukas Wagner
2026-11-03 12:57   ` Dominik Csapak
2026-11-03 15:23     ` Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-widget-toolkit v7 22/41] notifications: matcher: add better calendar editor Lukas Wagner
2026-11-03 13:56   ` Dominik Csapak [this message]
2026-11-03 14:40     ` Lukas Wagner
2026-11-03 14:57       ` Dominik Csapak
2026-11-03 10:48 ` [PATCH proxmox-widget-toolkit v7 23/41] notifications: matcher: consistently use title case for UI elements Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-backup v7 24/41] notification: opt into 'legacy-matchers' feature in proxmox-notify Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-backup v7 25/41] api: notification: add 'migrate-to-expression' parameter to get_matcher Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-backup v7 26/41] ui: notification: enable new matcher UI Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-backup v7 27/41] docs: notifications: rework matcher sections, describe expression option Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-perl-rs v7 28/41] notify: move proxmox-notify bindings from common to pve-rs Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-perl-rs v7 29/41] notify: matcher: pass matcher config / updater directly Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-perl-rs v7 30/41] notify: opt into 'legacy-matchers' feature in proxmox-notify Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-perl-rs v7 31/41] notify: add 'migrate_to_expression' parameter for get_matcher Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-perl-rs v7 32/41] debian: document versioned break for PVE::RS::Notify changes Lukas Wagner
2026-11-03 10:48 ` [PATCH pve-cluster v7 33/41] notify: adapt to changed module prefix for PVE::RS::Notify Lukas Wagner
2026-11-03 10:48 ` [PATCH manager v7 34/41] vzdump: " Lukas Wagner
2026-11-03 10:48 ` [PATCH manager v7 35/41] api: notification: pass config/updater directly to rust bindings Lukas Wagner
2026-11-03 10:48 ` [PATCH manager v7 36/41] api: notification: get_matcher: add 'migrate-to-expression' parameter Lukas Wagner
2026-11-03 10:48 ` [PATCH manager v7 37/41] api: notification: add 'expression' to matcher parameter schema Lukas Wagner
2026-11-03 10:48 ` [PATCH manager v7 38/41] ui: notification: enable new matcher UI Lukas Wagner
2026-11-03 10:48 ` [PATCH manager v7 39/41] api: notification: reject matchers with expression if not supported by all nodes Lukas Wagner
2026-11-03 10:48 ` [PATCH docs v7 40/41] notifications: rework matcher sections, describe expression option Lukas Wagner
2026-11-03 10:48 ` [PATCH proxmox-mail-forward v7 41/41] cargo: opt into legacy-matchers feature Lukas Wagner
2026-11-04 22:57 ` [PATCH many v7 00/41] notifications: add nested match expressions Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=8f8b7a19-02f9-4505-bad2-271631301381@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=l.wagner@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal