Files
gitea/templates/install.tmpl
T
petru 471cfdd161
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
Modified - [install] [backup] [database] [recovery] Consolidated database backup and installer recovery support.
- 1 - Add: Gitea now creates timestamped database backup bundles under `[backup].PATH`, exposes the backup schedule in the installer, and surfaces the `database_backup` cron task in admin monitoring.
- 2 - Add: installed instances now use `.gitea-installed` and `.gitea-recovery.ini` to enter email-gated recovery instead of falling back to public install mode when configuration or database access is broken.
- 3 - Mod: the installer recovery flow now covers backup-bundle restore, bundled or manual `app.ini` reuse, uploaded SQL/GZ database restores, and repository-filesystem recovery with source-specific validation, confirmations, and preserved launcher state.
- 4 - Fix: recovery now restores bundled `app.ini` snapshots when needed, discovers backup bundles from both the active backup path and persisted `.gitea-recovery.ini` path, and preserves SMTP and other rebuilt settings correctly when `app.ini` is missing or incomplete.
- 5 - Fix: recovery validation and restore handling now accept either a selected backup bundle or an uploaded SQL/GZ dump, keep sensitive secrets and existing `LFS_JWT_SECRET` when appropriate, clear SQLite restore targets before import, and complete the post-install handoff without redirect loops.
- 6 - Mod: fresh installs now default recovery email authorization to enabled with first-admin fallback, and the install/recovery UI, styling, and EN/RO wording were refined to match the final launcher behavior.

Co-Authored-By: petru @ codex (GPT-5) <codex@openai.com>
(cherry picked from commit 9879caf2292691b0cb521d12e6fee924b066bae2)
2026-06-01 03:56:03 +03:00

1283 lines
72 KiB
Handlebars

