Landing page (#22598)
|
@ -100,6 +100,38 @@
|
|||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Landing Page",
|
||||
"type": "gulp",
|
||||
"task": "develop-landing-page",
|
||||
"problemMatcher": {
|
||||
"owner": "ha-build",
|
||||
"source": "ha-build",
|
||||
"fileLocation": "absolute",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)",
|
||||
"severity": 1,
|
||||
"file": 2,
|
||||
"message": 3,
|
||||
"line": 4,
|
||||
"column": 5
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Changes detected. Starting compilation",
|
||||
"endsPattern": "Build done @"
|
||||
}
|
||||
},
|
||||
|
||||
"isBackground": true,
|
||||
"group": "build",
|
||||
"runOptions": {
|
||||
"instanceLimit": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Develop Demo",
|
||||
"type": "gulp",
|
||||
|
|
|
@ -327,4 +327,17 @@ module.exports.config = {
|
|||
},
|
||||
};
|
||||
},
|
||||
|
||||
landingPage({ isProdBuild, latestBuild }) {
|
||||
return {
|
||||
name: "landing-page" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
entrypoint: path.resolve(paths.landingPage_dir, "src/entrypoint.js"),
|
||||
},
|
||||
outputPath: outputPath(paths.landingPage_output_root, latestBuild),
|
||||
publicPath: publicPath(latestBuild),
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -38,3 +38,14 @@ gulp.task(
|
|||
])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-landing-page",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([
|
||||
paths.landingPage_output_root,
|
||||
paths.landingPage_build,
|
||||
paths.build_dir,
|
||||
])
|
||||
)
|
||||
);
|
||||
|
|
|
@ -257,6 +257,28 @@ gulp.task(
|
|||
)
|
||||
);
|
||||
|
||||
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-landing-page-dev",
|
||||
genPagesDevTask(
|
||||
LANDING_PAGE_PAGE_ENTRIES,
|
||||
paths.landingPage_dir,
|
||||
paths.landingPage_output_root
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-landing-page-prod",
|
||||
genPagesProdTask(
|
||||
LANDING_PAGE_PAGE_ENTRIES,
|
||||
paths.landingPage_dir,
|
||||
paths.landingPage_output_root,
|
||||
paths.landingPage_output_latest,
|
||||
paths.landingPage_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] };
|
||||
|
||||
gulp.task(
|
||||
|
|
|
@ -125,6 +125,11 @@ gulp.task("copy-translations-supervisor", async () => {
|
|||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-translations-landing-page", async () => {
|
||||
const staticDir = paths.landingPage_output_static;
|
||||
copyTranslations(staticDir);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-supervisor", async () => {
|
||||
const staticDir = paths.hassio_output_static;
|
||||
copyLocaleData(staticDir);
|
||||
|
@ -199,3 +204,14 @@ gulp.task("copy-static-gallery", async () => {
|
|||
copyLocaleData(paths.gallery_output_static);
|
||||
copyMdiIcons(paths.gallery_output_static);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-landing-page", async () => {
|
||||
// Copy landing-page static files
|
||||
fs.copySync(
|
||||
path.resolve(paths.landingPage_dir, "public"),
|
||||
paths.landingPage_output_root
|
||||
);
|
||||
|
||||
copyFonts(paths.landingPage_output_static);
|
||||
copyTranslations(paths.landingPage_output_static);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import gulp from "gulp";
|
||||
import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./translations.js";
|
||||
import "./webpack.js";
|
||||
|
||||
gulp.task(
|
||||
"develop-landing-page",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-landing-page",
|
||||
"translations-enable-merge-backend",
|
||||
"build-landing-page-translations",
|
||||
"copy-translations-landing-page",
|
||||
"build-locale-data",
|
||||
"copy-static-landing-page",
|
||||
"gen-pages-landing-page-dev",
|
||||
"webpack-watch-landing-page"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-landing-page",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-landing-page",
|
||||
"build-landing-page-translations",
|
||||
"copy-translations-landing-page",
|
||||
"build-locale-data",
|
||||
"copy-static-landing-page",
|
||||
"webpack-prod-landing-page",
|
||||
"gen-pages-landing-page-prod"
|
||||
)
|
||||
);
|
|
@ -172,12 +172,14 @@ const createMasterTranslation = () =>
|
|||
|
||||
const FRAGMENTS = ["base"];
|
||||
|
||||
const toggleSupervisorFragment = async () => {
|
||||
FRAGMENTS[0] = "supervisor";
|
||||
const setFragment = (fragment) => async () => {
|
||||
FRAGMENTS[0] = fragment;
|
||||
};
|
||||
|
||||
const panelFragment = (fragment) =>
|
||||
fragment !== "base" && fragment !== "supervisor";
|
||||
fragment !== "base" &&
|
||||
fragment !== "supervisor" &&
|
||||
fragment !== "landing-page";
|
||||
|
||||
const HASHES = new Map();
|
||||
|
||||
|
@ -224,6 +226,9 @@ const createTranslations = async () => {
|
|||
case "supervisor":
|
||||
// Supervisor key is at the top level
|
||||
return [flatten(data.supervisor), ""];
|
||||
case "landing-page":
|
||||
// landing-page key is at the top level
|
||||
return [flatten(data["landing-page"]), ""];
|
||||
default:
|
||||
// Create a fragment with only the given panel
|
||||
return [
|
||||
|
@ -322,5 +327,10 @@ gulp.task(
|
|||
|
||||
gulp.task(
|
||||
"build-supervisor-translations",
|
||||
gulp.series(toggleSupervisorFragment, "build-translations")
|
||||
gulp.series(setFragment("supervisor"), "build-translations")
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-landing-page-translations",
|
||||
gulp.series(setFragment("landing-page"), "build-translations")
|
||||
);
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
createDemoConfig,
|
||||
createGalleryConfig,
|
||||
createHassioConfig,
|
||||
createLandingPageConfig,
|
||||
} from "../webpack.cjs";
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) => [
|
||||
|
@ -41,6 +42,7 @@ const runDevServer = async ({
|
|||
contentBase,
|
||||
port,
|
||||
listenHost = undefined,
|
||||
proxy = undefined,
|
||||
}) => {
|
||||
if (listenHost === undefined) {
|
||||
// For dev container, we need to listen on all hosts
|
||||
|
@ -56,6 +58,7 @@ const runDevServer = async ({
|
|||
directory: contentBase,
|
||||
watch: true,
|
||||
},
|
||||
proxy,
|
||||
},
|
||||
compiler
|
||||
);
|
||||
|
@ -199,3 +202,30 @@ gulp.task("webpack-prod-gallery", () =>
|
|||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("webpack-watch-landing-page", () => {
|
||||
// This command will run forever because we don't close compiler
|
||||
webpack(
|
||||
process.env.ES5
|
||||
? bothBuilds(createLandingPageConfig, { isProdBuild: false })
|
||||
: createLandingPageConfig({ isProdBuild: false, latestBuild: true })
|
||||
).watch({ poll: isWsl }, doneHandler());
|
||||
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series(
|
||||
"build-landing-page-translations",
|
||||
"copy-translations-landing-page"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
gulp.task("webpack-prod-landing-page", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createLandingPageConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
isTestBuild: env.isTestBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
|
|
@ -33,6 +33,22 @@ module.exports = {
|
|||
),
|
||||
gallery_output_static: path.resolve(__dirname, "../gallery/dist/static"),
|
||||
|
||||
landingPage_dir: path.resolve(__dirname, "../landing-page"),
|
||||
landingPage_build: path.resolve(__dirname, "../landing-page/build"),
|
||||
landingPage_output_root: path.resolve(__dirname, "../landing-page/dist"),
|
||||
landingPage_output_latest: path.resolve(
|
||||
__dirname,
|
||||
"../landing-page/dist/frontend_latest"
|
||||
),
|
||||
landingPage_output_es5: path.resolve(
|
||||
__dirname,
|
||||
"../landing-page/dist/frontend_es5"
|
||||
),
|
||||
landingPage_output_static: path.resolve(
|
||||
__dirname,
|
||||
"../landing-page/dist/static"
|
||||
),
|
||||
|
||||
hassio_dir: path.resolve(__dirname, "../hassio"),
|
||||
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
|
||||
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
|
||||
|
|
|
@ -283,11 +283,15 @@ const createHassioConfig = ({
|
|||
const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createWebpackConfig(bundle.config.gallery({ isProdBuild, latestBuild }));
|
||||
|
||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createWebpackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
||||
|
||||
module.exports = {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createLandingPageConfig,
|
||||
createWebpackConfig,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# Home Assistant OS Landingpage
|
||||
|
||||
On initial startup of Home Assistant, HAOS needs to download Home Assistant core before the setup can start.
|
||||
In this time the [home-assistant/landingpage](https://github.com/home-assistant/landingpage) is serving a "Preparing Home Assistant" page.
|
||||
|
||||
## Functionality
|
||||
|
||||
- Progress bar to show download
|
||||
- Show / hide supervisor logs
|
||||
- Links
|
||||
- Read our Vision
|
||||
- Join our community
|
||||
- Download our app
|
||||
- DNS issue handler
|
||||
- if the supervisor is not able to connect to the internet
|
||||
- Show actions to set dns to google or cloudflare to resolve the issue
|
||||
- Error handler
|
||||
- if something with the installation goes wrong, we show the logs
|
||||
|
||||
## Develop
|
||||
|
||||
It is similar to the core frontend dev.
|
||||
|
||||
- frontend repo is building stuff
|
||||
- landingpage repo can set the frontend repo path and serve the dev frontend
|
||||
|
||||
### landingpage dev server
|
||||
|
||||
- clone [home-assistant/landingpage](https://github.com/home-assistant/landingpage)
|
||||
- Add frontend repo as mount to your devcontainer config
|
||||
- please do not commit this changes, you can remove it after initial dev container build, because the build will keep the options as long as you don't rebuild it.
|
||||
- `"mounts": ["source=/path/to/hass/frontend,target=/workspaces/frontend,type=bind,consistency=cached"]`
|
||||
- use the dev container
|
||||
- start the dev server with following optional env vars:
|
||||
- `SUPERVISOR_HOST` to have real supervisor data, you can [setup a supervisor remote API access](https://developers.home-assistant.io/docs/supervisor/development/#supervisor-api-access) and set the host of your supervisor. e.g.: `SUPERVISOR_HOST=192.168.0.20:8888`
|
||||
- `SUPERVISOR_TOKEN` the supervisor api token you get from the Remote API proxy Addon Logs
|
||||
- `FRONTEND_PATH` the path inside your container should be `/workspaces/frontend`
|
||||
- example: `SUPERVISOR_TOKEN=abc123 SUPERVISOR_HOST=192.168.0.20:8888 FRONTEND_PATH=/workspaces/frontend go run main.go http.go mdns.go`
|
||||
- You can also add this into your devcontainer settings, but then it's not so flexible to change if you want to test something else.
|
||||
|
||||
### frontend dev server
|
||||
|
||||
- install all dependencies
|
||||
- run `landing-page/script/develop`
|
|
@ -0,0 +1,8 @@
|
|||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
...rootConfig,
|
||||
{
|
||||
rules: {},
|
||||
},
|
||||
];
|
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="92" height="136" viewBox="0 0 92 136" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M76.38 104.9H3.03C2.24231 104.9 1.48688 105.213 0.929897 105.77C0.372914 106.327 0.0600052 107.082 0.0600052 107.87V132.47C0.0600052 133.258 0.372914 134.013 0.929897 134.57C1.48688 135.127 2.24231 135.44 3.03 135.44H12.36C13.1477 135.44 13.9031 135.127 14.4601 134.57C15.0171 134.013 15.33 133.258 15.33 132.47V120.17H76.39V132.47C76.39 133.258 76.7029 134.013 77.2599 134.57C77.8169 135.127 78.5723 135.44 79.36 135.44H88.69C89.4777 135.44 90.2331 135.127 90.7901 134.57C91.3471 134.013 91.66 133.258 91.66 132.47V107.87C91.66 107.082 91.3471 106.327 90.7901 105.77C90.2331 105.213 89.4777 104.9 88.69 104.9H76.39H76.38ZM50.04 2.24996C47.73 -0.0600439 43.95 -0.0600439 41.65 2.24996L4.25 39.65C1.94 41.96 0.0500031 46.52 0.0500031 49.78V83.7C0.0500031 86.96 2.72 89.64 5.99 89.64H85.7C88.96 89.64 91.64 86.97 91.64 83.7V49.78C91.64 46.52 89.75 41.96 87.44 39.65L50.04 2.24996Z" fill="#18BCF2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1011 B |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,10 @@
|
|||
<svg width="75" height="79" viewBox="0 0 75 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M73.8393 17.4898C72.6973 9.00165 65.2994 2.31235 56.5296 1.01614C55.05 0.797115 49.4441 0 36.4582 0H36.3612C23.3717 0 20.585 0.797115 19.1054 1.01614C10.5798 2.27644 2.79399 8.28712 0.904997 16.8758C-0.00358524 21.1056 -0.100549 25.7949 0.0682394 30.0965C0.308852 36.2651 0.355538 42.423 0.91577 48.5665C1.30307 52.6474 1.97872 56.6957 2.93763 60.6812C4.73325 68.042 12.0019 74.1676 19.1233 76.6666C26.7478 79.2728 34.9474 79.7055 42.8039 77.9162C43.6682 77.7151 44.5217 77.4817 45.3645 77.216C47.275 76.6092 49.5123 75.9305 51.1571 74.7385C51.1797 74.7217 51.1982 74.7001 51.2112 74.6753C51.2243 74.6504 51.2316 74.6229 51.2325 74.5948V68.6416C51.2321 68.6154 51.2259 68.5896 51.2142 68.5661C51.2025 68.5426 51.1858 68.522 51.1651 68.5058C51.1444 68.4896 51.1204 68.4783 51.0948 68.4726C51.0692 68.4669 51.0426 68.467 51.0171 68.4729C45.9835 69.675 40.8254 70.2777 35.6502 70.2682C26.7439 70.2682 24.3486 66.042 23.6626 64.2826C23.1113 62.762 22.7612 61.1759 22.6212 59.5646C22.6197 59.5375 22.6247 59.5105 22.6357 59.4857C22.6466 59.4609 22.6633 59.4391 22.6843 59.422C22.7053 59.4048 22.73 59.3929 22.7565 59.3871C22.783 59.3813 22.8104 59.3818 22.8367 59.3886C27.7864 60.5826 32.8604 61.1853 37.9522 61.1839C39.1768 61.1839 40.3978 61.1839 41.6224 61.1516C46.7435 61.008 52.1411 60.7459 57.1796 59.7621C57.3053 59.7369 57.431 59.7154 57.5387 59.6831C65.4861 58.157 73.0493 53.3672 73.8178 41.2381C73.8465 40.7606 73.9184 36.2364 73.9184 35.7409C73.9219 34.0569 74.4606 23.7949 73.8393 17.4898Z" fill="url(#paint0_linear_549_34)"/>
|
||||
<path d="M61.2484 27.0263V48.114H52.8916V27.6475C52.8916 23.3388 51.096 21.1413 47.4437 21.1413C43.4287 21.1413 41.4177 23.7409 41.4177 28.8755V40.0782H33.1111V28.8755C33.1111 23.7409 31.0965 21.1413 27.0815 21.1413C23.4507 21.1413 21.6371 23.3388 21.6371 27.6475V48.114H13.2839V27.0263C13.2839 22.7176 14.384 19.2946 16.5843 16.7572C18.8539 14.2258 21.8311 12.926 25.5264 12.926C29.8036 12.926 33.0357 14.5705 35.1905 17.8559L37.2698 21.346L39.3527 17.8559C41.5074 14.5705 44.7395 12.926 49.0095 12.926C52.7013 12.926 55.6784 14.2258 57.9553 16.7572C60.1531 19.2922 61.2508 22.7152 61.2484 27.0263Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_549_34" x1="37.0692" y1="0" x2="37.0692" y2="79" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6364FF"/>
|
||||
<stop offset="1" stop-color="#563ACC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="1200" height="1227" viewBox="0 0 1200 1227" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 430 B |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 69 KiB |
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
# Run the landing-page
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp build-landing-page
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
# Run the landing-page
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
|
||||
./node_modules/.bin/gulp develop-landing-page
|
|
@ -0,0 +1,334 @@
|
|||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
||||
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type {
|
||||
LandingPageKeys,
|
||||
LocalizeFunc,
|
||||
} from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-icon-button";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import "../../../src/components/ha-ansi-to-html";
|
||||
import "../../../src/components/ha-alert";
|
||||
import type { HaAnsiToHtml } from "../../../src/components/ha-ansi-to-html";
|
||||
import {
|
||||
getObserverLogs,
|
||||
downloadUrl as observerLogsDownloadUrl,
|
||||
} from "../data/observer";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { fileDownload } from "../../../src/util/file_download";
|
||||
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
|
||||
|
||||
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"landing-page-error": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const SCHEDULE_FETCH_OBSERVER_LOGS = 5;
|
||||
|
||||
@customElement("landing-page-logs")
|
||||
class LandingPageLogs extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
@query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml;
|
||||
|
||||
@query(".logs") private _logElement?: HTMLElement;
|
||||
|
||||
@query("#scroll-bottom-marker")
|
||||
private _scrollBottomMarkerElement?: HTMLElement;
|
||||
|
||||
@state() private _show = false;
|
||||
|
||||
@state() private _scrolledToBottomController =
|
||||
new IntersectionController<boolean>(this, {
|
||||
callback(this: IntersectionController<boolean>, entries) {
|
||||
return entries[0].isIntersecting;
|
||||
},
|
||||
});
|
||||
|
||||
@state() private _error = false;
|
||||
|
||||
@state() private _newLogsIndicator?: boolean;
|
||||
|
||||
@state() private _logLinesCount = 0;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="actions">
|
||||
<ha-button @click=${this._toggleLogDetails}>
|
||||
${this.localize(this._show ? "hide_details" : "show_details")}
|
||||
</ha-button>
|
||||
${this._show
|
||||
? html`<ha-icon-button
|
||||
.label=${this.localize("logs.download_logs")}
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadLogs}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.localize("logs.fetch_error")}
|
||||
>
|
||||
<ha-button @click=${this._startLogStream}>
|
||||
${this.localize("logs.retry")}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class=${classMap({
|
||||
logs: true,
|
||||
hidden: !this._show,
|
||||
})}
|
||||
>
|
||||
<ha-ansi-to-html></ha-ansi-to-html>
|
||||
<div id="scroll-bottom-marker"></div>
|
||||
</div>
|
||||
<ha-button
|
||||
class="new-logs-indicator ${classMap({
|
||||
visible:
|
||||
(this._show &&
|
||||
this._newLogsIndicator &&
|
||||
!this._scrolledToBottomController.value) ||
|
||||
false,
|
||||
})}"
|
||||
@click=${this._scrollToBottom}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowCollapseDown} slot="icon"></ha-svg-icon>
|
||||
${this.localize("logs.scroll_down_button")}
|
||||
<ha-svg-icon
|
||||
.path=${mdiArrowCollapseDown}
|
||||
slot="trailingIcon"
|
||||
></ha-svg-icon>
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._scrolledToBottomController.observe(this._scrollBottomMarkerElement!);
|
||||
|
||||
this._startLogStream();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (this._newLogsIndicator && this._scrolledToBottomController.value) {
|
||||
this._newLogsIndicator = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("_show") && this._show) {
|
||||
this._scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleLogDetails() {
|
||||
this._show = !this._show;
|
||||
}
|
||||
|
||||
private _scrollToBottom(): void {
|
||||
if (this._logElement) {
|
||||
this._newLogsIndicator = false;
|
||||
this._logElement!.scrollTo(0, this._logElement!.scrollHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private _displayLogs(logs: string, tempLogLine = "", clear = false): string {
|
||||
if (clear) {
|
||||
this._ansiToHtmlElement?.clear();
|
||||
this._logLinesCount = 0;
|
||||
}
|
||||
|
||||
const showError = ERROR_CHECK.test(logs);
|
||||
|
||||
const scrolledToBottom = this._scrolledToBottomController.value;
|
||||
const lines = `${tempLogLine}${logs}`
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
|
||||
// handle edge case where the last line is not complete
|
||||
if (logs.endsWith("\n")) {
|
||||
tempLogLine = "";
|
||||
} else {
|
||||
tempLogLine = lines.splice(-1, 1)[0];
|
||||
}
|
||||
|
||||
if (lines.length) {
|
||||
this._ansiToHtmlElement?.parseLinesToColoredPre(lines);
|
||||
this._logLinesCount += lines.length;
|
||||
}
|
||||
|
||||
if (showError) {
|
||||
fireEvent(this, "landing-page-error");
|
||||
this._show = true;
|
||||
}
|
||||
|
||||
if (showError || (scrolledToBottom && this._logElement)) {
|
||||
this._scrollToBottom();
|
||||
} else {
|
||||
this._newLogsIndicator = true;
|
||||
}
|
||||
|
||||
return tempLogLine;
|
||||
}
|
||||
|
||||
private async _startLogStream() {
|
||||
this._error = false;
|
||||
this._newLogsIndicator = false;
|
||||
this._ansiToHtmlElement?.clear();
|
||||
|
||||
try {
|
||||
const response = await getSupervisorLogsFollow();
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error("No stream body found");
|
||||
}
|
||||
|
||||
let tempLogLine = "";
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let done = false;
|
||||
|
||||
while (!done) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { value, done: readerDone } = await reader.read();
|
||||
done = readerDone;
|
||||
|
||||
if (value) {
|
||||
const chunk = decoder.decode(value, { stream: !done });
|
||||
tempLogLine = this._displayLogs(chunk, tempLogLine);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
||||
// fallback to observerlogs if there is a problem with supervisor
|
||||
this._loadObserverLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleObserverLogs() {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// check if supervisor logs are available
|
||||
const superVisorLogsResponse = await getSupervisorLogs(1);
|
||||
if (superVisorLogsResponse.ok) {
|
||||
this._startLogStream();
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore and continue with observer logs
|
||||
}
|
||||
this._loadObserverLogs();
|
||||
}, SCHEDULE_FETCH_OBSERVER_LOGS * 1000);
|
||||
}
|
||||
|
||||
private async _loadObserverLogs() {
|
||||
try {
|
||||
const response = await getObserverLogs();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Error fetching observer logs");
|
||||
}
|
||||
|
||||
const logs = await response.text();
|
||||
|
||||
this._displayLogs(logs, "", true);
|
||||
|
||||
this._scheduleObserverLogs();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._error = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _downloadLogs() {
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
|
||||
fileDownload(observerLogsDownloadUrl, `observer_${timeString}.log`);
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-alert {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions ha-icon-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -4px;
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.logs {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.logs.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.new-logs-indicator {
|
||||
--mdc-theme-primary: var(--text-primary-color);
|
||||
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
|
||||
transition: height 0.4s ease-out;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.new-logs-indicator.visible {
|
||||
height: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"landing-page-logs": LandingPageLogs;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import {
|
||||
type CSSResultGroup,
|
||||
LitElement,
|
||||
type PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
LandingPageKeys,
|
||||
LocalizeFunc,
|
||||
} from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-alert";
|
||||
import {
|
||||
ALTERNATIVE_DNS_SERVERS,
|
||||
getSupervisorNetworkInfo,
|
||||
setSupervisorNetworkDns,
|
||||
} from "../data/supervisor";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
|
||||
const SCHEDULE_FETCH_NETWORK_INFO_SECONDS = 5;
|
||||
|
||||
@customElement("landing-page-network")
|
||||
class LandingPageNetwork extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public localize!: LocalizeFunc<LandingPageKeys>;
|
||||
|
||||
@state() private _networkIssue = false;
|
||||
|
||||
@state() private _getNetworkInfoError = false;
|
||||
|
||||
@state() private _dnsPrimaryInterface?: string;
|
||||
|
||||
protected render() {
|
||||
if (!this._networkIssue && !this._getNetworkInfoError) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (this._getNetworkInfoError) {
|
||||
return html`
|
||||
<ha-alert alert-type="error">
|
||||
<p>${this.localize("network_issue.error_get_network_info")}</p>
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.localize("network_issue.title")}
|
||||
>
|
||||
<p>
|
||||
${this.localize("network_issue.description", {
|
||||
dns: this._dnsPrimaryInterface || "?",
|
||||
})}
|
||||
</p>
|
||||
<p>${this.localize("network_issue.resolve_different")}</p>
|
||||
${!this._dnsPrimaryInterface
|
||||
? html`
|
||||
<p>
|
||||
<b>${this.localize("network_issue.no_primary_interface")} </b>
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
<div class="actions">
|
||||
${ALTERNATIVE_DNS_SERVERS.map(
|
||||
({ translationKey }, key) =>
|
||||
html`<ha-button
|
||||
.index=${key}
|
||||
.disabled=${!this._dnsPrimaryInterface}
|
||||
@click=${this._setDns}
|
||||
>${this.localize(translationKey)}</ha-button
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this._fetchSupervisorInfo();
|
||||
}
|
||||
|
||||
private _scheduleFetchSupervisorInfo() {
|
||||
setTimeout(
|
||||
() => this._fetchSupervisorInfo(),
|
||||
SCHEDULE_FETCH_NETWORK_INFO_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private async _fetchSupervisorInfo() {
|
||||
let data;
|
||||
try {
|
||||
const response = await getSupervisorNetworkInfo();
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch network info");
|
||||
}
|
||||
|
||||
({ data } = await response.json());
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
this._getNetworkInfoError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this._getNetworkInfoError = false;
|
||||
|
||||
if (!data.host_internet) {
|
||||
this._networkIssue = true;
|
||||
const primaryInterface = data.interfaces.find(
|
||||
(intf) => intf.primary && intf.enabled
|
||||
);
|
||||
if (primaryInterface) {
|
||||
this._dnsPrimaryInterface = [
|
||||
...(primaryInterface.ipv4?.nameservers || []),
|
||||
...(primaryInterface.ipv6?.nameservers || []),
|
||||
].join(", ");
|
||||
}
|
||||
} else {
|
||||
this._networkIssue = false;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: this._networkIssue,
|
||||
});
|
||||
this._scheduleFetchSupervisorInfo();
|
||||
}
|
||||
|
||||
private async _setDns(ev) {
|
||||
const index = ev.target?.index;
|
||||
try {
|
||||
const response = await setSupervisorNetworkDns(index);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to set DNS");
|
||||
}
|
||||
this._networkIssue = false;
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
showAlertDialog(this, {
|
||||
title: this.localize("network_issue.failed"),
|
||||
warning: true,
|
||||
text: `${this.localize(
|
||||
"network_issue.set_dns_failed"
|
||||
)}${err?.message ? ` ${this.localize("network_issue.error")}: ${err.message}` : ""}`,
|
||||
confirmText: this.localize("network_issue.close"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"landing-page-network": LandingPageNetwork;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export async function getObserverLogs() {
|
||||
return fetch("/observer/logs");
|
||||
}
|
||||
|
||||
export const downloadUrl = "/observer/logs";
|
|
@ -0,0 +1,56 @@
|
|||
import type { LandingPageKeys } from "../../../src/common/translations/localize";
|
||||
|
||||
export const ALTERNATIVE_DNS_SERVERS: {
|
||||
ipv4: string[];
|
||||
ipv6: string[];
|
||||
translationKey: LandingPageKeys;
|
||||
}[] = [
|
||||
{
|
||||
ipv4: ["1.1.1.1", "1.0.0.1"],
|
||||
ipv6: ["2606:4700:4700::1111", "2606:4700:4700::1001"],
|
||||
translationKey: "network_issue.use_cloudflare",
|
||||
},
|
||||
{
|
||||
ipv4: ["8.8.8.8", "8.8.4.4"],
|
||||
ipv6: ["2001:4860:4860::8888", "2001:4860:4860::8844"],
|
||||
translationKey: "network_issue.use_google",
|
||||
},
|
||||
];
|
||||
|
||||
export async function getSupervisorLogs(lines = 100) {
|
||||
return fetch(`/supervisor/supervisor/logs?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSupervisorLogsFollow(lines = 500) {
|
||||
return fetch(`/supervisor/supervisor/logs/follow?lines=${lines}`, {
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSupervisorNetworkInfo() {
|
||||
return fetch("/supervisor/network/info");
|
||||
}
|
||||
|
||||
export const setSupervisorNetworkDns = async (dnsServerIndex: number) =>
|
||||
fetch("/supervisor/network/dns", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
ipv4: {
|
||||
method: "auto",
|
||||
nameservers: ALTERNATIVE_DNS_SERVERS[dnsServerIndex].ipv4,
|
||||
},
|
||||
ipv6: {
|
||||
method: "auto",
|
||||
nameservers: ALTERNATIVE_DNS_SERVERS[dnsServerIndex].ipv6,
|
||||
},
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import "./ha-landing-page";
|
||||
|
||||
import("../../src/resources/ha-style");
|
|
@ -0,0 +1,178 @@
|
|||
import "@material/mwc-linear-progress";
|
||||
import { type PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../src/components/ha-alert";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import "../../src/onboarding/onboarding-welcome-links";
|
||||
import "./components/landing-page-network";
|
||||
import "./components/landing-page-logs";
|
||||
import { extractSearchParam } from "../../src/common/url/search-params";
|
||||
import { onBoardingStyles } from "../../src/onboarding/styles";
|
||||
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
|
||||
import { LandingPageBaseElement } from "./landing-page-base-element";
|
||||
|
||||
const SCHEDULE_CORE_CHECK_SECONDS = 5;
|
||||
|
||||
@customElement("ha-landing-page")
|
||||
class HaLandingPage extends LandingPageBaseElement {
|
||||
@property({ attribute: false }) public translationFragment = "landing-page";
|
||||
|
||||
@state() private _networkIssue = false;
|
||||
|
||||
@state() private _supervisorError = false;
|
||||
|
||||
private _mobileApp =
|
||||
extractSearchParam("redirect_uri") === "homeassistant://auth-callback";
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<h1>${this.localize("header")}</h1>
|
||||
${!this._networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<mwc-linear-progress indeterminate></mwc-linear-progress>
|
||||
`
|
||||
: nothing}
|
||||
<landing-page-network
|
||||
@value-changed=${this._networkInfoChanged}
|
||||
.localize=${this.localize}
|
||||
></landing-page-network>
|
||||
|
||||
${this._supervisorError
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.localize("error_title")}
|
||||
>
|
||||
${this.localize("error_description")}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<landing-page-logs
|
||||
.localize=${this.localize}
|
||||
@landing-page-error=${this._showError}
|
||||
></landing-page-logs>
|
||||
</div>
|
||||
</ha-card>
|
||||
<onboarding-welcome-links
|
||||
.localize=${this.localize}
|
||||
.mobileApp=${this._mobileApp}
|
||||
></onboarding-welcome-links>
|
||||
<div class="footer">
|
||||
<ha-language-picker
|
||||
.value=${this.language}
|
||||
.label=${""}
|
||||
nativeName
|
||||
@value-changed=${this._languageChanged}
|
||||
></ha-language-picker>
|
||||
<a
|
||||
href="https://www.home-assistant.io/getting-started/onboarding/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize("ui.panel.page-onboarding.help")}</a
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
makeDialogManager(this, this.shadowRoot!);
|
||||
|
||||
if (window.innerWidth > 450) {
|
||||
import("../../src/resources/particles");
|
||||
}
|
||||
import("../../src/components/ha-language-picker");
|
||||
|
||||
this._scheduleCoreCheck();
|
||||
}
|
||||
|
||||
private _scheduleCoreCheck() {
|
||||
setTimeout(
|
||||
() => this._checkCoreAvailability(),
|
||||
SCHEDULE_CORE_CHECK_SECONDS * 1000
|
||||
);
|
||||
}
|
||||
|
||||
private async _checkCoreAvailability() {
|
||||
try {
|
||||
const response = await fetch("/manifest.json");
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
}
|
||||
} finally {
|
||||
this._scheduleCoreCheck();
|
||||
}
|
||||
}
|
||||
|
||||
private _showError() {
|
||||
this._supervisorError = true;
|
||||
}
|
||||
|
||||
private _networkInfoChanged(ev: CustomEvent) {
|
||||
this._networkIssue = ev.detail.value;
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
const language = ev.detail.value;
|
||||
if (language !== this.language && language) {
|
||||
this.language = language;
|
||||
try {
|
||||
localStorage.setItem("selectedLanguage", JSON.stringify(language));
|
||||
} catch (err: any) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
onBoardingStyles,
|
||||
css`
|
||||
.footer {
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-card .card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
ha-alert p {
|
||||
text-align: unset;
|
||||
}
|
||||
ha-language-picker {
|
||||
display: block;
|
||||
width: 200px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
--ha-select-height: 40px;
|
||||
--mdc-select-fill-color: none;
|
||||
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-idle-line-color: transparent;
|
||||
--mdc-select-hover-line-color: transparent;
|
||||
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-landing-page": HaLandingPage;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Home Assistant</title>
|
||||
<%= renderTemplate("../../../src/html/_header.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_style_base.html.template") %>
|
||||
<style>
|
||||
html {
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
color: var(--primary-text-color, #212121);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: var(--primary-background-color, #111111);
|
||||
color: var(--primary-text-color, #e1e1e1);
|
||||
}
|
||||
}
|
||||
body {
|
||||
height: auto;
|
||||
padding: 32px 0;
|
||||
}
|
||||
.content {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.header img {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body id="particles">
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<img src="/static/icons/favicon-192x192.png" alt="Home Assistant" />
|
||||
</div>
|
||||
<ha-landing-page></ha-landing-page>
|
||||
</div>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
|
||||
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,74 @@
|
|||
import type { PropertyValues } from "lit";
|
||||
import { LitElement } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import {
|
||||
computeLocalize,
|
||||
type LandingPageKeys,
|
||||
type LocalizeFunc,
|
||||
} from "../../src/common/translations/localize";
|
||||
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import type { Constructor, Resources } from "../../src/types";
|
||||
import {
|
||||
getLocalLanguage,
|
||||
getTranslation,
|
||||
} from "../../src/util/common-translation";
|
||||
import { computeDirectionStyles } from "../../src/common/util/compute_rtl";
|
||||
import themesMixin from "../../src/state/themes-mixin";
|
||||
import { translationMetadata } from "../../src/resources/translations-metadata";
|
||||
import type { HassBaseEl } from "../../src/state/hass-base-mixin";
|
||||
|
||||
export class LandingPageBaseElement extends themesMixin(
|
||||
ProvideHassLitMixin(LitElement) as unknown as Constructor<HassBaseEl>
|
||||
) {
|
||||
// Initialized to empty will prevent undefined errors if called before connected to DOM.
|
||||
@property({ attribute: false })
|
||||
public localize: LocalizeFunc<LandingPageKeys> = () => "";
|
||||
|
||||
// Use browser language setup before login.
|
||||
@property() public language?: string = getLocalLanguage();
|
||||
|
||||
@state() private _resources?: Resources;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._initializeLocalize();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
if (changedProperties.get("language")) {
|
||||
this._resources = undefined;
|
||||
this._initializeLocalize();
|
||||
}
|
||||
|
||||
if (
|
||||
this.language &&
|
||||
this._resources &&
|
||||
(changedProperties.has("language") || changedProperties.has("_resources"))
|
||||
) {
|
||||
this._setLocalize();
|
||||
}
|
||||
}
|
||||
|
||||
private async _initializeLocalize() {
|
||||
if (this._resources || !this.language) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await getTranslation(null, this.language);
|
||||
this._resources = {
|
||||
[this.language]: data,
|
||||
};
|
||||
}
|
||||
|
||||
private async _setLocalize() {
|
||||
this.localize = await computeLocalize(
|
||||
this.constructor.prototype,
|
||||
this.language!,
|
||||
this._resources!
|
||||
);
|
||||
computeDirectionStyles(
|
||||
translationMetadata.translations[this.language!].isRTL,
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import webpack from "../build-scripts/webpack.cjs";
|
||||
import env from "../build-scripts/env.cjs";
|
||||
|
||||
export default webpack.createLandingPageConfig({
|
||||
isProdBuild: env.isProdBuild(),
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
latestBuild: true,
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="92" height="136" viewBox="0 0 92 136" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M76.38 104.9H3.03C2.24231 104.9 1.48688 105.213 0.929897 105.77C0.372914 106.327 0.0600052 107.082 0.0600052 107.87V132.47C0.0600052 133.258 0.372914 134.013 0.929897 134.57C1.48688 135.127 2.24231 135.44 3.03 135.44H12.36C13.1477 135.44 13.9031 135.127 14.4601 134.57C15.0171 134.013 15.33 133.258 15.33 132.47V120.17H76.39V132.47C76.39 133.258 76.7029 134.013 77.2599 134.57C77.8169 135.127 78.5723 135.44 79.36 135.44H88.69C89.4777 135.44 90.2331 135.127 90.7901 134.57C91.3471 134.013 91.66 133.258 91.66 132.47V107.87C91.66 107.082 91.3471 106.327 90.7901 105.77C90.2331 105.213 89.4777 104.9 88.69 104.9H76.39H76.38ZM50.04 2.24996C47.73 -0.0600439 43.95 -0.0600439 41.65 2.24996L4.25 39.65C1.94 41.96 0.0500031 46.52 0.0500031 49.78V83.7C0.0500031 86.96 2.72 89.64 5.99 89.64H85.7C88.96 89.64 91.64 86.97 91.64 83.7V49.78C91.64 46.52 89.75 41.96 87.44 39.65L50.04 2.24996Z" fill="#18BCF2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1011 B |
|
@ -0,0 +1,10 @@
|
|||
<svg width="75" height="79" viewBox="0 0 75 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M73.8393 17.4898C72.6973 9.00165 65.2994 2.31235 56.5296 1.01614C55.05 0.797115 49.4441 0 36.4582 0H36.3612C23.3717 0 20.585 0.797115 19.1054 1.01614C10.5798 2.27644 2.79399 8.28712 0.904997 16.8758C-0.00358524 21.1056 -0.100549 25.7949 0.0682394 30.0965C0.308852 36.2651 0.355538 42.423 0.91577 48.5665C1.30307 52.6474 1.97872 56.6957 2.93763 60.6812C4.73325 68.042 12.0019 74.1676 19.1233 76.6666C26.7478 79.2728 34.9474 79.7055 42.8039 77.9162C43.6682 77.7151 44.5217 77.4817 45.3645 77.216C47.275 76.6092 49.5123 75.9305 51.1571 74.7385C51.1797 74.7217 51.1982 74.7001 51.2112 74.6753C51.2243 74.6504 51.2316 74.6229 51.2325 74.5948V68.6416C51.2321 68.6154 51.2259 68.5896 51.2142 68.5661C51.2025 68.5426 51.1858 68.522 51.1651 68.5058C51.1444 68.4896 51.1204 68.4783 51.0948 68.4726C51.0692 68.4669 51.0426 68.467 51.0171 68.4729C45.9835 69.675 40.8254 70.2777 35.6502 70.2682C26.7439 70.2682 24.3486 66.042 23.6626 64.2826C23.1113 62.762 22.7612 61.1759 22.6212 59.5646C22.6197 59.5375 22.6247 59.5105 22.6357 59.4857C22.6466 59.4609 22.6633 59.4391 22.6843 59.422C22.7053 59.4048 22.73 59.3929 22.7565 59.3871C22.783 59.3813 22.8104 59.3818 22.8367 59.3886C27.7864 60.5826 32.8604 61.1853 37.9522 61.1839C39.1768 61.1839 40.3978 61.1839 41.6224 61.1516C46.7435 61.008 52.1411 60.7459 57.1796 59.7621C57.3053 59.7369 57.431 59.7154 57.5387 59.6831C65.4861 58.157 73.0493 53.3672 73.8178 41.2381C73.8465 40.7606 73.9184 36.2364 73.9184 35.7409C73.9219 34.0569 74.4606 23.7949 73.8393 17.4898Z" fill="url(#paint0_linear_549_34)"/>
|
||||
<path d="M61.2484 27.0263V48.114H52.8916V27.6475C52.8916 23.3388 51.096 21.1413 47.4437 21.1413C43.4287 21.1413 41.4177 23.7409 41.4177 28.8755V40.0782H33.1111V28.8755C33.1111 23.7409 31.0965 21.1413 27.0815 21.1413C23.4507 21.1413 21.6371 23.3388 21.6371 27.6475V48.114H13.2839V27.0263C13.2839 22.7176 14.384 19.2946 16.5843 16.7572C18.8539 14.2258 21.8311 12.926 25.5264 12.926C29.8036 12.926 33.0357 14.5705 35.1905 17.8559L37.2698 21.346L39.3527 17.8559C41.5074 14.5705 44.7395 12.926 49.0095 12.926C52.7013 12.926 55.6784 14.2258 57.9553 16.7572C60.1531 19.2922 61.2508 22.7152 61.2484 27.0263Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_549_34" x1="37.0692" y1="0" x2="37.0692" y2="79" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6364FF"/>
|
||||
<stop offset="1" stop-color="#563ACC"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -33,6 +33,10 @@ export type LocalizeKeys =
|
|||
| `ui.panel.page-authorize.form.${string}`
|
||||
| `component.${string}`;
|
||||
|
||||
export type LandingPageKeys = FlattenObjectKeys<
|
||||
TranslationDict["landing-page"]
|
||||
>;
|
||||
|
||||
// Tweaked from https://www.raygesualdo.com/posts/flattening-object-keys-with-typescript-types
|
||||
export type FlattenObjectKeys<
|
||||
T extends Record<string, any>,
|
||||
|
|
|
@ -77,18 +77,16 @@ class DialogApp extends LitElement {
|
|||
--mdc-dialog-min-width: min(500px, 90vw);
|
||||
}
|
||||
.app-qr {
|
||||
margin: 24px auto 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
gap: 16px;
|
||||
gap: 32px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
.app-qr a,
|
||||
.app-qr img {
|
||||
flex: 1;
|
||||
max-width: 180px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,11 @@ class DialogCommunity extends LitElement {
|
|||
href="https://community.home-assistant.io/"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/icons/favicon-192x192.png" slot="graphic" />
|
||||
<img
|
||||
src="/static/icons/favicon-192x192.png"
|
||||
slot="graphic"
|
||||
alt="Home Assistant Logo"
|
||||
/>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.forums")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
@ -51,7 +55,11 @@ class DialogCommunity extends LitElement {
|
|||
href="https://newsletter.openhomefoundation.org/"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/icons/favicon-192x192.png" slot="graphic" />
|
||||
<img
|
||||
src="/static/icons/logo_ohf.svg"
|
||||
slot="graphic"
|
||||
alt="Open Home Foundation Logo"
|
||||
/>
|
||||
${this.localize(
|
||||
"ui.panel.page-onboarding.welcome.open_home_newsletter"
|
||||
)}
|
||||
|
@ -64,7 +72,11 @@ class DialogCommunity extends LitElement {
|
|||
href="https://www.home-assistant.io/join-chat"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/images/logo_discord.png" slot="graphic" />
|
||||
<img
|
||||
src="/static/images/logo_discord.png"
|
||||
slot="graphic"
|
||||
alt="Discord Logo"
|
||||
/>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.discord")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
@ -72,11 +84,15 @@ class DialogCommunity extends LitElement {
|
|||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://x.com/home_assistant"
|
||||
href="https://fosstodon.org/@homeassistant"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img class="x" src="/static/images/logo_x.svg" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.x")}
|
||||
<img
|
||||
src="/static/images/logo_mastodon.svg"
|
||||
slot="graphic"
|
||||
alt="Mastodon Logo"
|
||||
/>
|
||||
${this.localize("ui.panel.page-onboarding.welcome.mastodon")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
|
@ -96,12 +112,6 @@ class DialogCommunity extends LitElement {
|
|||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
img.x {
|
||||
filter: invert(1) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import "./onboarding-welcome-link";
|
|||
class OnboardingWelcomeLinks extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc<any>;
|
||||
|
||||
@property({ type: Boolean }) public mobileApp = false;
|
||||
|
||||
|
|
|
@ -7382,6 +7382,7 @@
|
|||
"open_home_newsletter": "Building the Open Home newsletter",
|
||||
"discord": "Discord chat",
|
||||
"x": "[%key:ui::panel::config::tips::join_x%]",
|
||||
"mastodon": "Mastodon",
|
||||
"playstore": "Get it on Google Play",
|
||||
"appstore": "Download on the App Store"
|
||||
},
|
||||
|
@ -7530,6 +7531,51 @@
|
|||
"key_m_hint": "Press 'm' on any page to get the My Home Assistant link"
|
||||
}
|
||||
},
|
||||
"landing-page": {
|
||||
"header": "Preparing Home Assistant",
|
||||
"subheader": "This may take 20 minutes or more",
|
||||
"show_details": "Show details",
|
||||
"hide_details": "Hide details",
|
||||
"network_issue": {
|
||||
"title": "Networking issue detected",
|
||||
"error_get_network_info": "Cannot get network information",
|
||||
"description": "Home Assistant OS detected a networking issue in your setup. As part of the initial setup, Home Assistant OS downloads the latest version of Home Assistant Core. This networking issue prevents this download. The network issue might be DNS related. The currently used DNS service is: {dns}.",
|
||||
"resolve_different": "To resolve this, you can try a different DNS server. Select one of the options below. Alternatively, change your router configuration to use your own custom DNS server.",
|
||||
"use_cloudflare": "Use Cloudflare DNS",
|
||||
"use_google": "Use Google DNS",
|
||||
"no_primary_interface": "Home Assistant OS wasn't able to detect a primary network interface, so you cannot define a DNS server!",
|
||||
"failed": "Failed",
|
||||
"set_dns_failed": "An error occurred while setting the DNS server. Please check the logs for more information and try again.",
|
||||
"error": "Error",
|
||||
"close": "[%key:ui::common::close%]"
|
||||
},
|
||||
"logs": {
|
||||
"scroll_down_button": "New logs - Click to scroll",
|
||||
"fetch_error": "Failed to fetch logs",
|
||||
"retry": "Retry",
|
||||
"download_logs": "[%key:ui::panel::config::logs::download_logs%]"
|
||||
},
|
||||
"error_title": "Error installing Home Assistant",
|
||||
"error_description": "An error occurred while installing Home Assistant. Please check the logs for more information.",
|
||||
"ui": {
|
||||
"panel": {
|
||||
"page-onboarding": {
|
||||
"welcome": {
|
||||
"vision": "[%key:ui::panel::page-onboarding::welcome::vision%]",
|
||||
"community": "[%key:ui::panel::page-onboarding::welcome::community%]",
|
||||
"download_app": "[%key:ui::panel::page-onboarding::welcome::download_app%]",
|
||||
"forums": "[%key:ui::panel::page-onboarding::welcome::forums%]",
|
||||
"open_home_newsletter": "[%key:ui::panel::page-onboarding::welcome::open_home_newsletter%]",
|
||||
"discord": "[%key:ui::panel::page-onboarding::welcome::discord%]",
|
||||
"mastodon": "[%key:ui::panel::page-onboarding::welcome::mastodon%]",
|
||||
"playstore": "[%key:ui::panel::page-onboarding::welcome::playstore%]",
|
||||
"appstore": "[%key:ui::panel::page-onboarding::welcome::appstore%]"
|
||||
},
|
||||
"help": "[%key:ui::panel::page-onboarding::help%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"supervisor": {
|
||||
"addon": {
|
||||
"failed_to_reset": "Failed to reset add-on configuration, {error}",
|
||||
|
|