{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content install">
<div class="ui grid install-config-container">
<div class="sixteen wide tw-text-center centered column">
<h3 class="ui top attached header">
{{ctx.Locale.Tr "install.title"}}
</h3>
<div class="ui attached segment">
{{if not (or .Err_DbInstalledBefore .Err_RepositoryFilesystemRecovery .Err_DatabaseBackupRecovery)}}{{template "base/alert" .}}{{end}} <!-- edit/add - by petru @ codex -->
<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p>
<form id="install-form" class="ui form js-install-form" action="{{.InstallFormAction}}" method="post" enctype="multipart/form-data"> <!-- edit/add - by petru @ codex -->
<!-- start edit/add - by petru @ codex -->
<div class="install-recovery-entry">
<div class="install-recovery-entry-copy">
<h4 class="ui header">{{.InstallRecoverySectionTitle}}</h4>
<p>{{.InstallRecoverySectionDesc}}</p>
</div>
<div class="install-recovery-entry-actions">
<button type="button" class="ui primary button js-install-recovery-launcher">{{ctx.Locale.Tr "install.recovery_reinstall_open"}}</button>
</div>
</div>
<!-- end edit/add - by petru @ codex -->
<input type="hidden" name="imported_app_ini" value="{{if .imported_app_ini}}true{{end}}"> <!-- edit/add - by petru @ codex -->
<input type="hidden" id="install_backup_restore_id" name="backup_restore_id" value="{{.backup_restore_id}}"> <!-- edit/add - by petru @ codex -->
<input type="hidden" id="install_backup_import_app_ini" name="backup_import_app_ini" value="{{if .backup_import_app_ini}}true{{end}}"> <!-- edit/add - by petru @ codex -->
<input type="hidden" id="install_backup_restore_db" name="backup_restore_db" value="{{if .backup_restore_db}}true{{end}}"> <!-- edit/add - by petru @ codex -->
<input type="hidden" id="install_recovery_mode" name="recovery_mode" value="{{.recovery_mode}}"> <!-- edit/add - by petru @ codex -->
<input type="hidden" id="install_reinstall_confirm_first" name="reinstall_confirm_first" value="{{if .reinstall_confirm_first}}true{{end}}"> <!-- edit/add - by petru @ codex -->
<input type="hidden" id="install_reinstall_confirm_second" name="reinstall_confirm_second" value="{{if .reinstall_confirm_second}}true{{end}}"> <!-- edit/add - by petru @ codex -->
<input type="hidden" id="install_reinstall_confirm_third" name="reinstall_confirm_third" value="{{if .reinstall_confirm_third}}true{{end}}"> <!-- edit/add - by petru @ codex -->
{{if .InstallIsRecoveryRequest}}<input type="hidden" name="recovery_request" value="true">{{end}} <!-- edit/add - by petru @ codex -->
<input type="hidden" name="imported_lfs_jwt_secret" value="{{.imported_lfs_jwt_secret}}">
<input type="hidden" name="imported_internal_token" value="{{.imported_internal_token}}">
<input type="hidden" name="imported_o_auth2_jwt_secret" value="{{.imported_o_auth2_jwt_secret}}">
<!-- Database Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.db_title"}}</h4>
<p>{{ctx.Locale.Tr "install.require_db_desc"}}</p>
<div class="inline required field {{if .Err_DbType}}error{{end}}">
<label>{{ctx.Locale.Tr "install.db_type"}}</label>
<div class="ui selection database type dropdown">
<input type="hidden" id="db_type" name="db_type" value="{{.CurDbType}}">
<div class="text">{{.CurDbType}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{range .DbTypeNames}}
<div class="item" data-value="{{.type}}">{{.name}}</div>
{{end}}
</div>
</div>
</div>
<div class="tw-mt-4 tw-hidden" data-db-setting-for="common-host">
<div class="inline required field {{if .Err_DbSetting}}error{{end}}">
<label for="db_host">{{ctx.Locale.Tr "install.host"}}</label>
<input id="db_host" name="db_host" value="{{.db_host}}">
</div>
<div class="inline required field {{if .Err_DbSetting}}error{{end}}">
<label for="db_user">{{ctx.Locale.Tr "install.user"}}</label>
<input id="db_user" name="db_user" value="{{.db_user}}">
</div>
<div class="inline required field {{if .Err_DbSetting}}error{{end}}">
<label for="db_passwd">{{ctx.Locale.Tr "install.password"}}</label>
<div class="ui input js-password-toggle-group" data-global-init="initPasswordVisibilityToggle" data-show-label="{{ctx.Locale.Tr "auth.show_password"}}" data-hide-label="{{ctx.Locale.Tr "auth.hide_password"}}">
<input id="db_passwd" name="db_passwd" type="password" value="{{.db_passwd}}" class="js-password-toggle-source">
</div>
</div>
<div class="inline required field {{if .Err_DbSetting}}error{{end}}">
<label for="db_name">{{ctx.Locale.Tr "install.db_name"}}</label>
<input id="db_name" name="db_name" value="{{.db_name}}">
</div>
</div>
<div class="tw-mt-4 tw-hidden" data-db-setting-for="postgres">
<div class="inline required field">
<label>{{ctx.Locale.Tr "install.ssl_mode"}}</label>
<div class="ui selection database type dropdown">
<input type="hidden" name="ssl_mode" value="{{if .ssl_mode}}{{.ssl_mode}}{{else}}disable{{end}}">
<div class="default text">disable</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<div class="item" data-value="disable">Disable</div>
<div class="item" data-value="require">Require</div>
<div class="item" data-value="verify-full">Verify Full</div>
</div>
</div>
</div>
<div class="inline field {{if .Err_DbSetting}}error{{end}}">
<label for="db_schema">{{ctx.Locale.Tr "install.db_schema"}}</label>
<input id="db_schema" name="db_schema" value="{{.db_schema}}">
<span class="help">{{ctx.Locale.Tr "install.db_schema_helper"}}</span>
</div>
</div>
<div class="tw-mt-4 tw-hidden" data-db-setting-for="sqlite3">
<div class="inline required field {{if or .Err_DbPath .Err_DbSetting}}error{{end}}">
<label for="db_path">{{ctx.Locale.Tr "install.path"}}</label>
<input id="db_path" name="db_path" value="{{.db_path}}">
<span class="help">{{ctx.Locale.Tr "install.sqlite_helper"}}</span>
</div>
</div>
<!-- start edit/add - by petru @ codex -->
<details class="optional field"{{if or .Err_BackupPath .Err_BackupRecovery}} open{{end}}>
<summary class="right-content tw-py-2{{if or .Err_BackupPath .Err_BackupRecovery}} tw-text-red{{end}}">
{{ctx.Locale.Tr "install.database_backup_title"}}
</summary>
<span class="desc">{{ctx.Locale.Tr "install.database_backup_desc"}}</span>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.database_backup_enabled"}}</label>
<input name="backup_enabled" type="checkbox" {{if .backup_enabled}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.database_backup_enabled_helper"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.database_backup_run_at_start"}}</label>
<input name="backup_run_at_start" type="checkbox" {{if .backup_run_at_start}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.database_backup_run_at_start_helper"}}</span>
</div>
<div class="inline field">
<label for="backup_schedule">{{ctx.Locale.Tr "install.database_backup_schedule"}}</label>
<input id="backup_schedule" name="backup_schedule" value="{{.backup_schedule}}" placeholder="@daily">
<span class="help">{{ctx.Locale.Tr "install.database_backup_schedule_helper"}}</span>
</div>
<div class="inline field">
<label for="backup_path">{{ctx.Locale.Tr "install.database_backup_path"}}</label>
<input id="backup_path" name="backup_path" value="{{.backup_path}}">
<span class="help">{{ctx.Locale.Tr "install.database_backup_path_helper"}}</span>
</div>
<div class="inline field">
<label for="backup_retention_count">{{ctx.Locale.Tr "install.database_backup_retention_count"}}</label>
<input id="backup_retention_count" name="backup_retention_count" type="number" min="0" value="{{.backup_retention_count}}">
<span class="help">{{ctx.Locale.Tr "install.database_backup_retention_count_helper"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.database_backup_compress"}}</label>
<input name="backup_compress" type="checkbox" {{if .backup_compress}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.database_backup_compress_helper"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.database_backup_include_app_ini_snapshot"}}</label>
<input name="backup_include_app_ini_snapshot" type="checkbox" {{if .backup_include_app_ini_snapshot}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.database_backup_include_app_ini_snapshot_helper"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.recovery_email_enabled"}}</label>
<input name="recovery_email_enabled" type="checkbox" {{if .recovery_email_enabled}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.recovery_email_enabled_helper"}}</span>
</div>
<div class="inline field {{if .Err_BackupRecovery}}error{{end}}">
<label for="recovery_allowed_emails">{{ctx.Locale.Tr "install.recovery_allowed_emails"}}</label>
<textarea id="recovery_allowed_emails" name="recovery_allowed_emails" rows="3">{{.recovery_allowed_emails}}</textarea>
<span class="help">{{ctx.Locale.Tr "install.recovery_allowed_emails_helper"}}</span>
</div>
</details>
<!-- end edit/add - by petru @ codex -->
<!-- General Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.general_title"}}</h4>
<div class="inline required field {{if .Err_AppName}}error{{end}}">
<label for="app_name">{{ctx.Locale.Tr "install.app_name"}}</label>
<input id="app_name" name="app_name" value="{{.app_name}}" required>
<span class="help">{{ctx.Locale.Tr "install.app_name_helper"}}</span>
</div>
<div class="inline field {{if .Err_DefaultLanguage}}error{{end}}">
<label>{{ctx.Locale.Tr "install.default_language"}}</label>
<div class="ui language selection dropdown">
<input name="default_language" type="hidden" value="{{.default_language}}">
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="text">{{range .AllLangs}}{{if eq $.default_language .Lang}}{{.Name}}{{end}}{{end}}</div>
<div class="menu">
{{range .AllLangs}}
<div class="item{{if eq $.default_language .Lang}} active selected{{end}}" data-value="{{.Lang}}">{{.Name}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "install.default_language_helper"}}</span>
</div>
<div class="inline required field {{if .Err_RepoRootPath}}error{{end}}">
<label for="repo_root_path">{{ctx.Locale.Tr "install.repo_path"}}</label>
<input id="repo_root_path" name="repo_root_path" value="{{.repo_root_path}}" required>
<span class="help">{{ctx.Locale.Tr "install.repo_path_helper"}}</span>
</div>
<div class="inline field {{if .Err_LFSRootPath}}error{{end}}">
<label for="lfs_root_path">{{ctx.Locale.Tr "install.lfs_path"}}</label>
<input id="lfs_root_path" name="lfs_root_path" value="{{.lfs_root_path}}">
<span class="help">{{ctx.Locale.Tr "install.lfs_path_helper"}}</span>
</div>
<div class="inline field">
<label for="run_user">{{ctx.Locale.Tr "install.run_user"}}</label>
<input id="run_user" name="run_user" value="{{.run_user}}" readonly>
<span class="help">{{ctx.Locale.Tr "install.run_user_helper"}}</span>
</div>
<div class="inline required field">
<label for="domain">{{ctx.Locale.Tr "install.domain"}}</label>
<input id="domain" name="domain" value="{{.domain}}" placeholder="demo.gitea.com" required>
<span class="help">{{ctx.Locale.Tr "install.domain_helper"}}</span>
</div>
<div class="inline field">
<label for="ssh_port">{{ctx.Locale.Tr "install.ssh_port"}}</label>
<input id="ssh_port" name="ssh_port" value="{{.ssh_port}}">
<span class="help">{{ctx.Locale.Tr "install.ssh_port_helper"}}</span>
</div>
<div class="inline required field">
<label for="http_port">{{ctx.Locale.Tr "install.http_port"}}</label>
<input id="http_port" name="http_port" value="{{.http_port}}" required>
<span class="help">{{ctx.Locale.Tr "install.http_port_helper"}}</span>
</div>
<div class="inline required field">
<label for="app_url">{{ctx.Locale.Tr "install.app_url"}}</label>
<input id="app_url" name="app_url" value="{{.app_url}}" placeholder="https://demo.gitea.com" required>
<span class="help">{{ctx.Locale.Tr "install.app_url_helper"}}</span>
</div>
<div class="inline required field">
<label for="log_root_path">{{ctx.Locale.Tr "install.log_root_path"}}</label>
<input id="log_root_path" name="log_root_path" value="{{.log_root_path}}" placeholder="log" required>
<span class="help">{{ctx.Locale.Tr "install.log_root_path_helper"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.enable_update_checker"}}</label>
<input name="enable_update_checker" type="checkbox">
</div>
<span class="help">{{ctx.Locale.Tr "install.enable_update_checker_helper"}}</span>
</div>
<!-- Optional Settings -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4>
<div>
<!-- Branding -->
<details class="optional field"{{if .Err_Branding}} open{{end}}>
<summary class="right-content tw-py-2{{if .Err_Branding}} tw-text-red{{end}}">
{{ctx.Locale.Tr "install.branding_title"}}
</summary>
<span class="desc">{{ctx.Locale.Tr "install.branding_desc"}}</span>
<div class="inline field">
<div class="ui checkbox">
<label for="branding_use_shared_assets">{{ctx.Locale.Tr "install.branding.shared_assets"}}</label>
<input id="branding_use_shared_assets" name="branding_use_shared_assets" type="checkbox" {{if .branding_use_shared_assets}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.branding.shared_assets_helper"}}</span>
</div>
<div class="inline field">
<label for="logo_svg" id="branding_logo_svg_label" data-default-label="{{ctx.Locale.Tr "install.branding.logo_svg"}}" data-shared-label="{{ctx.Locale.Tr "install.branding.logo_and_favicon_svg"}}">{{ctx.Locale.Tr "install.branding.logo_svg"}}</label>
<input id="logo_svg" name="logo_svg" type="file" accept=".svg,image/svg+xml">
<span class="help">{{ctx.Locale.Tr "install.branding.logo_svg_helper" .BrandingMaxFileSizeKB}}</span>
</div>
<div class="inline field">
<label for="logo_png" id="branding_logo_png_label" data-default-label="{{ctx.Locale.Tr "install.branding.logo_png"}}" data-shared-label="{{ctx.Locale.Tr "install.branding.logo_and_favicon_png"}}">{{ctx.Locale.Tr "install.branding.logo_png"}}</label>
<input id="logo_png" name="logo_png" type="file" accept=".png,image/png">
<span class="help">{{ctx.Locale.Tr "install.branding.logo_png_helper" .BrandingMaxFileSizeKB .BrandingMinPNGEdge .BrandingMinPNGEdge}}</span>
</div>
<div class="inline field">
<label for="loading_png">{{ctx.Locale.Tr "install.branding.loading_png"}}</label>
<input id="loading_png" name="loading_png" type="file" accept=".png,image/png">
<span class="help">{{ctx.Locale.Tr "install.branding.loading_png_helper" .BrandingMaxFileSizeKB .BrandingMinPNGEdge .BrandingMinPNGEdge}}</span>
</div>
<div class="inline field js-install-branding-favicon-field">
<label for="favicon_svg">{{ctx.Locale.Tr "install.branding.favicon_svg"}}</label>
<input id="favicon_svg" name="favicon_svg" type="file" accept=".svg,image/svg+xml">
<span class="help">{{ctx.Locale.Tr "install.branding.favicon_svg_helper" .BrandingMaxFileSizeKB}}</span>
</div>
<div class="inline field js-install-branding-favicon-field">
<label for="favicon_png">{{ctx.Locale.Tr "install.branding.favicon_png"}}</label>
<input id="favicon_png" name="favicon_png" type="file" accept=".png,image/png">
<span class="help">{{ctx.Locale.Tr "install.branding.favicon_png_helper" .BrandingMaxFileSizeKB .BrandingMinPNGEdge .BrandingMinPNGEdge}}</span>
</div>
</details>
<!-- Email -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_SMTP}} tw-text-red{{end}}">
{{ctx.Locale.Tr "install.email_title"}}
</summary>
<div class="inline field">
<label for="smtp_addr">{{ctx.Locale.Tr "install.smtp_addr"}}</label>
<input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}">
</div>
<div class="inline field">
<label for="smtp_port">{{ctx.Locale.Tr "install.smtp_port"}}</label>
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}">
</div>
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}">
<label for="smtp_from_name">{{ctx.Locale.Tr "install.smtp_from"}}</label>
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-flex-wrap" style="width: 60%;">
<div class="ui input" style="flex: 1 1 200px;">
<input id="smtp_from_name" name="smtp_from_name" value="{{.smtp_from_name}}" placeholder="{{ctx.Locale.Tr "install.smtp_from_name_placeholder"}}">
</div>
<div class="ui input" style="flex: 1 1 240px;">
<input id="smtp_from_address" name="smtp_from_address" type="email" value="{{.smtp_from_address}}" placeholder="{{ctx.Locale.Tr "install.smtp_from_address_placeholder"}}">
</div>
</div>
<span class="help">{{ctx.Locale.TrString "install.smtp_from_helper"}}{{/* it contains lt/gt chars*/}}</span>
</div>
<div class="inline field {{if .Err_SMTPUser}}error{{end}}">
<label for="smtp_user">{{ctx.Locale.Tr "install.mailer_user"}}</label>
<input id="smtp_user" name="smtp_user" value="{{.smtp_user}}">
</div>
<div class="inline field">
<label for="smtp_passwd">{{ctx.Locale.Tr "install.mailer_password"}}</label>
<div class="ui input js-password-toggle-group" data-global-init="initPasswordVisibilityToggle" data-show-label="{{ctx.Locale.Tr "auth.show_password"}}" data-hide-label="{{ctx.Locale.Tr "auth.hide_password"}}">
<input id="smtp_passwd" name="smtp_passwd" type="password" value="{{.smtp_passwd}}" class="js-password-toggle-source">
</div>
</div>
<div class="inline field">
<label for="test_mail_email">{{ctx.Locale.Tr "install.test_mail_to"}}</label>
<div class="tw-inline-flex tw-items-center tw-gap-2 tw-flex-wrap" style="width: 60%;">
<div class="ui test_mail small input">
<input id="test_mail_email" name="test_mail_email" type="email" placeholder="{{ctx.Locale.Tr "admin.config.test_email_placeholder"}}" size="40">
<button class="ui tiny js-install-test-mail-button button" data-mail-action-ui="inline-v2" data-action="{{.InstallTestMailAction}}" type="button" style="background: var(--color-primary); color: var(--color-primary-contrast); border-color: var(--color-primary);">{{ctx.Locale.Tr "admin.config.send_test_mail_submit"}}</button>
</div>
<span class="js-install-test-mail-message"></span>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "install.mail_notify"}}</label>
<input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.mail_notify_helper"}}</span>
</div>
</details>
<!-- Registration management -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Registration}} tw-text-red{{end}}">
{{ctx.Locale.Tr "install.registration_title"}}
</summary>
<div class="inline field">
<label>{{ctx.Locale.Tr "install.registration_mode"}}</label>
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input name="registration_mode" type="radio" value="admin_only" {{if eq .registration_mode "admin_only"}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.registration_mode.admin_only"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.registration_mode.admin_only_helper"}}</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="registration_mode" type="radio" value="local_only" {{if eq .registration_mode "local_only"}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.registration_mode.local_only"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.registration_mode.local_only_helper"}}</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="registration_mode" type="radio" value="external_only" {{if eq .registration_mode "external_only"}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.registration_mode.external_only"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.registration_mode.external_only_helper"}}</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="registration_mode" type="radio" value="local_and_external" {{if or (not .registration_mode) (eq .registration_mode "local_and_external")}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.registration_mode.local_and_external"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.registration_mode.local_and_external_helper"}}</p>
</div>
</div>
</div>
<div class="inline field" id="registration-local-options">
<label>{{ctx.Locale.Tr "install.registration_local_options"}}</label>
<div class="grouped fields">
<div class="field">
<div class="ui checkbox">
<input name="register_confirm" type="checkbox" {{if or .register_confirm .register_manual_confirm}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.register_confirm"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.register_confirm_helper"}}</p>
</div>
<div class="field">
<div class="ui checkbox">
<input name="register_manual_confirm" type="checkbox" {{if .register_manual_confirm}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.register_manual_confirm"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.register_manual_confirm_helper"}}</p>
</div>
<div class="field">
<div class="ui checkbox">
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.enable_captcha"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.enable_captcha_popup"}}</p>
</div>
</div>
</div>
<div class="inline field" id="registration-external-options">
<label>{{ctx.Locale.Tr "install.registration_external_options"}}</label>
<div class="grouped fields">
<div class="field">
<div class="ui checkbox">
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.openid_signin"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.openid_signin_helper"}}</p>
</div>
<div class="field">
<div class="ui checkbox">
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.openid_signup"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.openid_signup_helper"}}</p>
</div>
</div>
</div>
</details>
<!-- Server and other services -->
<details class="optional field">
<summary class="right-content tw-py-2">
{{ctx.Locale.Tr "install.server_service_title"}}
</summary>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label>
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label>
<input name="default_keep_email_private" type="checkbox" {{if .default_keep_email_private}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_organization_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_organization"}}</label>
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label>
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}>
</div>
</div>
<div class="inline field">
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label>
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}">
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span>
</div>
<div class="inline field">
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label>
<div class="ui selection dropdown">
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}">
<div class="text">{{.password_algorithm}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{range .PasswordHashAlgorithms}}
<div class="item" data-value="{{.}}">{{.}}</div>
{{end}}
</div>
</div>
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span>
</div>
</details>
<!-- Repository options -->
<details class="optional field">
<summary class="right-content tw-py-2">
{{ctx.Locale.Tr "install.repository_options_title"}}
</summary>
{{/* start edit/add - by petru @ codex */}}
<div class="inline field">
<div class="ui checkbox">
<label for="allow_adoption_of_unadopted_repositories">{{ctx.Locale.Tr "install.allow_adoption_of_unadopted_repositories"}}</label>
<input id="allow_adoption_of_unadopted_repositories" name="allow_adoption_of_unadopted_repositories" type="checkbox" {{if .allow_adoption_of_unadopted_repositories}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.allow_adoption_of_unadopted_repositories_helper"}}</span>
</div>
<div class="inline field">
<div class="ui checkbox">
<label for="allow_delete_of_unadopted_repositories">{{ctx.Locale.Tr "install.allow_delete_of_unadopted_repositories"}}</label>
<input id="allow_delete_of_unadopted_repositories" name="allow_delete_of_unadopted_repositories" type="checkbox" {{if .allow_delete_of_unadopted_repositories}}checked{{end}}>
</div>
<span class="help">{{ctx.Locale.Tr "install.allow_delete_of_unadopted_repositories_helper"}}</span>
</div>
{{/* end edit/add - by petru @ codex */}}
<div class="inline field">
<label for="release_max_files">{{ctx.Locale.Tr "install.release_max_files"}}</label>
<input id="release_max_files" name="release_max_files" type="number" min="1" step="1" value="{{.release_max_files}}">
<span class="help">{{ctx.Locale.Tr "install.release_max_files_helper"}}</span>
</div>
<div class="inline field">
<label for="release_file_max_size">{{ctx.Locale.Tr "install.release_file_max_size"}}</label>
<input id="release_file_max_size" name="release_file_max_size" type="number" min="1" step="1" value="{{.release_file_max_size}}">
<span class="help">{{ctx.Locale.Tr "install.release_file_max_size_helper"}}</span>
</div>
</details>
<!-- Admin -->
<details class="optional field">
<summary class="right-content tw-py-2{{if .Err_Admin}} tw-text-red{{end}}">
{{ctx.Locale.Tr "install.admin_title"}}
</summary>
<span class="desc">{{ctx.Locale.Tr "install.admin_setting_desc"}}</span>
<div class="inline field {{if .Err_AdminName}}error{{end}}">
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label>
<input id="admin_name" name="admin_name" value="{{.admin_name}}">
</div>
<div class="inline field {{if .Err_AdminEmail}}error{{end}}">
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label>
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}">
</div>
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label>
<div class="ui input js-password-toggle-group" data-global-init="initPasswordVisibilityToggle" data-show-label="{{ctx.Locale.Tr "auth.show_password"}}" data-hide-label="{{ctx.Locale.Tr "auth.hide_password"}}" data-confirm-field-selector=".js-install-admin-password-confirm-field" data-confirm-input-selector=".js-install-admin-password-confirm">
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}" class="js-password-toggle-source">
</div>
</div>
<div class="inline field js-password-toggle-confirm-field js-install-admin-password-confirm-field {{if .Err_AdminPasswd}}error{{end}}">
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label>
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}" class="js-password-toggle-confirm js-install-admin-password-confirm">
</div>
<div class="inline field">
<label>{{ctx.Locale.Tr "install.admin_management_policy"}}</label>
<div class="grouped fields">
<div class="field">
<div class="ui radio checkbox">
<input name="admin_management_policy" type="radio" value="super_admin_only" {{if eq .admin_management_policy "super_admin_only"}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.admin_management_policy.super_admin_only"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.admin_management_policy.super_admin_only_helper"}}</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="admin_management_policy" type="radio" value="grantor_only" {{if or (not .admin_management_policy) (eq .admin_management_policy "grantor_only")}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.admin_management_policy.grantor_only"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.admin_management_policy.grantor_only_helper"}}</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input name="admin_management_policy" type="radio" value="grantor_inheritance" {{if eq .admin_management_policy "grantor_inheritance"}}checked{{end}}>
<label>{{ctx.Locale.Tr "install.admin_management_policy.grantor_inheritance"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "install.admin_management_policy.grantor_inheritance_helper"}}</p>
</div>
</div>
</div>
</details>
</div>
<div class="divider"></div>
{{if .EnvConfigKeys}}
<!-- Environment Config -->
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.env_config_keys"}}</h4>
<div class="inline field">
<div class="right-content">
{{ctx.Locale.Tr "install.env_config_keys_prompt"}}
</div>
<div class="right-content tw-mt-2">
{{range .EnvConfigKeys}}<span class="ui label">{{.}}</span>{{end}}
</div>
</div>
{{end}}
<div class="inline field">
<div class="right-content">
{{$copyBtn := svg "octicon-copy" 14}}
{{$filePath := HTMLFormat `<span class="ui label">%s</span> <button class="btn interact-fg" data-clipboard-text="%s">%s</button>` .CustomConfFile .CustomConfFile $copyBtn}}
{{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}}
</div>
{{if not (or .Err_DbInstalledBefore .Err_RepositoryFilesystemRecovery .Err_DatabaseBackupRecovery)}} <!-- edit/add - by petru @ codex -->
<div class="tw-mt-4 tw-mb-2 tw-text-center">
<button
class="ui primary button js-install-confirm-button"
data-install-label-template="{{ctx.Locale.Tr "install.install_btn_confirm" "__SITE_NAME__"}}"
>{{ctx.Locale.Tr "install.install_btn_confirm" .InstallerSiteName}}</button>
</div>
{{end}} <!-- edit/add - by petru @ codex -->
</div>
</form>
</div>
</div>
</div>
</div>
<!-- start edit/add - by petru @ codex -->
<div class="ui large modal install-recovery-launcher-modal js-install-recovery-launcher-modal">
<div class="header">{{.InstallRecoveryLauncherTitle}}</div>
<div class="content">
{{if .InstallRecoveryProblemText}}
<div class="ui negative message install-recovery-launcher-problem">{{.InstallRecoveryProblemText}}</div>
{{end}}
<div class="ui form">
<h5 class="ui dividing header">{{ctx.Locale.Tr "install.recovery_source_title"}}</h5>
{{if .InstallRecoveryOptionDatabaseBackupAvailable}}
<div class="ui raised segment">
<h5 class="ui small header">{{ctx.Locale.Tr "install.recovery_launcher_backup_select"}}</h5>
<div class="inline field install-recovery-inline-field">
<label for="launcher_backup_restore_id">{{ctx.Locale.Tr "install.recovery_launcher_backup_bundle"}}</label>
<select id="launcher_backup_restore_id" class="install-recovery-select">
<option value="">{{ctx.Locale.Tr "install.recovery_launcher_backup_select_placeholder"}}</option>
{{range .InstallRecoveryBackups}}
<option value="{{.ID}}" data-has-app-ini="{{if .HasAppINI}}true{{else}}false{{end}}" data-has-database="{{if .HasDatabase}}true{{else}}false{{end}}" {{if eq $.backup_restore_id .ID}}selected{{end}}>{{.Label}}</option>
{{end}}
</select>
</div>
<p class="help">{{ctx.Locale.Tr "install.recovery_database_backup_select_helper"}}</p>
<div class="grouped fields js-launcher-backup-actions tw-hidden">
<div class="field js-launcher-backup-import-app-ini-field tw-hidden">
<div class="ui checkbox">
<input id="launcher_backup_import_app_ini" type="checkbox" data-import-action="{{.InstallBackupAppINIImportAction}}" {{if .backup_import_app_ini}}checked{{end}}>
<label for="launcher_backup_import_app_ini">{{ctx.Locale.Tr "install.recovery_launcher_backup_import_app_ini"}}</label>
</div>
</div>
<div class="field js-launcher-backup-restore-db-field tw-hidden">
<div class="ui checkbox">
<input id="launcher_backup_restore_db" type="checkbox" {{if .backup_restore_db}}checked{{end}}>
<label for="launcher_backup_restore_db">{{ctx.Locale.Tr "install.recovery_launcher_backup_restore_db"}}</label>
</div>
</div>
</div>
</div>
{{end}}
<div class="ui raised segment js-launcher-import-app-ini-segment">
<h5 class="ui small header">{{ctx.Locale.Tr "install.import_app_ini_title"}}</h5>
<div class="inline field install-recovery-inline-field">
<div class="install-recovery-inline-control-row">
<label for="app_ini_file">{{ctx.Locale.Tr "install.import_app_ini_file"}}</label>
<input id="app_ini_file" name="app_ini_file" type="file" accept=".ini,text/plain" form="install-form" data-import-action="{{.InstallImportAction}}">
<button type="button" class="ui button basic small js-install-recovery-reset-app-ini tw-hidden">{{ctx.Locale.Tr "install.recovery_launcher_reset"}}</button>
</div>
</div>
<span class="help">{{ctx.Locale.Tr "install.import_app_ini_helper" .AppINIImportMaxSizeKB}}</span>
<div class="field">
<div class="ui checkbox">
<input id="import_sensitive_secrets" name="import_sensitive_secrets" type="checkbox" form="install-form" {{if .import_sensitive_secrets}}checked{{end}}>
<label for="import_sensitive_secrets">{{ctx.Locale.Tr "install.import_app_ini_sensitive_secrets"}}</label>
</div>
<span class="help">{{ctx.Locale.Tr "install.import_app_ini_sensitive_secrets_helper"}}</span>
</div>
</div>
<div class="ui raised segment js-launcher-database-backup-file-segment">
<h5 class="ui small header">{{ctx.Locale.Tr "install.recovery_launcher_database_backup_file"}}</h5>
<div class="inline field install-recovery-inline-field">
<div class="install-recovery-inline-control-row">
<label for="database_backup_file">{{ctx.Locale.Tr "install.recovery_launcher_sql_backup_file"}}</label>
<input id="database_backup_file" name="database_backup_file" type="file" accept=".sql,.gz,application/gzip,application/x-gzip,text/plain" form="install-form">
<button type="button" class="ui button basic small js-install-recovery-reset-db-backup tw-hidden">{{ctx.Locale.Tr "install.recovery_launcher_reset"}}</button>
</div>
</div>
<span class="help">{{ctx.Locale.Tr "install.recovery_launcher_sql_backup_file_helper"}}</span>
</div>
{{if and .InstallRecoveryOptionRepositoryFilesystemAvailable (not .InstallRecoveryOptionExistingDBAvailable) (not .InstallRecoveryOptionDatabaseBackupAvailable)}}
<div class="ui raised segment js-launcher-repository-filesystem-segment">
<h5 class="ui small header">{{ctx.Locale.Tr "install.recovery_source_repository_filesystem"}}</h5>
<div class="field">
<div class="ui checkbox">
<input id="launcher_repository_filesystem" type="checkbox" {{if eq .recovery_mode "repository_filesystem"}}checked{{end}}>
<label for="launcher_repository_filesystem">{{ctx.Locale.Tr "install.recovery_source_repository_filesystem_helper"}}</label>
</div>
</div>
</div>
{{end}}
<div class="ui raised segment js-install-recovery-launcher-confirm-panel tw-hidden" data-confirm-kind="backup_restore">
<div class="grouped fields">
<div class="field">
<div class="ui checkbox">
<input id="launcher_backup_confirm_first" type="checkbox">
<label for="launcher_backup_confirm_first">{{ctx.Locale.Tr "install.recovery_database_backup_confirm_check_1"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input id="launcher_backup_confirm_second" type="checkbox">
<label for="launcher_backup_confirm_second">{{ctx.Locale.Tr "install.recovery_database_backup_confirm_check_2"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input id="launcher_backup_confirm_third" type="checkbox">
<label for="launcher_backup_confirm_third">{{ctx.Locale.Tr "install.recovery_database_backup_confirm_check_3"}}</label>
</div>
</div>
</div>
</div>
<div class="ui raised segment js-install-recovery-launcher-confirm-panel tw-hidden" data-confirm-kind="partial_restore">
<div class="grouped fields">
<div class="field">
<div class="ui checkbox">
<input id="launcher_partial_confirm_first" type="checkbox">
<label for="launcher_partial_confirm_first">{{ctx.Locale.Tr "install.reinstall_confirm_check_1"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input id="launcher_partial_confirm_second" type="checkbox">
<label for="launcher_partial_confirm_second">{{ctx.Locale.Tr "install.reinstall_confirm_check_2"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input id="launcher_partial_confirm_third" type="checkbox">
<label for="launcher_partial_confirm_third">{{ctx.Locale.Tr "install.reinstall_confirm_check_3"}}</label>
</div>
</div>
</div>
</div>
<div class="ui raised segment js-install-recovery-launcher-confirm-panel tw-hidden" data-confirm-kind="repository_filesystem">
<div class="grouped fields">
<div class="field">
<div class="ui checkbox">
<input id="launcher_repository_confirm_first" type="checkbox">
<label for="launcher_repository_confirm_first">{{ctx.Locale.Tr "install.recovery_repository_filesystem_confirm_check_1"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input id="launcher_repository_confirm_second" type="checkbox">
<label for="launcher_repository_confirm_second">{{ctx.Locale.Tr "install.recovery_repository_filesystem_confirm_check_2"}}</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input id="launcher_repository_confirm_third" type="checkbox">
<label for="launcher_repository_confirm_third">{{ctx.Locale.Tr "install.recovery_repository_filesystem_confirm_check_3"}}</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="actions">
<button type="button" class="ui button deny">{{ctx.Locale.Tr "cancel"}}</button>
<button type="submit" form="install-form" class="ui primary button approve js-install-recovery-launcher-save">{{ctx.Locale.Tr "save"}}</button>
</div>
</div>
<!-- end edit/add - by petru @ codex -->
<div class="install-language-balloon" id="install-language-balloon" role="status" aria-live="polite">
{{ctx.Locale.Tr "install.language_balloon"}}
</div>
<script>
const installAppINIInput = document.querySelector('#app_ini_file');
const dismissInstallSuccessFlash = () => {
window.setTimeout(() => {
for (const element of document.querySelectorAll('.flash-message.flash-success')) {
element.remove();
}
}, 5000);
};
// start edit/add - by petru @ codex
const syncInstallFormFromImportedResponse = (form, parsed) => {
const importedForm = parsed.querySelector('.js-install-form');
if (!importedForm) throw new Error('import form not found');
for (const importedInput of importedForm.querySelectorAll('input, textarea, select')) {
if (!(importedInput instanceof HTMLInputElement || importedInput instanceof HTMLTextAreaElement || importedInput instanceof HTMLSelectElement)) continue;
if (!importedInput.name) continue;
if (importedInput.type === 'file') continue;
if (importedInput.type === 'radio') {
const currentRadio = form.querySelector(`input[type="radio"][name="${CSS.escape(importedInput.name)}"][value="${CSS.escape(importedInput.value)}"]`);
if (currentRadio instanceof HTMLInputElement) currentRadio.checked = importedInput.checked;
continue;
}
if (importedInput.type === 'checkbox') {
const currentCheckbox = form.querySelector(`input[type="checkbox"][name="${CSS.escape(importedInput.name)}"]`) || document.querySelector(`input[type="checkbox"][name="${CSS.escape(importedInput.name)}"][form="install-form"]`); // edit/add - by petru @ codex
if (currentCheckbox instanceof HTMLInputElement) {
currentCheckbox.checked = importedInput.checked;
currentCheckbox.dispatchEvent(new Event('change', {bubbles: true}));
const checkboxContainer = currentCheckbox.closest('.ui.checkbox');
if (checkboxContainer) {
checkboxContainer.classList.toggle('checked', importedInput.checked);
}
}
continue;
}
const currentInput = form.querySelector(`[name="${CSS.escape(importedInput.name)}"]`) || document.querySelector(`[name="${CSS.escape(importedInput.name)}"][form="install-form"]`); // edit/add - by petru @ codex
if (currentInput instanceof HTMLInputElement || currentInput instanceof HTMLTextAreaElement || currentInput instanceof HTMLSelectElement) {
currentInput.value = importedInput.value;
currentInput.dispatchEvent(new Event('change', {bubbles: true}));
}
}
const importedDbTypeInput = importedForm.querySelector('#db_type');
const currentDbTypeInput = form.querySelector('#db_type');
if (importedDbTypeInput instanceof HTMLInputElement && currentDbTypeInput instanceof HTMLInputElement) {
currentDbTypeInput.value = importedDbTypeInput.value;
currentDbTypeInput.dispatchEvent(new Event('change', {bubbles: true}));
const currentDbText = form.querySelector('.database.type.dropdown .text');
const importedDbText = importedForm.querySelector('.database.type.dropdown .text');
if (currentDbText && importedDbText) currentDbText.textContent = importedDbText.textContent;
}
const importedLangInput = importedForm.querySelector('input[name="default_language"]');
const currentLangInput = form.querySelector('input[name="default_language"]');
if (importedLangInput instanceof HTMLInputElement && currentLangInput instanceof HTMLInputElement) {
currentLangInput.value = importedLangInput.value;
currentLangInput.dispatchEvent(new Event('change', {bubbles: true}));
const currentLangText = form.querySelector('.language.selection.dropdown .text');
const importedLangText = importedForm.querySelector('.language.selection.dropdown .text');
if (currentLangText && importedLangText) currentLangText.textContent = importedLangText.textContent;
}
for (const existingFlash of document.querySelectorAll('.flash-message')) {
existingFlash.remove();
}
const importedAlerts = parsed.querySelector('.ui.attached.segment > .ui.message.flash-message, .ui.attached.segment > .flash-message');
if (importedAlerts) {
const segment = document.querySelector('.ui.attached.segment');
if (segment) {
segment.insertBefore(importedAlerts.cloneNode(true), segment.firstChild);
dismissInstallSuccessFlash();
}
}
};
const importInstallFormState = async (form, action) => {
const formData = new FormData(form);
try {
const response = await fetch(action, {
method: 'POST',
body: formData,
credentials: 'same-origin',
});
const html = await response.text();
const parsed = new DOMParser().parseFromString(html, 'text/html');
syncInstallFormFromImportedResponse(form, parsed);
return true;
} catch {
form.action = action;
form.submit();
return false;
}
};
// end edit/add - by petru @ codex
if (installAppINIInput) {
installAppINIInput.addEventListener('change', async () => {
if (!installAppINIInput.files || installAppINIInput.files.length === 0) return;
const form = document.querySelector('.js-install-form');
if (!form) return;
const imported = await importInstallFormState(form, installAppINIInput.dataset.importAction); // edit/add - by petru @ codex
if (imported && importSensitiveSecretsCheckbox instanceof HTMLInputElement) {
importSensitiveSecretsCheckbox.checked = true; // edit/add - by petru @ codex
const checkboxContainer = importSensitiveSecretsCheckbox.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.add('checked');
}
if (imported) syncLauncherBackupState(); // edit/add - by petru @ codex
syncRecoveryFileResetButtons(); // edit/add - by petru @ codex
});
}
const setInstallTestMailButtonState = (button, state) => {
if (state === 'success') {
button.style.background = 'var(--color-green)';
button.style.color = 'var(--color-white)';
button.style.borderColor = 'var(--color-green)';
} else if (state === 'failed') {
button.style.background = 'var(--color-red)';
button.style.color = 'var(--color-white)';
button.style.borderColor = 'var(--color-red)';
} else {
button.style.background = 'var(--color-primary)';
button.style.color = 'var(--color-primary-contrast)';
button.style.borderColor = 'var(--color-primary)';
}
};
const deriveInstallMailerName = (appName) => {
const trimmed = appName.trim();
if (!trimmed) return 'Gitea';
const colonIndex = trimmed.indexOf(':');
if (colonIndex >= 0) {
const siteName = trimmed.slice(0, colonIndex).trim();
return siteName || 'Gitea';
}
return trimmed.split(/\s+/, 1)[0] || 'Gitea';
};
const installForm = document.querySelector('.js-install-form');
const recoveryLauncherButton = document.querySelector('.js-install-recovery-launcher');
const recoveryLauncherModal = document.querySelector('.js-install-recovery-launcher-modal');
const hiddenBackupRestoreID = installForm?.querySelector('#install_backup_restore_id');
const hiddenBackupImportAppINI = installForm?.querySelector('#install_backup_import_app_ini');
const hiddenBackupRestoreDB = installForm?.querySelector('#install_backup_restore_db');
const hiddenRecoveryMode = installForm?.querySelector('#install_recovery_mode');
const hiddenReinstallConfirmFirst = installForm?.querySelector('#install_reinstall_confirm_first');
const hiddenReinstallConfirmSecond = installForm?.querySelector('#install_reinstall_confirm_second');
const hiddenReinstallConfirmThird = installForm?.querySelector('#install_reinstall_confirm_third');
const launcherBackupRestoreID = recoveryLauncherModal?.querySelector('#launcher_backup_restore_id');
const launcherBackupImportAppINI = recoveryLauncherModal?.querySelector('#launcher_backup_import_app_ini');
const launcherBackupRestoreDB = recoveryLauncherModal?.querySelector('#launcher_backup_restore_db');
const launcherRepositoryFilesystem = recoveryLauncherModal?.querySelector('#launcher_repository_filesystem');
const launcherDatabaseBackupFile = recoveryLauncherModal?.querySelector('#database_backup_file');
const importSensitiveSecretsCheckbox = document.querySelector('#import_sensitive_secrets'); // edit/add - by petru @ codex
const resetAppINIButton = recoveryLauncherModal?.querySelector('.js-install-recovery-reset-app-ini');
const resetDatabaseBackupButton = recoveryLauncherModal?.querySelector('.js-install-recovery-reset-db-backup');
const launcherBackupActions = recoveryLauncherModal?.querySelector('.js-launcher-backup-actions');
const launcherBackupImportAppINIField = recoveryLauncherModal?.querySelector('.js-launcher-backup-import-app-ini-field');
const launcherBackupRestoreDBField = recoveryLauncherModal?.querySelector('.js-launcher-backup-restore-db-field');
const launcherImportAppINISegment = recoveryLauncherModal?.querySelector('.js-launcher-import-app-ini-segment');
const launcherDatabaseBackupFileSegment = recoveryLauncherModal?.querySelector('.js-launcher-database-backup-file-segment');
const launcherRepositoryFilesystemSegment = recoveryLauncherModal?.querySelector('.js-launcher-repository-filesystem-segment');
const launcherConfirmPanels = recoveryLauncherModal ? recoveryLauncherModal.querySelectorAll('.js-install-recovery-launcher-confirm-panel') : [];
const launcherSaveButton = recoveryLauncherModal?.querySelector('.js-install-recovery-launcher-save');
const appNameInput = installForm?.querySelector('#app_name');
const smtpFromNameInput = installForm?.querySelector('#smtp_from_name');
const installConfirmButtons = installForm ? installForm.querySelectorAll('.js-install-confirm-button') : [];
const setLauncherPanelVisible = (panel, visible) => {
panel.classList.toggle('tw-hidden', !visible);
for (const input of panel.querySelectorAll('input[type="checkbox"]')) {
if (!(input instanceof HTMLInputElement)) continue;
if (!visible) input.checked = false;
const checkboxContainer = input.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.toggle('checked', visible && input.checked);
}
};
const resetLauncherConfirmations = () => { // edit/add - by petru @ codex
for (const panel of launcherConfirmPanels) {
for (const input of panel.querySelectorAll('input[type="checkbox"]')) {
if (!(input instanceof HTMLInputElement)) continue;
input.checked = false;
const checkboxContainer = input.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.remove('checked');
}
}
if (hiddenReinstallConfirmFirst instanceof HTMLInputElement) hiddenReinstallConfirmFirst.value = '';
if (hiddenReinstallConfirmSecond instanceof HTMLInputElement) hiddenReinstallConfirmSecond.value = '';
if (hiddenReinstallConfirmThird instanceof HTMLInputElement) hiddenReinstallConfirmThird.value = '';
};
const clearImportedAppINIPreviewState = () => { // edit/add - by petru @ codex
if (!installForm) return;
const importedAppINIState = installForm.querySelector('input[name="imported_app_ini"]');
if (importedAppINIState instanceof HTMLInputElement) importedAppINIState.value = '';
const importedLFSJWTSecret = installForm.querySelector('input[name="imported_lfs_jwt_secret"]');
if (importedLFSJWTSecret instanceof HTMLInputElement) importedLFSJWTSecret.value = '';
const importedInternalToken = installForm.querySelector('input[name="imported_internal_token"]');
if (importedInternalToken instanceof HTMLInputElement) importedInternalToken.value = '';
const importedOAuth2JWTSecret = installForm.querySelector('input[name="imported_o_auth2_jwt_secret"]');
if (importedOAuth2JWTSecret instanceof HTMLInputElement) importedOAuth2JWTSecret.value = '';
if (importSensitiveSecretsCheckbox instanceof HTMLInputElement) {
importSensitiveSecretsCheckbox.checked = false; // edit/add - by petru @ codex
const checkboxContainer = importSensitiveSecretsCheckbox.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.remove('checked');
}
};
const syncLauncherSaveButton = () => {
if (!(launcherSaveButton instanceof HTMLButtonElement)) return;
const hasBundleSelected = launcherBackupRestoreID instanceof HTMLSelectElement && launcherBackupRestoreID.value !== '';
const hasSQLBackupSelected = launcherDatabaseBackupFile instanceof HTMLInputElement && !!launcherDatabaseBackupFile.files && launcherDatabaseBackupFile.files.length > 0;
const repositoryFilesystemSelected = launcherRepositoryFilesystem instanceof HTMLInputElement && launcherRepositoryFilesystem.checked;
const importedAppINIState = installForm?.querySelector('input[name="imported_app_ini"]'); // edit/add - by petru @ codex
const hasImportedAppINI = importedAppINIState instanceof HTMLInputElement && importedAppINIState.value === 'true'; // edit/add - by petru @ codex
const importSelected = launcherBackupImportAppINI instanceof HTMLInputElement && !launcherBackupImportAppINI.closest('.tw-hidden') && launcherBackupImportAppINI.checked;
const restoreSelected = launcherBackupRestoreDB instanceof HTMLInputElement && !launcherBackupRestoreDB.closest('.tw-hidden') && launcherBackupRestoreDB.checked;
let requiredPanel = null;
if (repositoryFilesystemSelected) {
requiredPanel = recoveryLauncherModal?.querySelector('[data-confirm-kind="repository_filesystem"]');
} else if (hasSQLBackupSelected || (hasBundleSelected && restoreSelected)) {
requiredPanel = recoveryLauncherModal?.querySelector('[data-confirm-kind="backup_restore"]');
} else if ((hasBundleSelected && (importSelected || restoreSelected)) || hasImportedAppINI) {
requiredPanel = recoveryLauncherModal?.querySelector('[data-confirm-kind="partial_restore"]');
}
if (!requiredPanel) {
if (hiddenReinstallConfirmFirst instanceof HTMLInputElement) hiddenReinstallConfirmFirst.value = '';
if (hiddenReinstallConfirmSecond instanceof HTMLInputElement) hiddenReinstallConfirmSecond.value = '';
if (hiddenReinstallConfirmThird instanceof HTMLInputElement) hiddenReinstallConfirmThird.value = '';
launcherSaveButton.disabled = (hasBundleSelected && !(importSelected || restoreSelected)) || (!hasBundleSelected && !hasSQLBackupSelected && !hasImportedAppINI);
return;
}
const confirmationInputs = requiredPanel.querySelectorAll('input[type="checkbox"]');
if (hiddenReinstallConfirmFirst instanceof HTMLInputElement) hiddenReinstallConfirmFirst.value = confirmationInputs[0] instanceof HTMLInputElement && confirmationInputs[0].checked ? 'true' : '';
if (hiddenReinstallConfirmSecond instanceof HTMLInputElement) hiddenReinstallConfirmSecond.value = confirmationInputs[1] instanceof HTMLInputElement && confirmationInputs[1].checked ? 'true' : '';
if (hiddenReinstallConfirmThird instanceof HTMLInputElement) hiddenReinstallConfirmThird.value = confirmationInputs[2] instanceof HTMLInputElement && confirmationInputs[2].checked ? 'true' : '';
launcherSaveButton.disabled = [...confirmationInputs].some((input) => !(input instanceof HTMLInputElement) || !input.checked);
};
const syncLauncherBackupState = () => {
if (hiddenBackupRestoreID instanceof HTMLInputElement && launcherBackupRestoreID instanceof HTMLSelectElement) {
hiddenBackupRestoreID.value = launcherBackupRestoreID.value;
}
if (hiddenBackupImportAppINI instanceof HTMLInputElement && launcherBackupImportAppINI instanceof HTMLInputElement) {
hiddenBackupImportAppINI.value = launcherBackupImportAppINI.checked ? 'true' : '';
}
if (hiddenBackupRestoreDB instanceof HTMLInputElement && launcherBackupRestoreDB instanceof HTMLInputElement) {
hiddenBackupRestoreDB.value = launcherBackupRestoreDB.checked ? 'true' : '';
}
const selectedBackupOption = launcherBackupRestoreID instanceof HTMLSelectElement ? launcherBackupRestoreID.selectedOptions[0] : null;
const hasBundleSelected = launcherBackupRestoreID instanceof HTMLSelectElement && launcherBackupRestoreID.value !== '';
const hasAppINIResource = !!selectedBackupOption && selectedBackupOption.dataset.hasAppIni === 'true';
const hasDatabaseResource = !!selectedBackupOption && selectedBackupOption.dataset.hasDatabase === 'true';
const repositoryFilesystemSelected = launcherRepositoryFilesystem instanceof HTMLInputElement && launcherRepositoryFilesystem.checked;
if (launcherBackupActions) {
launcherBackupActions.classList.toggle('tw-hidden', !hasBundleSelected);
}
if (launcherBackupImportAppINIField) {
launcherBackupImportAppINIField.classList.toggle('tw-hidden', !(hasBundleSelected && hasAppINIResource));
}
if (launcherBackupRestoreDBField) {
launcherBackupRestoreDBField.classList.toggle('tw-hidden', !(hasBundleSelected && hasDatabaseResource));
}
if (launcherBackupImportAppINI instanceof HTMLInputElement && !(hasBundleSelected && hasAppINIResource)) {
launcherBackupImportAppINI.checked = false;
const checkboxContainer = launcherBackupImportAppINI.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.remove('checked');
}
if (launcherBackupRestoreDB instanceof HTMLInputElement && !(hasBundleSelected && hasDatabaseResource)) {
launcherBackupRestoreDB.checked = false;
const checkboxContainer = launcherBackupRestoreDB.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.remove('checked');
}
if (repositoryFilesystemSelected) {
if (launcherBackupRestoreDB instanceof HTMLInputElement) {
launcherBackupRestoreDB.checked = false;
const checkboxContainer = launcherBackupRestoreDB.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.remove('checked');
}
if (launcherDatabaseBackupFile instanceof HTMLInputElement) {
launcherDatabaseBackupFile.value = '';
}
}
if (launcherImportAppINISegment) {
const hideImportAppINISection = hasBundleSelected && launcherBackupImportAppINI instanceof HTMLInputElement && launcherBackupImportAppINI.checked;
launcherImportAppINISegment.classList.toggle('tw-hidden', hideImportAppINISection);
if (hideImportAppINISection && installAppINIInput instanceof HTMLInputElement) {
installAppINIInput.value = '';
}
}
const importedAppINIState = installForm?.querySelector('input[name="imported_app_ini"]');
const hasImportedAppINI = importedAppINIState instanceof HTMLInputElement && importedAppINIState.value === 'true';
const forceExactAppINIImport = hasBundleSelected && launcherBackupImportAppINI instanceof HTMLInputElement && launcherBackupImportAppINI.checked;
if (importSensitiveSecretsCheckbox instanceof HTMLInputElement) {
if (forceExactAppINIImport) {
importSensitiveSecretsCheckbox.checked = true; // edit/add - by petru @ codex
}
importSensitiveSecretsCheckbox.disabled = forceExactAppINIImport;
const checkboxContainer = importSensitiveSecretsCheckbox.closest('.ui.checkbox');
if (checkboxContainer) {
checkboxContainer.classList.toggle('checked', importSensitiveSecretsCheckbox.checked);
checkboxContainer.classList.toggle('disabled', forceExactAppINIImport);
}
}
if (launcherDatabaseBackupFileSegment) {
const hideDatabaseBackupFileSection = hasBundleSelected && launcherBackupRestoreDB instanceof HTMLInputElement && launcherBackupRestoreDB.checked;
launcherDatabaseBackupFileSegment.classList.toggle('tw-hidden', hideDatabaseBackupFileSection);
if (hideDatabaseBackupFileSection && launcherDatabaseBackupFile instanceof HTMLInputElement) {
launcherDatabaseBackupFile.value = '';
}
}
const hasSQLBackupSelected = launcherDatabaseBackupFile instanceof HTMLInputElement && !!launcherDatabaseBackupFile.files && launcherDatabaseBackupFile.files.length > 0;
let activeConfirmKind = '';
if (repositoryFilesystemSelected) {
activeConfirmKind = 'repository_filesystem';
} else if (hasSQLBackupSelected || (hasBundleSelected && launcherBackupRestoreDB instanceof HTMLInputElement && launcherBackupRestoreDB.checked)) {
activeConfirmKind = 'backup_restore';
} else if ((hasBundleSelected && ((launcherBackupImportAppINI instanceof HTMLInputElement && launcherBackupImportAppINI.checked) || (launcherBackupRestoreDB instanceof HTMLInputElement && launcherBackupRestoreDB.checked))) || hasImportedAppINI) {
activeConfirmKind = 'partial_restore';
}
if (hiddenRecoveryMode instanceof HTMLInputElement) {
hiddenRecoveryMode.value = activeConfirmKind === 'backup_restore' ? 'database_backup' : activeConfirmKind === 'partial_restore' ? 'existing_database' : activeConfirmKind === 'repository_filesystem' ? 'repository_filesystem' : '';
}
if (launcherRepositoryFilesystemSegment) {
launcherRepositoryFilesystemSegment.classList.toggle('tw-hidden', hasSQLBackupSelected);
}
if (hasSQLBackupSelected && launcherRepositoryFilesystem instanceof HTMLInputElement) {
launcherRepositoryFilesystem.checked = false;
const checkboxContainer = launcherRepositoryFilesystem.closest('.ui.checkbox');
if (checkboxContainer) checkboxContainer.classList.remove('checked');
}
for (const panel of launcherConfirmPanels) {
setLauncherPanelVisible(panel, panel instanceof HTMLElement && panel.dataset.confirmKind === activeConfirmKind);
}
syncLauncherSaveButton();
};
// start edit/add - by petru @ codex
const syncInstallPanelFromSelectedBackupAppINI = async () => {
if (!(installForm instanceof HTMLFormElement)) return;
if (!(launcherBackupImportAppINI instanceof HTMLInputElement) || !launcherBackupImportAppINI.checked) return;
if (!(launcherBackupRestoreID instanceof HTMLSelectElement) || launcherBackupRestoreID.value === '') return;
const importAction = launcherBackupImportAppINI.dataset.importAction;
if (!importAction) return;
await importInstallFormState(installForm, importAction);
syncLauncherBackupState();
};
const syncRecoveryFileResetButtons = () => {
if (resetAppINIButton) {
const importedAppINIState = installForm?.querySelector('input[name="imported_app_ini"]'); // edit/add - by petru @ codex
const hasImportedAppINI = importedAppINIState instanceof HTMLInputElement && importedAppINIState.value === 'true'; // edit/add - by petru @ codex
const hasBundleImportedAppINI = hiddenBackupImportAppINI instanceof HTMLInputElement && hiddenBackupImportAppINI.value === 'true'; // edit/add - by petru @ codex
resetAppINIButton.classList.toggle('tw-hidden', !(hasImportedAppINI && !hasBundleImportedAppINI)); // edit/add - by petru @ codex
}
if (resetDatabaseBackupButton) {
const hasDatabaseBackupFile = launcherDatabaseBackupFile instanceof HTMLInputElement && !!launcherDatabaseBackupFile.files && launcherDatabaseBackupFile.files.length > 0;
resetDatabaseBackupButton.classList.toggle('tw-hidden', !hasDatabaseBackupFile);
}
};
// end edit/add - by petru @ codex
if (recoveryLauncherButton && recoveryLauncherModal) {
recoveryLauncherButton.addEventListener('click', () => {
window.$(recoveryLauncherModal).modal({
autofocus: false,
closable: true,
}).modal('show');
});
}
if (launcherBackupRestoreID instanceof HTMLSelectElement) {
launcherBackupRestoreID.addEventListener('change', async () => {
if (launcherBackupRestoreID.value === '') resetLauncherConfirmations(); // edit/add - by petru @ codex
syncLauncherBackupState();
await syncInstallPanelFromSelectedBackupAppINI();
});
}
if (launcherBackupImportAppINI instanceof HTMLInputElement) {
launcherBackupImportAppINI.addEventListener('change', async () => {
if (!launcherBackupImportAppINI.checked) resetLauncherConfirmations(); // edit/add - by petru @ codex
syncLauncherBackupState();
await syncInstallPanelFromSelectedBackupAppINI();
});
}
if (launcherBackupRestoreDB instanceof HTMLInputElement) {
launcherBackupRestoreDB.addEventListener('change', () => {
if (!launcherBackupRestoreDB.checked) resetLauncherConfirmations(); // edit/add - by petru @ codex
syncLauncherBackupState();
});
}
if (launcherRepositoryFilesystem instanceof HTMLInputElement) {
launcherRepositoryFilesystem.addEventListener('change', () => {
if (!launcherRepositoryFilesystem.checked) resetLauncherConfirmations(); // edit/add - by petru @ codex
syncLauncherBackupState();
syncRecoveryFileResetButtons(); // edit/add - by petru @ codex
});
}
if (launcherDatabaseBackupFile instanceof HTMLInputElement) {
launcherDatabaseBackupFile.addEventListener('change', () => {
if (!launcherDatabaseBackupFile.files || launcherDatabaseBackupFile.files.length === 0) resetLauncherConfirmations(); // edit/add - by petru @ codex
syncLauncherBackupState();
syncRecoveryFileResetButtons(); // edit/add - by petru @ codex
});
}
if (resetAppINIButton instanceof HTMLButtonElement && installAppINIInput instanceof HTMLInputElement) {
resetAppINIButton.addEventListener('click', () => {
installAppINIInput.value = ''; // edit/add - by petru @ codex
clearImportedAppINIPreviewState(); // edit/add - by petru @ codex
resetLauncherConfirmations(); // edit/add - by petru @ codex
syncLauncherBackupState(); // edit/add - by petru @ codex
syncRecoveryFileResetButtons(); // edit/add - by petru @ codex
});
}
if (resetDatabaseBackupButton instanceof HTMLButtonElement && launcherDatabaseBackupFile instanceof HTMLInputElement) {
resetDatabaseBackupButton.addEventListener('click', () => {
launcherDatabaseBackupFile.value = ''; // edit/add - by petru @ codex
resetLauncherConfirmations(); // edit/add - by petru @ codex
syncLauncherBackupState(); // edit/add - by petru @ codex
syncRecoveryFileResetButtons(); // edit/add - by petru @ codex
});
}
for (const panel of launcherConfirmPanels) {
for (const input of panel.querySelectorAll('input[type="checkbox"]')) {
if (!(input instanceof HTMLInputElement)) continue;
input.addEventListener('change', syncLauncherSaveButton);
}
}
syncLauncherBackupState();
syncRecoveryFileResetButtons(); // edit/add - by petru @ codex
if (appNameInput instanceof HTMLInputElement && smtpFromNameInput instanceof HTMLInputElement) {
let lastAutoMailerName = smtpFromNameInput.value.trim() || deriveInstallMailerName(appNameInput.value);
const syncInstallBranding = () => {
const siteName = deriveInstallMailerName(appNameInput.value);
const currentValue = smtpFromNameInput.value.trim();
if (!currentValue || currentValue === lastAutoMailerName) {
smtpFromNameInput.value = siteName;
}
lastAutoMailerName = siteName;
for (const installConfirmButton of installConfirmButtons) {
if (!(installConfirmButton instanceof HTMLButtonElement)) continue;
installConfirmButton.textContent = installConfirmButton.dataset.installLabelTemplate.replace('__SITE_NAME__', siteName);
}
};
syncInstallBranding();
appNameInput.addEventListener('input', syncInstallBranding);
appNameInput.addEventListener('change', syncInstallBranding);
} else if (appNameInput instanceof HTMLInputElement && installConfirmButtons.length > 0) {
const syncInstallButton = () => {
const siteName = deriveInstallMailerName(appNameInput.value);
for (const installConfirmButton of installConfirmButtons) {
if (!(installConfirmButton instanceof HTMLButtonElement)) continue;
installConfirmButton.textContent = installConfirmButton.dataset.installLabelTemplate.replace('__SITE_NAME__', siteName);
}
};
syncInstallButton();
appNameInput.addEventListener('input', syncInstallButton);
appNameInput.addEventListener('change', syncInstallButton);
}
for (const button of document.querySelectorAll('.js-install-test-mail-button')) {
button.addEventListener('click', async () => {
const form = button.closest('.js-install-form');
if (!form) return;
const message = form.querySelector('.js-install-test-mail-message');
const testMailInput = form.querySelector('#test_mail_email');
const adminEmailInput = form.querySelector('#admin_email');
if (testMailInput && !testMailInput.value.trim() && adminEmailInput?.value.trim()) {
testMailInput.value = adminEmailInput.value.trim();
}
button.disabled = true;
try {
const formData = new FormData(form);
const response = await fetch(button.getAttribute('data-action'), {
method: 'POST',
body: formData,
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) throw new Error(response.statusText);
const result = await response.json();
setInstallTestMailButtonState(button, result.state);
message.textContent = result.message;
message.style.color = result.state === 'success' ? 'var(--color-green)' : 'var(--color-red)';
if (result.state === 'success') {
window.setTimeout(() => {
setInstallTestMailButtonState(button, '');
}, 5000);
}
} catch {
setInstallTestMailButtonState(button, 'failed');
message.textContent = window.config.i18n.error_occurred;
message.style.color = 'var(--color-red)';
} finally {
button.disabled = false;
}
});
}
document.addEventListener('DOMContentLoaded', () => {
dismissInstallSuccessFlash();
{{if or .Err_DbInstalledBefore .Err_RepositoryFilesystemRecovery .Err_DatabaseBackupRecovery}}
if (recoveryLauncherModal) {
window.$(recoveryLauncherModal).modal({
autofocus: false,
closable: false,
}).modal('show');
}
{{end}}
const installLanguageBalloon = document.querySelector('#install-language-balloon');
const footerLanguageSelector = document.querySelector('#footer-language-selector');
if (!installLanguageBalloon || !footerLanguageSelector) return;
const hideInstallLanguageBalloon = () => {
installLanguageBalloon.classList.add('is-hidden');
};
window.setTimeout(() => {
installLanguageBalloon.classList.add('is-visible');
}, 0);
document.addEventListener('click', hideInstallLanguageBalloon, {once: true});
});
</script>
{{template "base/footer" .}}